@abelxiaoxing/cadence 1.2.1 → 1.2.3

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.
@@ -30,7 +30,6 @@ import {
30
30
  import type { GateApprovalProof } from "./delivery-compiler.ts";
31
31
  import {
32
32
  DeliveryValidationError,
33
- IMPLEMENT_PLAN_SCHEMA_VERSION,
34
33
  type ImplementPlan,
35
34
  type PlanTaskDraft,
36
35
  } from "./delivery-compiler.ts";
@@ -45,7 +44,13 @@ import { type OperationLease, RunStore } from "./run-store.ts";
45
44
  import { observeSafePath } from "./safe-path.ts";
46
45
  import type { ResolvedStateRoot } from "./state-root.ts";
47
46
  import type { WorkflowActivityUpdate } from "./subagent-activity.ts";
48
- import type { CandidateArtifactSubmission } from "./submit-tool.ts";
47
+ import {
48
+ type CandidateArtifactSubmission,
49
+ type CandidateContextBoundary,
50
+ type CandidateContextRef,
51
+ classifyCandidateContextRequest,
52
+ normalizeCandidateContextRefs,
53
+ } from "./submit-tool.ts";
49
54
  import {
50
55
  type BeginCandidateInput,
51
56
  TASK_LEDGER_LIMITS,
@@ -70,11 +75,72 @@ const LOCKFILES = new Set([
70
75
  "pnpm-lock.yaml",
71
76
  "yarn.lock",
72
77
  ]);
73
- const IMPLEMENTATION_ROUTE_REQUIREMENTS = Object.freeze({
74
- minContextWindow: 128_000,
75
- minOutputTokens: 64_000,
78
+ const IMPLEMENTATION_ROUTE_LIMITS = Object.freeze({
79
+ preferredContextWindow: 128_000,
80
+ preferredOutputTokens: 64_000,
81
+ minimumContextWindow: 16_000,
82
+ minimumOutputTokens: 8_000,
76
83
  });
77
84
 
85
+ function implementationRouteRequirements(
86
+ input: {
87
+ task?: PlanTaskDraft;
88
+ artifactCorrection?: RedArtifactCorrection;
89
+ repair?: { failureIdentities: string[] };
90
+ } = {},
91
+ ) {
92
+ if (!input.task) {
93
+ return {
94
+ minContextWindow: IMPLEMENTATION_ROUTE_LIMITS.minimumContextWindow,
95
+ minOutputTokens: IMPLEMENTATION_ROUTE_LIMITS.minimumOutputTokens,
96
+ };
97
+ }
98
+ const serializedBytes = Buffer.byteLength(canonicalJson(input.task), "utf8");
99
+ const pathCount = Object.values(input.task.phases).reduce(
100
+ (total, phase) =>
101
+ total + phase.read.length + phase.write.length + phase.delete.length,
102
+ 0,
103
+ );
104
+ const complexity =
105
+ serializedBytes +
106
+ pathCount * 512 +
107
+ (input.artifactCorrection ? 16_384 : 0) +
108
+ (input.repair ? input.repair.failureIdentities.length * 256 + 16_384 : 0);
109
+ return {
110
+ minContextWindow: Math.min(
111
+ IMPLEMENTATION_ROUTE_LIMITS.preferredContextWindow,
112
+ Math.max(
113
+ IMPLEMENTATION_ROUTE_LIMITS.minimumContextWindow,
114
+ 16_384 + complexity * 2,
115
+ ),
116
+ ),
117
+ minOutputTokens: Math.min(
118
+ IMPLEMENTATION_ROUTE_LIMITS.preferredOutputTokens,
119
+ Math.max(
120
+ IMPLEMENTATION_ROUTE_LIMITS.minimumOutputTokens,
121
+ 8_192 + Math.ceil(complexity / 2),
122
+ ),
123
+ ),
124
+ };
125
+ }
126
+
127
+ const SEMANTIC_ROUTE_FAILURE_CODES = new Set([
128
+ "child-no-structural-submit",
129
+ "invalid-structural-result",
130
+ "structural-identity-mismatch",
131
+ "invalid-diff",
132
+ "candidate-diff-invalid",
133
+ ]);
134
+
135
+ function semanticRouteFailure(
136
+ value: DurableCandidateProposal,
137
+ ): string | undefined {
138
+ return (value.kind === "retryable" || value.kind === "paused") &&
139
+ SEMANTIC_ROUTE_FAILURE_CODES.has(value.code)
140
+ ? value.code
141
+ : undefined;
142
+ }
143
+
78
144
  function isCancellationException(error: unknown, signal: AbortSignal): boolean {
79
145
  if (!signal.aborted) return false;
80
146
  if (error === signal.reason) return true;
@@ -131,6 +197,7 @@ const ENGINE_SCHEMA = `
131
197
  phase TEXT NOT NULL,
132
198
  pause_code TEXT,
133
199
  context_request_json TEXT,
200
+ attempt_diagnostic_json TEXT,
134
201
  route_id TEXT,
135
202
  route_fingerprint TEXT,
136
203
  queue_position INTEGER,
@@ -171,7 +238,14 @@ interface WorkflowContextRequest {
171
238
  | "approved-context-needed"
172
239
  | "task-split-needed"
173
240
  | "boundary-review-needed";
174
- refs: string[];
241
+ refs: CandidateContextRef[];
242
+ }
243
+
244
+ interface RedArtifactCorrection {
245
+ code: "red-artifact-constraint";
246
+ attempt: number;
247
+ maxAttempts: number;
248
+ contextRequest?: WorkflowContextRequest;
175
249
  }
176
250
 
177
251
  export const APPROVAL_AUTHORITY_CATEGORIES = [
@@ -277,7 +351,9 @@ export type WorkflowAttemptOutcome = (
277
351
  | { kind: "operation-cancelled"; code: "cancelled" }
278
352
  ) &
279
353
  WorkflowWorkspaceFacts &
280
- WorkflowRouteFacts;
354
+ WorkflowRouteFacts & {
355
+ attemptDiagnostic?: SafeAttemptDiagnostic;
356
+ };
281
357
 
282
358
  export interface WorkflowWorker {
283
359
  runAttempt(input: {
@@ -296,6 +372,8 @@ export interface WorkflowWorker {
296
372
  attribution: "introduced";
297
373
  failureIdentities: string[];
298
374
  };
375
+ artifactCorrection?: RedArtifactCorrection;
376
+ contextRequest?: WorkflowContextRequest;
299
377
  signal: AbortSignal;
300
378
  onActivity?: (
301
379
  event: Pick<
@@ -307,6 +385,7 @@ export interface WorkflowWorker {
307
385
  rebind(input: {
308
386
  runId: string;
309
387
  taskId: string;
388
+ task: PlanTaskDraft;
310
389
  role: "implementation-worker";
311
390
  routeId: string;
312
391
  }):
@@ -340,7 +419,6 @@ export interface WorkflowWorker {
340
419
  }
341
420
 
342
421
  export interface WorkflowDelivery {
343
- version: 2;
344
422
  gate: "gate-a" | "gate-b";
345
423
  revision: number;
346
424
  receiptHash: string;
@@ -480,11 +558,22 @@ interface EngineTaskRow {
480
558
  phase: "red" | "green" | "refactor";
481
559
  pause_code: string | null;
482
560
  context_request_json: string | null;
561
+ attempt_diagnostic_json: string | null;
483
562
  route_id: string | null;
484
563
  route_fingerprint: string | null;
485
564
  queue_position: number | null;
486
565
  }
487
566
 
567
+ interface SafeAttemptDiagnostic {
568
+ fingerprint?: string;
569
+ finalCategory?: string;
570
+ submitAttempts?: number;
571
+ schema?: string;
572
+ identityMismatch?: string[];
573
+ sameFailureCount?: number;
574
+ action?: "rebind-or-revise-delivery";
575
+ }
576
+
488
577
  interface EngineOperationRow {
489
578
  command: string;
490
579
  state: string;
@@ -568,24 +657,72 @@ function normalizeWorkflowContextRequest(
568
657
  "approved-context-needed",
569
658
  "task-split-needed",
570
659
  "boundary-review-needed",
571
- ].includes(String(value.code)) ||
572
- !Array.isArray(value.refs) ||
573
- value.refs.length === 0 ||
574
- value.refs.length > 32 ||
575
- value.refs.some(
576
- (relative) =>
577
- typeof relative !== "string" || !isValidRelativePath(relative),
578
- ) ||
579
- new Set(value.refs).size !== value.refs.length
660
+ ].includes(String(value.code))
580
661
  ) {
581
662
  throw new Error("workflow-context-request-invalid");
582
663
  }
664
+ let refs: CandidateContextRef[];
665
+ try {
666
+ refs = normalizeCandidateContextRefs(value.refs);
667
+ } catch {
668
+ throw new Error("workflow-context-request-invalid");
669
+ }
583
670
  return {
584
671
  code: value.code as WorkflowContextRequest["code"],
585
- refs: [...(value.refs as string[])],
672
+ refs,
586
673
  };
587
674
  }
588
675
 
676
+ function contextApprovalRefs(request: WorkflowContextRequest): string[] {
677
+ return request.refs.flatMap((ref) =>
678
+ ref.kind === "requested-path" ? [ref.path] : [],
679
+ );
680
+ }
681
+
682
+ function contextBoundaryForTask(
683
+ task: PlanTaskDraft,
684
+ phase: "red" | "green" | "refactor",
685
+ ): CandidateContextBoundary {
686
+ const boundary = task.phases[phase];
687
+ if (!boundary) throw new Error("workflow-task-phase-invalid");
688
+ return {
689
+ phase,
690
+ readPaths: boundary.read,
691
+ writePaths: boundary.write,
692
+ deletePaths: boundary.delete,
693
+ taskPaths: [
694
+ ...new Set(
695
+ Object.values(task.phases).flatMap((candidate) => [
696
+ ...candidate.read,
697
+ ...candidate.write,
698
+ ...candidate.delete,
699
+ ]),
700
+ ),
701
+ ],
702
+ redWritePaths: task.phases.red.write,
703
+ agents: {
704
+ impact: task.agents.impact,
705
+ ...(task.agents.target ? { target: task.agents.target } : {}),
706
+ },
707
+ };
708
+ }
709
+
710
+ function classifyPersistedContextRequest(
711
+ request: WorkflowContextRequest,
712
+ task: PlanTaskDraft,
713
+ phase: "red" | "green" | "refactor",
714
+ ) {
715
+ return classifyCandidateContextRequest(
716
+ {
717
+ kind: "context-request",
718
+ candidateId: "candidate-durable-reclassification",
719
+ code: request.code,
720
+ refs: request.refs,
721
+ },
722
+ contextBoundaryForTask(task, phase),
723
+ );
724
+ }
725
+
589
726
  function approvalRequirement(
590
727
  code: string,
591
728
  contextRequest?: WorkflowContextRequest,
@@ -599,7 +736,7 @@ function approvalRequirement(
599
736
  return {
600
737
  category: requirement.category,
601
738
  requiredGates: [...requirement.requiredGates],
602
- refs: contextRequest ? [...contextRequest.refs] : [],
739
+ refs: contextRequest ? contextApprovalRefs(contextRequest) : [],
603
740
  };
604
741
  }
605
742
 
@@ -908,7 +1045,6 @@ function parsePlan(value: string): ImplementPlan {
908
1045
  function assertPlan(value: unknown): asserts value is ImplementPlan {
909
1046
  if (
910
1047
  !isRecord(value) ||
911
- value.schemaVersion !== IMPLEMENT_PLAN_SCHEMA_VERSION ||
912
1048
  typeof value.changeId !== "string" ||
913
1049
  !Array.isArray(value.tasks) ||
914
1050
  !Array.isArray(value.outputs) ||
@@ -954,7 +1090,6 @@ function assertDelivery(
954
1090
  ): asserts value is WorkflowDelivery {
955
1091
  if (
956
1092
  !isRecord(value) ||
957
- value.version !== 2 ||
958
1093
  (value.gate !== "gate-a" && value.gate !== "gate-b") ||
959
1094
  !Number.isSafeInteger(value.revision) ||
960
1095
  (value.revision as number) < 1 ||
@@ -1155,6 +1290,7 @@ function ensureEngineColumns(database: DatabaseSync): void {
1155
1290
  ensure("workflow_engine_tasks", [
1156
1291
  ["route_fingerprint", "TEXT"],
1157
1292
  ["context_request_json", "TEXT"],
1293
+ ["attempt_diagnostic_json", "TEXT"],
1158
1294
  ]);
1159
1295
  }
1160
1296
 
@@ -1171,6 +1307,7 @@ export type DurableCandidateProposal =
1171
1307
  kind: "paused" | "retryable" | "approval-needed";
1172
1308
  code: string;
1173
1309
  contextRequest?: WorkflowContextRequest;
1310
+ attemptDiagnostic?: SafeAttemptDiagnostic;
1174
1311
  }
1175
1312
  | { kind: "operation-cancelled"; code: "cancelled" };
1176
1313
 
@@ -1224,7 +1361,6 @@ interface DurableVerificationObservation {
1224
1361
  }
1225
1362
 
1226
1363
  interface DurableVerificationBaseline {
1227
- version: 1;
1228
1364
  revisionId: string;
1229
1365
  targetContracts: Array<{
1230
1366
  taskId: string;
@@ -1282,6 +1418,8 @@ export interface DurableWorkflowEngineOptions {
1282
1418
  attribution: "introduced";
1283
1419
  failureIdentities: string[];
1284
1420
  };
1421
+ artifactCorrection?: RedArtifactCorrection;
1422
+ contextRequest?: WorkflowContextRequest;
1285
1423
  signal: AbortSignal;
1286
1424
  onHeaders(): void;
1287
1425
  onProgress(): void;
@@ -1571,7 +1709,7 @@ function verificationIdentities(
1571
1709
  return [...new Set(supplied)].sort();
1572
1710
  }
1573
1711
  if (result.ok) return [];
1574
- return [hash("verification-failure-v1", scope, verification.id, result.code)];
1712
+ return [hash("verification-failure", scope, verification.id, result.code)];
1575
1713
  }
1576
1714
 
1577
1715
  function parseVerificationBaseline(
@@ -1579,7 +1717,6 @@ function parseVerificationBaseline(
1579
1717
  ): DurableVerificationBaseline {
1580
1718
  if (
1581
1719
  !isRecord(value) ||
1582
- value.version !== 1 ||
1583
1720
  typeof value.revisionId !== "string" ||
1584
1721
  !SHA256.test(value.revisionId) ||
1585
1722
  !Array.isArray(value.targetContracts) ||
@@ -2038,7 +2175,6 @@ class DurableWorkflowComposition
2038
2175
  });
2039
2176
  if (!fullSuite.ok) return fullSuite;
2040
2177
  const baseline: DurableVerificationBaseline = {
2041
- version: 1,
2042
2178
  revisionId: input.resources.baselineRevisionId,
2043
2179
  targetContracts: plan.tasks.map((task) => ({
2044
2180
  taskId: task.taskId,
@@ -2319,6 +2455,9 @@ class DurableWorkflowComposition
2319
2455
  ledger: TaskLedger,
2320
2456
  input: Parameters<WorkflowWorker["runAttempt"]>[0],
2321
2457
  ): WorkflowAttemptOutcome | undefined {
2458
+ if (input.artifactCorrection?.code === "red-artifact-constraint") {
2459
+ return undefined;
2460
+ }
2322
2461
  const projection = ledger.projection({
2323
2462
  runId: input.runId,
2324
2463
  taskId: input.taskId,
@@ -2866,7 +3005,13 @@ class DurableWorkflowComposition
2866
3005
  ...(request.routeFingerprint
2867
3006
  ? { expectedFingerprint: request.routeFingerprint }
2868
3007
  : {}),
2869
- requirements: IMPLEMENTATION_ROUTE_REQUIREMENTS,
3008
+ requirements: implementationRouteRequirements({
3009
+ task: request.task,
3010
+ ...(request.artifactCorrection
3011
+ ? { artifactCorrection: request.artifactCorrection }
3012
+ : {}),
3013
+ repair: { failureIdentities: input.failureIdentities },
3014
+ }),
2870
3015
  });
2871
3016
  if (!rebound.ok) return { kind: "paused", code: rebound.code };
2872
3017
  }
@@ -2885,9 +3030,16 @@ class DurableWorkflowComposition
2885
3030
  runId: request.runId,
2886
3031
  operationId: `${request.operationId}:${request.taskId}:repair:${input.repairAttempt}`,
2887
3032
  role: "implementation-worker",
2888
- requirements: IMPLEMENTATION_ROUTE_REQUIREMENTS,
3033
+ requirements: implementationRouteRequirements({
3034
+ task: request.task,
3035
+ ...(request.artifactCorrection
3036
+ ? { artifactCorrection: request.artifactCorrection }
3037
+ : {}),
3038
+ repair: { failureIdentities: input.failureIdentities },
3039
+ }),
2889
3040
  signal: request.signal,
2890
3041
  ...(request.onActivity ? { onActivity: request.onActivity } : {}),
3042
+ classifyResult: semanticRouteFailure,
2891
3043
  execute: (attempt) => {
2892
3044
  identity = {
2893
3045
  candidateId: `candidate-${randomUUID()}`,
@@ -2925,6 +3077,12 @@ class DurableWorkflowComposition
2925
3077
  },
2926
3078
  route: attempt.route,
2927
3079
  repair,
3080
+ ...(request.artifactCorrection
3081
+ ? { artifactCorrection: request.artifactCorrection }
3082
+ : {}),
3083
+ ...(request.contextRequest
3084
+ ? { contextRequest: request.contextRequest }
3085
+ : {}),
2928
3086
  signal: attempt.signal,
2929
3087
  onHeaders: attempt.onHeaders,
2930
3088
  onProgress: attempt.onProgress,
@@ -2972,7 +3130,8 @@ class DurableWorkflowComposition
2972
3130
  proposal.kind !== "sealed-candidate"
2973
3131
  ) {
2974
3132
  return proposal.kind === "operation-cancelled" ||
2975
- proposal.kind === "paused"
3133
+ proposal.kind === "paused" ||
3134
+ proposal.kind === "approval-needed"
2976
3135
  ? withRoute(proposal)
2977
3136
  : reject(proposal, {
2978
3137
  category: "artifact",
@@ -3213,7 +3372,9 @@ class DurableWorkflowComposition
3213
3372
  changes: revisionChanges(proposalRoot, approvedPaths),
3214
3373
  });
3215
3374
  const requiredOutputs = request.plan.outputs.filter(
3216
- (output) => output.producer.taskId === request.taskId,
3375
+ (output) =>
3376
+ output.producer.taskId === request.taskId &&
3377
+ (!request.artifactCorrection || output.producer.phase !== "refactor"),
3217
3378
  );
3218
3379
  if (
3219
3380
  requiredOutputs.some(
@@ -3352,7 +3513,7 @@ class DurableWorkflowComposition
3352
3513
  #verifiedPhaseReplay(
3353
3514
  ledger: TaskLedger,
3354
3515
  input: Parameters<WorkflowWorker["runAttempt"]>[0],
3355
- ): { artifactHash: string; exitCode: number } {
3516
+ ): { artifactHash: string; exitCode: number; isolatedRevisionId: string } {
3356
3517
  const projection = ledger.projection({
3357
3518
  runId: input.runId,
3358
3519
  taskId: input.taskId,
@@ -3366,6 +3527,8 @@ class DurableWorkflowComposition
3366
3527
  !event ||
3367
3528
  typeof event.artifactHash !== "string" ||
3368
3529
  !SHA256.test(event.artifactHash) ||
3530
+ typeof event.isolatedRevisionId !== "string" ||
3531
+ !SHA256.test(event.isolatedRevisionId) ||
3369
3532
  typeof event.exitCode !== "number" ||
3370
3533
  event.actualClassification !== expectedClassification(input.phase)
3371
3534
  ) {
@@ -3374,6 +3537,285 @@ class DurableWorkflowComposition
3374
3537
  return {
3375
3538
  artifactHash: event.artifactHash,
3376
3539
  exitCode: event.exitCode,
3540
+ isolatedRevisionId: event.isolatedRevisionId,
3541
+ };
3542
+ }
3543
+
3544
+ async #runRedArtifactCorrection(input: {
3545
+ resources: DurableRunResources;
3546
+ ledger: TaskLedger;
3547
+ baseline?: DurableVerificationBaseline;
3548
+ attempt: Parameters<WorkflowWorker["runAttempt"]>[0];
3549
+ }): Promise<WorkflowAttemptOutcome> {
3550
+ const { resources, ledger } = input;
3551
+ const request = input.attempt;
3552
+ if (
3553
+ request.phase !== "green" ||
3554
+ request.artifactCorrection?.code !== "red-artifact-constraint"
3555
+ ) {
3556
+ throw new Error("workflow-red-artifact-correction-invalid");
3557
+ }
3558
+ const priorRed = this.#verifiedPhaseReplay(ledger, {
3559
+ ...request,
3560
+ phase: "red",
3561
+ });
3562
+ if (!hasVerificationLifecycle(request.plan)) {
3563
+ return { kind: "paused", code: "red-artifact-constraint" };
3564
+ }
3565
+ const stableRevisionId = resources.currentRevisionId;
3566
+ const correctionPaths = [
3567
+ ...new Set(
3568
+ Object.values(request.task.phases).flatMap((phase) => [
3569
+ ...phase.write,
3570
+ ...phase.delete,
3571
+ ]),
3572
+ ),
3573
+ ];
3574
+ const rejectCorrection = async <T extends WorkflowAttemptOutcome>(
3575
+ outcome: T,
3576
+ ): Promise<T> => {
3577
+ if (resources.currentRevisionId !== stableRevisionId) {
3578
+ await this.#rollbackWorkspacePaths({
3579
+ resources,
3580
+ baseRevisionId: stableRevisionId,
3581
+ rejectedRevisionId: resources.currentRevisionId,
3582
+ paths: correctionPaths,
3583
+ });
3584
+ }
3585
+ return outcome;
3586
+ };
3587
+ const failureIdentities = [
3588
+ hash(
3589
+ "red-artifact-constraint",
3590
+ request.taskId,
3591
+ request.task.phases.red.verification.id,
3592
+ ),
3593
+ ];
3594
+ const correctionAttemptUsed = request.artifactCorrection.attempt;
3595
+ if (
3596
+ correctionAttemptUsed < 1 ||
3597
+ correctionAttemptUsed > request.artifactCorrection.maxAttempts
3598
+ ) {
3599
+ throw new Error("workflow-red-artifact-correction-invalid");
3600
+ }
3601
+ const repaired = await this.#runRepairCandidate({
3602
+ resources,
3603
+ ledger,
3604
+ attempt: request,
3605
+ repairAttempt: correctionAttemptUsed,
3606
+ failureIdentities,
3607
+ });
3608
+ if (repaired.kind !== "repair-committed") {
3609
+ return rejectCorrection(repaired);
3610
+ }
3611
+
3612
+ const correctedRevision = resources.workspaces.getRevision(
3613
+ repaired.commit.isolatedRevisionId,
3614
+ );
3615
+ const acceptedRedRevision = resources.workspaces.getRevision(
3616
+ priorRed.isolatedRevisionId,
3617
+ );
3618
+ if (!acceptedRedRevision.parentRevisionId) {
3619
+ throw new Error("workflow-ledger-revision-base-invalid");
3620
+ }
3621
+ const correctedRedChanges: Record<
3622
+ string,
3623
+ { kind: "absent" } | { kind: "file"; bytes: Uint8Array; mode: number }
3624
+ > = {};
3625
+ for (const relative of [
3626
+ ...new Set([
3627
+ ...request.task.phases.red.write,
3628
+ ...request.task.phases.red.delete,
3629
+ ]),
3630
+ ]) {
3631
+ const entry = correctedRevision.entries[relative];
3632
+ correctedRedChanges[relative] =
3633
+ entry?.kind === "file"
3634
+ ? {
3635
+ kind: "file",
3636
+ bytes: resources.artifacts.read(entry.hash),
3637
+ mode: entry.mode,
3638
+ }
3639
+ : { kind: "absent" };
3640
+ }
3641
+ const correctedRedRevision = resources.workspaces.createRevision({
3642
+ parentRevisionId: acceptedRedRevision.parentRevisionId,
3643
+ changes: correctedRedChanges,
3644
+ });
3645
+ const verifyCorrectedContract = async (input: {
3646
+ phase: "red" | "green";
3647
+ revisionId: string;
3648
+ verification: StructuredVerificationContract;
3649
+ }): Promise<DurablePhaseVerificationResult> => {
3650
+ const root = mkdtempSync(path.join(resources.root, "correction-check-"));
3651
+ try {
3652
+ resources.workspaces.materialize(input.revisionId, root);
3653
+ return await this.#options.verifyPhase({
3654
+ runId: request.runId,
3655
+ deliveryRevision: request.deliveryRevision,
3656
+ taskId: request.taskId,
3657
+ phase: input.phase,
3658
+ root,
3659
+ verification: structuredClone(input.verification),
3660
+ signal: request.signal,
3661
+ });
3662
+ } finally {
3663
+ rmSync(root, { recursive: true, force: true });
3664
+ }
3665
+ };
3666
+ emitWorkflowActivity(request.onActivity, { state: "verifying" });
3667
+ const redVerification = await verifyCorrectedContract({
3668
+ phase: "red",
3669
+ revisionId: correctedRedRevision.revisionId,
3670
+ verification: request.task.phases.red.verification,
3671
+ });
3672
+ if (!redVerification.ok) {
3673
+ return rejectCorrection({
3674
+ ...redVerification,
3675
+ ...(redVerification.kind === "retryable"
3676
+ ? { retryPolicy: "verification" as const }
3677
+ : {}),
3678
+ });
3679
+ }
3680
+ if (
3681
+ redVerification.classification !== "expected-red" ||
3682
+ redVerification.exitCode === 0
3683
+ ) {
3684
+ return rejectCorrection({
3685
+ kind: "retryable",
3686
+ code: "verification-rejected",
3687
+ retryPolicy: "verification",
3688
+ });
3689
+ }
3690
+ const greenVerification = await verifyCorrectedContract({
3691
+ phase: "green",
3692
+ revisionId: repaired.commit.isolatedRevisionId,
3693
+ verification: request.task.phases.green.verification,
3694
+ });
3695
+ if (!greenVerification.ok) {
3696
+ return rejectCorrection({
3697
+ ...greenVerification,
3698
+ ...(greenVerification.kind === "retryable"
3699
+ ? { retryPolicy: "verification" as const }
3700
+ : {}),
3701
+ });
3702
+ }
3703
+ if (
3704
+ greenVerification.classification !== "expected-green" ||
3705
+ greenVerification.exitCode !== 0
3706
+ ) {
3707
+ return rejectCorrection({
3708
+ kind: "retryable",
3709
+ code: "verification-rejected",
3710
+ retryPolicy: "verification",
3711
+ });
3712
+ }
3713
+
3714
+ const repairEvent: VerifiedTaskEvent = {
3715
+ runId: request.runId,
3716
+ taskId: request.taskId,
3717
+ eventId: `red-artifact-correction-${hash(
3718
+ request.operationId,
3719
+ request.taskId,
3720
+ repaired.commit.artifactHash,
3721
+ ).slice(0, 40)}`,
3722
+ kind: "repair-verified",
3723
+ phase: request.phase,
3724
+ attempt: correctionAttemptUsed,
3725
+ commandId: request.task.repairVerification.id,
3726
+ exitCode: 0,
3727
+ diagnostic: repaired.commit.diagnostic,
3728
+ artifactHash: repaired.commit.artifactHash,
3729
+ isolatedRevisionId: repaired.commit.isolatedRevisionId,
3730
+ outputFacts: repaired.commit.outputFacts,
3731
+ attribution: "introduced",
3732
+ failureIdentities: [
3733
+ hash(
3734
+ "red-artifact-constraint",
3735
+ request.taskId,
3736
+ request.task.phases.red.verification.id,
3737
+ ),
3738
+ ],
3739
+ routeId: repaired.commit.routeId,
3740
+ routeFingerprint: repaired.commit.routeFingerprint,
3741
+ };
3742
+ const greenPaths = new Set([
3743
+ ...request.task.phases.green.write,
3744
+ ...request.task.phases.green.delete,
3745
+ ]);
3746
+ const greenEvent: VerifiedTaskEvent = {
3747
+ runId: request.runId,
3748
+ taskId: request.taskId,
3749
+ eventId: `green-corrected-${hash(
3750
+ request.operationId,
3751
+ request.taskId,
3752
+ repaired.commit.artifactHash,
3753
+ ).slice(0, 40)}`,
3754
+ kind: "phase-verified",
3755
+ phase: "green",
3756
+ commandId: request.task.phases.green.verification.id,
3757
+ exitCode: greenVerification.exitCode,
3758
+ expectedClassification: "expected-green",
3759
+ actualClassification: greenVerification.classification,
3760
+ diagnostic: greenVerification.diagnostic,
3761
+ artifactHash: repaired.commit.artifactHash,
3762
+ isolatedRevisionId: repaired.commit.isolatedRevisionId,
3763
+ outputFacts: repaired.commit.outputFacts.filter((fact) =>
3764
+ greenPaths.has(fact.path),
3765
+ ),
3766
+ routeId: repaired.commit.routeId,
3767
+ routeFingerprint: repaired.commit.routeFingerprint,
3768
+ };
3769
+ const events: VerifiedTaskEvent[] = [repairEvent, greenEvent];
3770
+ if (input.baseline) {
3771
+ const affected = await this.#verifyTaskAffected({
3772
+ resources,
3773
+ baseline: input.baseline,
3774
+ task: request.task,
3775
+ revisionId: repaired.commit.isolatedRevisionId,
3776
+ signal: request.signal,
3777
+ });
3778
+ if (affected.kind === "repairable") {
3779
+ return rejectCorrection({
3780
+ kind: "retryable",
3781
+ code: "red-artifact-constraint",
3782
+ retryPolicy: "artifact",
3783
+ });
3784
+ }
3785
+ if (affected.kind !== "verified") return rejectCorrection(affected);
3786
+ }
3787
+ if (request.task.phases.refactor) {
3788
+ ledger.commitVerifiedEvents(events);
3789
+ return {
3790
+ kind: "phase-committed",
3791
+ artifactHash: repaired.commit.artifactHash,
3792
+ isolatedRevisionId: repaired.commit.isolatedRevisionId,
3793
+ exitCode: greenVerification.exitCode,
3794
+ classification: greenVerification.classification,
3795
+ routeId: repaired.commit.routeId,
3796
+ routeFingerprint: repaired.commit.routeFingerprint,
3797
+ };
3798
+ }
3799
+ const completedRevision = this.#completeTracking(resources, request.taskId);
3800
+ ledger.commitTaskCompletion({
3801
+ events,
3802
+ factKey: `task-final-${hash(request.taskId).slice(0, 40)}`,
3803
+ fact: {
3804
+ taskId: request.taskId,
3805
+ phase: request.phase,
3806
+ isolatedRevisionId: completedRevision.revisionId,
3807
+ attribution: "introduced",
3808
+ repairAttempts: correctionAttemptUsed,
3809
+ },
3810
+ });
3811
+ return {
3812
+ kind: "phase-committed",
3813
+ artifactHash: repaired.commit.artifactHash,
3814
+ isolatedRevisionId: completedRevision.revisionId,
3815
+ exitCode: greenVerification.exitCode,
3816
+ classification: greenVerification.classification,
3817
+ routeId: repaired.commit.routeId,
3818
+ routeFingerprint: repaired.commit.routeFingerprint,
3377
3819
  };
3378
3820
  }
3379
3821
 
@@ -3598,6 +4040,16 @@ class DurableWorkflowComposition
3598
4040
  verificationBaseline = captured.baseline;
3599
4041
  }
3600
4042
  this.#openTask(resources, ledger, input);
4043
+ if (input.artifactCorrection?.code === "red-artifact-constraint") {
4044
+ return retained(
4045
+ await this.#runRedArtifactCorrection({
4046
+ resources,
4047
+ ledger,
4048
+ ...(verificationBaseline ? { baseline: verificationBaseline } : {}),
4049
+ attempt: input,
4050
+ }),
4051
+ );
4052
+ }
3601
4053
  if (input.repair) {
3602
4054
  if (!verificationBaseline) {
3603
4055
  throw new Error("workflow-verification-baseline-unavailable");
@@ -3644,7 +4096,13 @@ class DurableWorkflowComposition
3644
4096
  ...(input.routeFingerprint
3645
4097
  ? { expectedFingerprint: input.routeFingerprint }
3646
4098
  : {}),
3647
- requirements: IMPLEMENTATION_ROUTE_REQUIREMENTS,
4099
+ requirements: implementationRouteRequirements({
4100
+ task: input.task,
4101
+ ...(input.artifactCorrection
4102
+ ? { artifactCorrection: input.artifactCorrection }
4103
+ : {}),
4104
+ ...(input.repair ? { repair: input.repair } : {}),
4105
+ }),
3648
4106
  });
3649
4107
  if (!rebound.ok) {
3650
4108
  return retained({ kind: "paused", code: rebound.code });
@@ -3660,9 +4118,16 @@ class DurableWorkflowComposition
3660
4118
  runId: input.runId,
3661
4119
  operationId: `${input.operationId}:${input.taskId}:${input.phase}`,
3662
4120
  role: "implementation-worker",
3663
- requirements: IMPLEMENTATION_ROUTE_REQUIREMENTS,
4121
+ requirements: implementationRouteRequirements({
4122
+ task: input.task,
4123
+ ...(input.artifactCorrection
4124
+ ? { artifactCorrection: input.artifactCorrection }
4125
+ : {}),
4126
+ ...(input.repair ? { repair: input.repair } : {}),
4127
+ }),
3664
4128
  signal: input.signal,
3665
4129
  ...(input.onActivity ? { onActivity: input.onActivity } : {}),
4130
+ classifyResult: semanticRouteFailure,
3666
4131
  execute: (attempt) => {
3667
4132
  identity = {
3668
4133
  candidateId: `candidate-${randomUUID()}`,
@@ -3699,6 +4164,9 @@ class DurableWorkflowComposition
3699
4164
  deletePaths: phase.delete,
3700
4165
  },
3701
4166
  route: attempt.route,
4167
+ ...(input.contextRequest
4168
+ ? { contextRequest: input.contextRequest }
4169
+ : {}),
3702
4170
  signal: attempt.signal,
3703
4171
  onHeaders: attempt.onHeaders,
3704
4172
  onProgress: attempt.onProgress,
@@ -3747,7 +4215,8 @@ class DurableWorkflowComposition
3747
4215
  proposal.kind !== "sealed-candidate"
3748
4216
  ) {
3749
4217
  return proposal.kind === "operation-cancelled" ||
3750
- proposal.kind === "paused"
4218
+ proposal.kind === "paused" ||
4219
+ proposal.kind === "approval-needed"
3751
4220
  ? retained(proposal)
3752
4221
  : reject(proposal, {
3753
4222
  category: "artifact",
@@ -4277,7 +4746,7 @@ class DurableWorkflowComposition
4277
4746
  runId: input.runId,
4278
4747
  role: input.role,
4279
4748
  routeId: input.routeId,
4280
- requirements: IMPLEMENTATION_ROUTE_REQUIREMENTS,
4749
+ requirements: implementationRouteRequirements({ task: input.task }),
4281
4750
  });
4282
4751
  return rebound.ok
4283
4752
  ? {
@@ -5080,8 +5549,18 @@ export class WorkflowEngine {
5080
5549
  this.#database
5081
5550
  .prepare(
5082
5551
  `UPDATE workflow_engine_tasks
5083
- SET state = 'paused', pause_code = 'operation-interrupted',
5084
- context_request_json = NULL, queue_position = NULL
5552
+ SET state = 'paused',
5553
+ pause_code = CASE
5554
+ WHEN pause_code = 'red-artifact-constraint'
5555
+ THEN pause_code
5556
+ ELSE 'operation-interrupted'
5557
+ END,
5558
+ context_request_json = CASE
5559
+ WHEN pause_code = 'red-artifact-constraint'
5560
+ THEN context_request_json
5561
+ ELSE NULL
5562
+ END,
5563
+ queue_position = NULL
5085
5564
  WHERE run_id = ? AND state IN ('phase-running', 'validating')`,
5086
5565
  )
5087
5566
  .run(row.run_id);
@@ -5154,8 +5633,8 @@ export class WorkflowEngine {
5154
5633
  return this.#database
5155
5634
  .prepare(
5156
5635
  `SELECT task_id, task_order, delivery_revision, plan_json, state,
5157
- phase, pause_code, context_request_json, route_id,
5158
- route_fingerprint, queue_position
5636
+ phase, pause_code, context_request_json, attempt_diagnostic_json,
5637
+ route_id, route_fingerprint, queue_position
5159
5638
  FROM workflow_engine_tasks WHERE run_id = ? ORDER BY task_order`,
5160
5639
  )
5161
5640
  .all(runId) as unknown as EngineTaskRow[];
@@ -5452,7 +5931,8 @@ export class WorkflowEngine {
5452
5931
  `UPDATE workflow_engine_tasks
5453
5932
  SET task_order = ?, delivery_revision = ?, plan_json = ?,
5454
5933
  state = 'pending', phase = 'red', pause_code = NULL,
5455
- context_request_json = NULL, queue_position = NULL
5934
+ context_request_json = NULL, attempt_diagnostic_json = NULL,
5935
+ queue_position = NULL
5456
5936
  WHERE run_id = ? AND task_id = ?`,
5457
5937
  )
5458
5938
  .run(taskOrder, delivery.revision, taskJson, runId, task.taskId);
@@ -5509,6 +5989,7 @@ export class WorkflowEngine {
5509
5989
  phase?: "red" | "green" | "refactor";
5510
5990
  pauseCode?: string | null;
5511
5991
  contextRequest?: WorkflowContextRequest | null;
5992
+ attemptDiagnostic?: SafeAttemptDiagnostic | null;
5512
5993
  routeId?: string | null;
5513
5994
  routeFingerprint?: string | null;
5514
5995
  queuePosition?: number | null;
@@ -5539,6 +6020,14 @@ export class WorkflowEngine {
5539
6020
  ),
5540
6021
  );
5541
6022
  }
6023
+ if (values.attemptDiagnostic !== undefined) {
6024
+ assignments.push("attempt_diagnostic_json = ?");
6025
+ parameters.push(
6026
+ values.attemptDiagnostic === null
6027
+ ? null
6028
+ : JSON.stringify(values.attemptDiagnostic),
6029
+ );
6030
+ }
5542
6031
  if (values.routeId !== undefined) {
5543
6032
  assignments.push("route_id = ?");
5544
6033
  parameters.push(values.routeId);
@@ -5745,6 +6234,24 @@ export class WorkflowEngine {
5745
6234
  const orderedPhases = phases(task);
5746
6235
  let phaseIndex = orderedPhases.indexOf(row.phase);
5747
6236
  let artifactAttempts = 0;
6237
+ const persistedContextRequest = row.context_request_json
6238
+ ? normalizeWorkflowContextRequest(
6239
+ JSON.parse(row.context_request_json) as unknown,
6240
+ )
6241
+ : undefined;
6242
+ const persistedRedArtifactCorrection =
6243
+ row.phase === "green" &&
6244
+ persistedContextRequest !== undefined &&
6245
+ classifyPersistedContextRequest(persistedContextRequest, task, "green")
6246
+ .code === "red-artifact-constraint";
6247
+ let redArtifactCorrection =
6248
+ row.phase === "green" &&
6249
+ (row.pause_code === "red-artifact-constraint" ||
6250
+ persistedRedArtifactCorrection);
6251
+ let requestedContext = persistedContextRequest;
6252
+ let correctionContextRequest = redArtifactCorrection
6253
+ ? persistedContextRequest
6254
+ : undefined;
5748
6255
  if (phaseIndex < 0) throw new Error("workflow-task-phase-invalid");
5749
6256
  while (phaseIndex < orderedPhases.length) {
5750
6257
  const phase = orderedPhases[phaseIndex];
@@ -5771,6 +6278,7 @@ export class WorkflowEngine {
5771
6278
  JSON.parse(current.verification_json) as unknown,
5772
6279
  )
5773
6280
  : undefined;
6281
+ const plan = this.#planForRevision(runId, row.delivery_revision);
5774
6282
  const repair =
5775
6283
  verificationStatus?.scope === "change-task-affected" &&
5776
6284
  verificationStatus.attribution === "introduced" &&
@@ -5782,19 +6290,32 @@ export class WorkflowEngine {
5782
6290
  failureIdentities: [...verificationStatus.failureIdentities],
5783
6291
  }
5784
6292
  : undefined;
6293
+ const artifactCorrection =
6294
+ redArtifactCorrection && phase === "green"
6295
+ ? {
6296
+ code: "red-artifact-constraint" as const,
6297
+ attempt: artifactAttempts + 1,
6298
+ maxAttempts: plan.verification.artifactCorrection.maxAttempts,
6299
+ ...(correctionContextRequest
6300
+ ? {
6301
+ contextRequest: structuredClone(correctionContextRequest),
6302
+ }
6303
+ : {}),
6304
+ }
6305
+ : undefined;
5785
6306
  this.#setTask(
5786
6307
  runId,
5787
6308
  row.task_id,
5788
6309
  {
5789
6310
  state: "phase-running",
5790
6311
  phase,
5791
- pauseCode: null,
5792
- contextRequest: null,
6312
+ pauseCode: artifactCorrection?.code ?? null,
6313
+ contextRequest: requestedContext ?? null,
6314
+ attemptDiagnostic: null,
5793
6315
  queuePosition: null,
5794
6316
  },
5795
6317
  lease,
5796
6318
  );
5797
- const plan = this.#planForRevision(runId, row.delivery_revision);
5798
6319
  let outcome: WorkflowAttemptOutcome;
5799
6320
  try {
5800
6321
  outcome = await this.#worker.runAttempt({
@@ -5825,6 +6346,8 @@ export class WorkflowEngine {
5825
6346
  }
5826
6347
  : {}),
5827
6348
  ...(repair ? { repair } : {}),
6349
+ ...(artifactCorrection ? { artifactCorrection } : {}),
6350
+ ...(requestedContext ? { contextRequest: requestedContext } : {}),
5828
6351
  signal,
5829
6352
  onActivity: (event) => {
5830
6353
  const projection = this.#runStore.status(runId);
@@ -5863,10 +6386,69 @@ export class WorkflowEngine {
5863
6386
  ) {
5864
6387
  this.#setVerificationStatus(runId, outcome.verification, lease);
5865
6388
  }
6389
+ if (
6390
+ outcome.kind !== "phase-committed" &&
6391
+ outcome.kind !== "operation-cancelled" &&
6392
+ outcome.contextRequest
6393
+ ) {
6394
+ requestedContext = normalizeWorkflowContextRequest(
6395
+ outcome.contextRequest,
6396
+ );
6397
+ }
6398
+ if (
6399
+ outcome.kind !== "phase-committed" &&
6400
+ outcome.kind !== "operation-cancelled" &&
6401
+ outcome.attemptDiagnostic
6402
+ ) {
6403
+ const previous = row.attempt_diagnostic_json
6404
+ ? (JSON.parse(row.attempt_diagnostic_json) as SafeAttemptDiagnostic)
6405
+ : undefined;
6406
+ const fingerprint = hash(
6407
+ outcome.code,
6408
+ outcome.attemptDiagnostic.finalCategory ?? "unknown",
6409
+ outcome.attemptDiagnostic.schema ?? "unknown",
6410
+ JSON.stringify(outcome.attemptDiagnostic.identityMismatch ?? []),
6411
+ ).slice(0, 16);
6412
+ const sameFailure = previous?.fingerprint === fingerprint;
6413
+ const sameFailureCount = sameFailure
6414
+ ? (previous?.sameFailureCount ?? 1) + 1
6415
+ : 1;
6416
+ const attemptDiagnostic = {
6417
+ ...outcome.attemptDiagnostic,
6418
+ fingerprint,
6419
+ sameFailureCount,
6420
+ ...(sameFailureCount > 1
6421
+ ? { action: "rebind-or-revise-delivery" as const }
6422
+ : {}),
6423
+ };
6424
+ this.#setTask(runId, row.task_id, { attemptDiagnostic }, lease);
6425
+ row.attempt_diagnostic_json = JSON.stringify(attemptDiagnostic);
6426
+ }
5866
6427
  if (outcome.kind === "retryable" && outcome.retryPolicy === "artifact") {
6428
+ if (outcome.code === "red-artifact-constraint") {
6429
+ redArtifactCorrection = true;
6430
+ correctionContextRequest = outcome.contextRequest
6431
+ ? normalizeWorkflowContextRequest(outcome.contextRequest)
6432
+ : correctionContextRequest;
6433
+ requestedContext = correctionContextRequest ?? requestedContext;
6434
+ }
5867
6435
  artifactAttempts += 1;
5868
6436
  const maxAttempts = plan.verification.artifactCorrection.maxAttempts;
5869
6437
  if (artifactAttempts < maxAttempts) {
6438
+ this.#setTask(
6439
+ runId,
6440
+ row.task_id,
6441
+ {
6442
+ state: "retryable",
6443
+ phase,
6444
+ pauseCode: redArtifactCorrection
6445
+ ? "red-artifact-constraint"
6446
+ : outcome.code,
6447
+ contextRequest: requestedContext ?? null,
6448
+ queuePosition: null,
6449
+ },
6450
+ lease,
6451
+ );
5870
6452
  emitWorkflowActivity(onActivity, {
5871
6453
  state: "retrying",
5872
6454
  stage: this.#runStore.status(runId).stage,
@@ -5903,6 +6485,7 @@ export class WorkflowEngine {
5903
6485
  phase,
5904
6486
  pauseCode: null,
5905
6487
  contextRequest: null,
6488
+ attemptDiagnostic: null,
5906
6489
  queuePosition: null,
5907
6490
  },
5908
6491
  lease,
@@ -5910,6 +6493,9 @@ export class WorkflowEngine {
5910
6493
  return;
5911
6494
  }
5912
6495
  artifactAttempts = 0;
6496
+ redArtifactCorrection = false;
6497
+ requestedContext = undefined;
6498
+ correctionContextRequest = undefined;
5913
6499
  phaseIndex += 1;
5914
6500
  if (phaseIndex >= orderedPhases.length) {
5915
6501
  this.#setTask(
@@ -5920,6 +6506,7 @@ export class WorkflowEngine {
5920
6506
  phase,
5921
6507
  pauseCode: null,
5922
6508
  contextRequest: null,
6509
+ attemptDiagnostic: null,
5923
6510
  queuePosition: null,
5924
6511
  },
5925
6512
  lease,
@@ -5934,6 +6521,7 @@ export class WorkflowEngine {
5934
6521
  phase: orderedPhases[phaseIndex],
5935
6522
  pauseCode: null,
5936
6523
  contextRequest: null,
6524
+ attemptDiagnostic: null,
5937
6525
  queuePosition: null,
5938
6526
  },
5939
6527
  lease,
@@ -5975,10 +6563,9 @@ export class WorkflowEngine {
5975
6563
  pauseCode: approvalCodeInvalid
5976
6564
  ? "approval-code-invalid"
5977
6565
  : outcome.code,
5978
- contextRequest:
5979
- !approvalCodeInvalid && outcome.contextRequest
5980
- ? normalizeWorkflowContextRequest(outcome.contextRequest)
5981
- : null,
6566
+ contextRequest: approvalCodeInvalid
6567
+ ? null
6568
+ : (requestedContext ?? null),
5982
6569
  queuePosition: null,
5983
6570
  },
5984
6571
  lease,
@@ -5990,11 +6577,119 @@ export class WorkflowEngine {
5990
6577
  }
5991
6578
 
5992
6579
  #firstPaused(rows: EngineTaskRow[]): EngineTaskRow | undefined {
5993
- return rows.find((row) =>
5994
- ["paused", "retryable", "approval-needed"].includes(row.state),
6580
+ return (
6581
+ rows.find((row) => row.state === "approval-needed") ??
6582
+ rows.find((row) => ["paused", "retryable"].includes(row.state))
5995
6583
  );
5996
6584
  }
5997
6585
 
6586
+ #recoverLegacyContextApproval(runId: string): boolean {
6587
+ const projection = this.#runStore.status(runId);
6588
+ if (!["approval-needed", "paused"].includes(projection.state)) return false;
6589
+ const rows = this.#tasks(runId);
6590
+ const approvalRows = rows.filter((row) => row.state === "approval-needed");
6591
+ if (approvalRows.length === 0) return false;
6592
+ const reclassified = rows.flatMap((row) => {
6593
+ if (
6594
+ row.state !== "approval-needed" ||
6595
+ row.pause_code !== "boundary-review-needed" ||
6596
+ row.context_request_json === null
6597
+ ) {
6598
+ return [];
6599
+ }
6600
+ try {
6601
+ const request = normalizeWorkflowContextRequest(
6602
+ JSON.parse(row.context_request_json) as unknown,
6603
+ );
6604
+ const classified = classifyPersistedContextRequest(
6605
+ request,
6606
+ this.#taskPlan(row),
6607
+ row.phase,
6608
+ );
6609
+ if (classified.kind === "approval-needed") return [];
6610
+ return [
6611
+ {
6612
+ row,
6613
+ state: classified.kind === "retryable" ? "retryable" : "paused",
6614
+ code: classified.code,
6615
+ contextRequest: JSON.stringify(
6616
+ normalizeWorkflowContextRequest(classified.contextRequest),
6617
+ ),
6618
+ },
6619
+ ];
6620
+ } catch {
6621
+ return [];
6622
+ }
6623
+ });
6624
+ const reclassifiedTaskIds = new Set(
6625
+ reclassified.map((entry) => entry.row.task_id),
6626
+ );
6627
+ const retainedApprovals = approvalRows.filter(
6628
+ (row) => !reclassifiedTaskIds.has(row.task_id),
6629
+ );
6630
+ const updateTasks = (database: DatabaseSync) => {
6631
+ for (const entry of reclassified) {
6632
+ const updated = database
6633
+ .prepare(
6634
+ `UPDATE workflow_engine_tasks
6635
+ SET state = ?, phase = ?, pause_code = ?, context_request_json = ?,
6636
+ queue_position = NULL
6637
+ WHERE run_id = ? AND task_id = ?
6638
+ AND state = 'approval-needed'
6639
+ AND pause_code = 'boundary-review-needed'`,
6640
+ )
6641
+ .run(
6642
+ entry.state,
6643
+ entry.row.phase,
6644
+ entry.code,
6645
+ entry.contextRequest,
6646
+ runId,
6647
+ entry.row.task_id,
6648
+ );
6649
+ if (Number(updated.changes) !== 1) {
6650
+ throw new Error("workflow-legacy-context-race");
6651
+ }
6652
+ }
6653
+ };
6654
+ const transitionWithUpdates = (
6655
+ to: "paused" | "approval-needed",
6656
+ code: string,
6657
+ ) =>
6658
+ this.#runStore.transitionAtomically(
6659
+ {
6660
+ runId,
6661
+ to,
6662
+ operationId: `legacy-context-${hash(
6663
+ runId,
6664
+ String(projection.sequence),
6665
+ to,
6666
+ code,
6667
+ JSON.stringify(
6668
+ reclassified.map((entry) => [entry.row.task_id, entry.code]),
6669
+ ),
6670
+ ).slice(0, 40)}`,
6671
+ code,
6672
+ },
6673
+ updateTasks,
6674
+ );
6675
+ if (retainedApprovals.length > 0) {
6676
+ if (projection.state === "approval-needed") {
6677
+ if (reclassified.length === 0) return false;
6678
+ this.#transaction(() => updateTasks(this.#database));
6679
+ } else {
6680
+ transitionWithUpdates(
6681
+ "approval-needed",
6682
+ retainedApprovals[0].pause_code ?? "approval-code-invalid",
6683
+ );
6684
+ }
6685
+ return true;
6686
+ }
6687
+ const first = reclassified[0];
6688
+ if (!first) return false;
6689
+ transitionWithUpdates("paused", first.code);
6690
+ return true;
6691
+ }
6692
+
5998
6693
  async #finishChange(
5999
6694
  runId: string,
6000
6695
  operationId: string,
@@ -6568,6 +7263,21 @@ export class WorkflowEngine {
6568
7263
  return this.#statusByRun(runId);
6569
7264
  }
6570
7265
 
7266
+ #safeAttemptDiagnostic(
7267
+ _runId: string,
7268
+ row: EngineTaskRow | undefined,
7269
+ ): SafeAttemptDiagnostic | undefined {
7270
+ if (!row) return undefined;
7271
+ if (row.attempt_diagnostic_json) {
7272
+ try {
7273
+ return JSON.parse(row.attempt_diagnostic_json) as SafeAttemptDiagnostic;
7274
+ } catch {
7275
+ return undefined;
7276
+ }
7277
+ }
7278
+ return undefined;
7279
+ }
7280
+
6571
7281
  #statusByRun(runId: string): Record<string, unknown> {
6572
7282
  const projection = this.#runStore.status(runId);
6573
7283
  const rows = this.#tasks(runId);
@@ -6592,14 +7302,17 @@ export class WorkflowEngine {
6592
7302
  )
6593
7303
  : undefined;
6594
7304
  const pauseCode =
6595
- projection.pauseCode ?? firstPaused?.pause_code ?? "task-paused";
7305
+ projection.state === "approval-needed" &&
7306
+ firstPaused?.state === "approval-needed"
7307
+ ? (firstPaused.pause_code ?? projection.pauseCode ?? "task-paused")
7308
+ : (projection.pauseCode ?? firstPaused?.pause_code ?? "task-paused");
6596
7309
  const approval =
6597
7310
  projection.state === "approval-needed"
6598
7311
  ? approvalRequirement(pauseCode, contextRequest)
6599
7312
  : undefined;
7313
+ const attemptDiagnostic = this.#safeAttemptDiagnostic(runId, firstPaused);
6600
7314
  const currentRevision = projection.deliveryRevision ?? 0;
6601
7315
  return {
6602
- version: 2,
6603
7316
  runId: projection.runId,
6604
7317
  stage: projection.stage,
6605
7318
  ...(projection.change ? { change: projection.change } : {}),
@@ -6616,6 +7329,7 @@ export class WorkflowEngine {
6616
7329
  ? {
6617
7330
  pause: {
6618
7331
  code: pauseCode,
7332
+ ...(attemptDiagnostic ? { diagnostic: attemptDiagnostic } : {}),
6619
7333
  },
6620
7334
  }
6621
7335
  : {}),
@@ -6941,6 +7655,7 @@ export class WorkflowEngine {
6941
7655
  ) {
6942
7656
  const runId = this.#lookupRun(command.stage, command.change);
6943
7657
  if (!runId) throw new Error("run-not-found");
7658
+ this.#recoverLegacyContextApproval(runId);
6944
7659
  const replay = this.#beginOperation(
6945
7660
  runId,
6946
7661
  command.operationId,
@@ -7098,6 +7813,7 @@ export class WorkflowEngine {
7098
7813
  const rebound = this.#worker.rebind({
7099
7814
  runId,
7100
7815
  taskId: task.task_id,
7816
+ task: structuredClone(this.#taskPlan(task)),
7101
7817
  role: "implementation-worker",
7102
7818
  routeId: command.routeId,
7103
7819
  });
@@ -7336,7 +8052,19 @@ export class WorkflowEngine {
7336
8052
  );
7337
8053
  case "status": {
7338
8054
  const runId = this.#lookupRun(command.stage, command.change);
7339
- if (!runId) throw new Error("run-not-found");
8055
+ if (!runId) {
8056
+ return {
8057
+ stage: command.stage,
8058
+ change: command.change,
8059
+ state: "not-started",
8060
+ durable: true,
8061
+ completed: false,
8062
+ legalCommands: ["start"],
8063
+ tasks: [],
8064
+ queue: [],
8065
+ };
8066
+ }
8067
+ this.#recoverLegacyContextApproval(runId);
7340
8068
  return this.#withAvailableDelivery(this.#statusByRun(runId));
7341
8069
  }
7342
8070
  case "resume":
@@ -7401,7 +8129,7 @@ export class WorkflowEngine {
7401
8129
  outcome.handoffId === existing.handoff_id &&
7402
8130
  outcome.runId === existing.owner_run_id &&
7403
8131
  outcome.receiptHash === existing.receipt_hash &&
7404
- outcome.selectorBeforeCutover === "v1-bootstrap" &&
8132
+ outcome.selectorBeforeCutover === "bootstrap" &&
7405
8133
  existing.state === "prepared" &&
7406
8134
  outcome.state === "prepared" &&
7407
8135
  outcome.selectorCasPending === true;
@@ -7422,7 +8150,7 @@ export class WorkflowEngine {
7422
8150
  if (
7423
8151
  existing.state !== "prepared" ||
7424
8152
  outcome.state !== "prepared" ||
7425
- outcome.selectorBeforeCutover !== "v1-bootstrap" ||
8153
+ outcome.selectorBeforeCutover !== "bootstrap" ||
7426
8154
  outcome.selectorCasPending !== true ||
7427
8155
  acceptanceFacts === undefined ||
7428
8156
  workspaceManifestHash === undefined
@@ -7479,7 +8207,7 @@ export class WorkflowEngine {
7479
8207
  acceptanceHash: input.acceptanceHash,
7480
8208
  ...(acceptanceFacts ? { acceptanceFacts } : {}),
7481
8209
  ...(workspaceManifestHash ? { workspaceManifestHash } : {}),
7482
- selectorBeforeCutover: "v1-bootstrap",
8210
+ selectorBeforeCutover: "bootstrap",
7483
8211
  selectorCasPending: true,
7484
8212
  };
7485
8213
  const preparedRows = this.#database