@abelxiaoxing/cadence 1.4.0 → 1.5.0

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 (66) hide show
  1. package/README.md +130 -8
  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/config/plan-draft.quick.example.json +78 -0
  8. package/package.json +10 -4
  9. package/prompts/abel-design.md +101 -44
  10. package/prompts/abel-implement.md +9 -0
  11. package/src/activity-contracts.ts +82 -0
  12. package/src/candidate-artifact.ts +143 -0
  13. package/src/candidate-patch.ts +31 -11
  14. package/src/candidate-workspace.ts +74 -0
  15. package/src/change-contract-schema.ts +191 -0
  16. package/src/change-contract.ts +5 -0
  17. package/src/change-verification.ts +920 -0
  18. package/src/child-budget.ts +58 -0
  19. package/src/child-model.ts +142 -0
  20. package/src/child-session.ts +289 -326
  21. package/src/contracts.ts +200 -76
  22. package/src/delivery-compiler.ts +105 -120
  23. package/src/delivery-revision.ts +42 -0
  24. package/src/design-control.ts +113 -4
  25. package/src/design-diagnostics.ts +150 -0
  26. package/src/durable-contracts.ts +213 -0
  27. package/src/durable-workflow.ts +148 -3570
  28. package/src/evidence-draft.ts +122 -0
  29. package/src/execution-profile.ts +38 -0
  30. package/src/implement-graph.ts +3 -0
  31. package/src/implement-plan.ts +70 -0
  32. package/src/index.ts +59 -522
  33. package/src/isolation-backend.ts +62 -40
  34. package/src/model-source.ts +22 -0
  35. package/src/operator-cli-entry.mjs +24 -0
  36. package/src/operator-cli.mjs +1421 -0
  37. package/src/operator-tools.ts +249 -0
  38. package/src/package-candidate.ts +227 -0
  39. package/src/package-verification.ts +86 -8
  40. package/src/package-workflow.ts +291 -0
  41. package/src/packet-runtime.ts +27 -48
  42. package/src/parent-provider.ts +86 -198
  43. package/src/phase-execution.ts +2248 -0
  44. package/src/pi-adapter.ts +19 -0
  45. package/src/plan-draft-summary.ts +149 -0
  46. package/src/plan-draft.ts +493 -0
  47. package/src/single-task-draft.ts +142 -0
  48. package/src/subagent-activity.ts +32 -66
  49. package/src/submit-tool.ts +20 -59
  50. package/src/transport-budget.ts +52 -0
  51. package/src/verification-capability.ts +5 -3
  52. package/src/verification-diagnostics.ts +76 -0
  53. package/src/verification-environment-io.ts +280 -0
  54. package/src/verification-environment.ts +56 -154
  55. package/src/verification-scan.ts +64 -0
  56. package/src/worker-broker.ts +39 -57
  57. package/src/workflow-policy.ts +14 -53
  58. package/src/workflow-recovery-policy.ts +95 -0
  59. package/src/workflow-scheduling.ts +105 -0
  60. package/src/workflow-state-machine.ts +105 -497
  61. package/src/workflow-status.ts +384 -0
  62. package/src/workspace-io-entry.mjs +19 -1
  63. package/src/workspace-io-worker.mjs +275 -50
  64. package/src/workspace-io.ts +6 -1
  65. package/src/empty-resource-loader.ts +0 -38
  66. 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,25 @@ 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";
36
+ import { expandSingleTaskDraft } from "./single-task-draft.ts";
26
37
  import {
27
38
  bindDraftVerificationInputs,
28
39
  validateVerificationAdapterCapability,
@@ -103,40 +114,6 @@ export class DeliveryValidationError extends Error {
103
114
  }
104
115
  }
105
116
 
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
117
  const SAFE_PLAN_ERROR_CODE = /^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$/u;
141
118
 
142
119
  function planErrorCode(error: unknown): string {
@@ -182,6 +159,22 @@ export function assessDeliveryTraceability(input: {
182
159
  plan: ImplementPlan;
183
160
  }): DeliveryTraceabilityAssessment {
184
161
  const diagnostics = new Set<string>();
162
+ let authorMarkdown = input.tasksMarkdown;
163
+ let bindings: string | undefined;
164
+ try {
165
+ const region = taskVerificationRegion(input.tasksMarkdown);
166
+ if (region) {
167
+ authorMarkdown =
168
+ input.tasksMarkdown.slice(0, region.start) +
169
+ input.tasksMarkdown.slice(region.end);
170
+ bindings = region.text;
171
+ if (bindings !== renderTasks(input.plan)) {
172
+ diagnostics.add("traceability-managed-region-stale");
173
+ }
174
+ }
175
+ } catch {
176
+ return { ok: false, diagnostics: ["traceability-managed-region-invalid"] };
177
+ }
185
178
  const headings = new Set<string>();
186
179
  for (const spec of input.specs) {
187
180
  if (!isValidRelativePath(spec.path) || !spec.path.endsWith("/spec.md")) {
@@ -206,7 +199,7 @@ export function assessDeliveryTraceability(input: {
206
199
  }
207
200
  }
208
201
  const references = [
209
- ...input.tasksMarkdown.matchAll(/`(specs\/[^`\n]+\/spec\.md#[^`\n]+)`/gu),
202
+ ...authorMarkdown.matchAll(/`(specs\/[^`\n]+\/spec\.md#[^`\n]+)`/gu),
210
203
  ].flatMap((match) => (match[1] ? [match[1].trim()] : []));
211
204
  if (references.length === 0) diagnostics.add("traceability-reference-empty");
212
205
  const referenceSet = new Set(references);
@@ -220,9 +213,7 @@ export function assessDeliveryTraceability(input: {
220
213
  diagnostics.add("traceability-scenario-unowned");
221
214
  }
222
215
  const taskIds = input.plan.tasks.map((task) => task.taskId).sort();
223
- if (
224
- taskIds.some((taskId) => !input.tasksMarkdown.includes(`\`${taskId}\``))
225
- ) {
216
+ if (taskIds.some((taskId) => !authorMarkdown.includes(`\`${taskId}\``))) {
226
217
  diagnostics.add("traceability-task-unmapped");
227
218
  }
228
219
  const verificationIds = input.plan.tasks
@@ -236,7 +227,8 @@ export function assessDeliveryTraceability(input: {
236
227
  .sort();
237
228
  if (
238
229
  verificationIds.some(
239
- (verificationId) => !input.tasksMarkdown.includes(verificationId),
230
+ (verificationId) =>
231
+ !(bindings ?? authorMarkdown).includes(verificationId),
240
232
  )
241
233
  ) {
242
234
  diagnostics.add("traceability-verification-unmapped");
@@ -260,70 +252,16 @@ export function assessDeliveryTraceability(input: {
260
252
  };
261
253
  }
262
254
 
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;
255
+ export type { DesignPlanDiagnostic } from "./design-diagnostics.ts";
256
+ export { DesignPlanValidationError } from "./design-diagnostics.ts";
257
+ export type {
258
+ ImplementPlan,
259
+ PlanAgentsCheckpointOperation,
260
+ PlanTaskDraft,
261
+ PlanTracking,
262
+ PlanVerification,
263
+ } from "./implement-plan.ts";
264
+ export type { PlanDraft, PlanPhaseDraft, PlanTaskInput } from "./plan-draft.ts";
327
265
 
328
266
  export interface CompiledDelivery {
329
267
  plan: ImplementPlan;
@@ -690,7 +628,7 @@ function normalizeVerification(
690
628
  return cloned;
691
629
  }
692
630
 
693
- function normalizePhase(value: unknown, readOnly = false): PlanPhaseDraft {
631
+ function normalizePhase(value: unknown, readOnly = false): PhaseBoundary {
694
632
  if (
695
633
  !isRecord(value) ||
696
634
  !exactKeys(value, [
@@ -837,7 +775,7 @@ function normalizeTask(value: unknown): PlanTaskDraft {
837
775
  }
838
776
 
839
777
  function graphFromPlan(plan: ImplementPlan): ImplementGraphBoundary {
840
- const stripPhase = (phase: PlanPhaseDraft): PhaseBoundary => ({
778
+ const stripPhase = (phase: PhaseBoundary): PhaseBoundary => ({
841
779
  read: phase.read,
842
780
  write: phase.write,
843
781
  delete: phase.delete,
@@ -1177,7 +1115,7 @@ function normalizeDraft(value: unknown): ImplementPlan {
1177
1115
  if (!draftGraph.ok) {
1178
1116
  throw new DesignPlanValidationError(
1179
1117
  `delivery-plan-invalid:${draftGraph.reason}`,
1180
- [draftGraph.diagnostic],
1118
+ draftGraph.diagnostics ?? [draftGraph.diagnostic],
1181
1119
  );
1182
1120
  }
1183
1121
  const plan: ImplementPlan = {
@@ -1247,32 +1185,79 @@ function normalizeDraft(value: unknown): ImplementPlan {
1247
1185
  return plan;
1248
1186
  }
1249
1187
 
1188
+ const TASK_BINDINGS_START = "<!-- ABEL:VERIFICATION-BINDINGS:START -->";
1189
+ const TASK_BINDINGS_END = "<!-- ABEL:VERIFICATION-BINDINGS:END -->";
1190
+
1191
+ function taskVerificationRegion(markdown: string) {
1192
+ const markers = markdown.match(/<!-- ABEL:VERIFICATION-BINDINGS/gu) ?? [];
1193
+ if (markers.length === 0) return undefined;
1194
+ const match =
1195
+ /^<!-- ABEL:VERIFICATION-BINDINGS:START -->\r?\n[\s\S]*?^<!-- ABEL:VERIFICATION-BINDINGS:END -->(?=\r?$)/mu.exec(
1196
+ markdown,
1197
+ );
1198
+ if (markers.length !== 2 || !match) {
1199
+ throw new DesignPlanValidationError("traceability-managed-region-invalid", [
1200
+ { code: "traceability-managed-region-invalid", path: "tasks.md" },
1201
+ ]);
1202
+ }
1203
+ return {
1204
+ start: match.index,
1205
+ end: match.index + match[0].length,
1206
+ text: match[0],
1207
+ };
1208
+ }
1209
+
1210
+ // Only identity bindings are generated. Author task checkboxes and Scenario
1211
+ // ownership remain outside this region and are validated independently.
1250
1212
  function renderTasks(plan: ImplementPlan): string {
1251
- const lines = ["# Implementation tasks", ""];
1252
- for (const task of plan.tasks) {
1253
- lines.push(`- [ ] ${task.taskId} — ${task.objective}`);
1213
+ const lines = [TASK_BINDINGS_START, "## Compiled verification bindings", ""];
1214
+ for (const task of [...plan.tasks].sort((a, b) =>
1215
+ compareCanonicalStrings(a.taskId, b.taskId),
1216
+ )) {
1254
1217
  lines.push(
1255
- ` - Depends on: ${task.dependsOn.length > 0 ? task.dependsOn.join(", ") : "[]"}`,
1218
+ `- Task \`${task.taskId}\` (${task.verificationMode ?? "behavior"})`,
1256
1219
  );
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}`);
1220
+ if (!task.verificationMode || task.verificationMode === "behavior") {
1221
+ lines.push(` - Red: \`${task.phases.red.verification.id}\``);
1222
+ }
1223
+ lines.push(` - Green: \`${task.phases.green.verification.id}\``);
1224
+ if (task.phases.refactor) {
1225
+ lines.push(` - Refactor: \`${task.phases.refactor.verification.id}\``);
1226
+ }
1264
1227
  }
1265
- lines.push("");
1228
+ lines.push(TASK_BINDINGS_END);
1266
1229
  return lines.join("\n");
1267
1230
  }
1268
1231
 
1232
+ export function bindTaskVerifications(
1233
+ markdown: string,
1234
+ plan: ImplementPlan,
1235
+ ): string {
1236
+ const region = taskVerificationRegion(markdown);
1237
+ const bindings = renderTasks(plan);
1238
+ if (region) {
1239
+ return (
1240
+ markdown.slice(0, region.start) + bindings + markdown.slice(region.end)
1241
+ );
1242
+ }
1243
+ return `${markdown}${markdown.endsWith("\n") ? "\n" : "\n\n"}${bindings}\n`;
1244
+ }
1245
+
1269
1246
  export function compileImplementPlan(
1270
1247
  draft: unknown,
1271
1248
  options: { consumerRoot: string; bindExecutionInputs?: boolean },
1272
1249
  ): CompiledDelivery {
1273
1250
  try {
1251
+ draft = expandSingleTaskDraft(draft);
1252
+ // Resolve omitted manifest command bytes before named definitions are
1253
+ // validated and purpose identities are hashed. The second pass below
1254
+ // binds execution inputs on the expanded contracts with their final IDs.
1255
+ if (options.bindExecutionInputs)
1256
+ draft = bindDraftVerificationInputs(options.consumerRoot, draft);
1257
+ draft = expandPlanDraft(draft);
1274
1258
  if (options.bindExecutionInputs)
1275
1259
  draft = bindDraftVerificationInputs(options.consumerRoot, draft);
1260
+ draft = preparePlanDraft(draft);
1276
1261
  const normalized = normalizeDraft(draft);
1277
1262
  const plan =
1278
1263
  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"),