@abelxiaoxing/cadence 1.0.12 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/runtime.ts CHANGED
@@ -17,12 +17,14 @@ import {
17
17
  type ImplementApplyOperation,
18
18
  type ImplementationPhase,
19
19
  type ImplementDiscardOperation,
20
+ type ImplementGraphReadinessDiagnostic,
20
21
  type ImplementOutcome,
21
22
  type ImplementRunRequest,
22
23
  LIMITS,
23
24
  type PhaseAttempt,
24
25
  type RequestEnvelope,
25
26
  type RunRequest,
27
+ type SafeFailureDiagnostic,
26
28
  type TaskBoundary,
27
29
  type TaskFailure,
28
30
  validateAgentsCheckpointAttempt,
@@ -43,6 +45,11 @@ import {
43
45
  snapshotFile,
44
46
  snapshotFiles,
45
47
  } from "./file-snapshot.ts";
48
+ import {
49
+ assessImplementGraphReadiness,
50
+ hashImplementGraphBoundary,
51
+ type ImplementExecutionFacts,
52
+ } from "./implement-graph.ts";
46
53
  import type { ParentPayloadBridge } from "./parent-payload-bridge.ts";
47
54
  import { customPhaseRuntime, runtimeFromContext } from "./parent-provider.ts";
48
55
  import { applyAgentsCheckpoint, applyRetainedPatch } from "./patch.ts";
@@ -58,8 +65,8 @@ import {
58
65
  resolveSubagentEndpoint,
59
66
  type SubagentEndpoint,
60
67
  } from "./subagent-endpoint.ts";
61
- import { assessVerificationReadiness } from "./verification-capability.ts";
62
68
  import {
69
+ type ImplementGraphRecord,
63
70
  type TaskRecord,
64
71
  taskConflictOf,
65
72
  taskRecordKey,
@@ -100,7 +107,7 @@ function targetsImplement(request: unknown): boolean {
100
107
  );
101
108
  }
102
109
 
103
- function cloneLegacyEnvelope(request: RequestEnvelope): RequestEnvelope {
110
+ function cloneNonImplementEnvelope(request: RequestEnvelope): RequestEnvelope {
104
111
  return {
105
112
  ...request,
106
113
  taskId: request.taskId ?? request.id,
@@ -177,6 +184,39 @@ function nextDeclaredPhase(
177
184
  return null;
178
185
  }
179
186
 
187
+ function taskBoundaryOf(
188
+ graph: ImplementGraphRecord,
189
+ taskId: string,
190
+ ): TaskBoundary | null {
191
+ const task = graph.boundary.tasks.find(
192
+ (candidate) => candidate.taskId === taskId,
193
+ );
194
+ return task
195
+ ? ({
196
+ changeId: graph.boundary.changeId,
197
+ ...structuredClone(task),
198
+ } satisfies TaskBoundary)
199
+ : null;
200
+ }
201
+
202
+ function appliedPhasesOf(record: TaskRecord): ImplementationPhase[] {
203
+ const phases: ImplementationPhase[] = ["red", "green"];
204
+ if (record.boundary.phases.refactor) phases.push("refactor");
205
+ if (
206
+ record.state.kind === "completed" ||
207
+ record.state.kind === "agents-checkpoint-pending"
208
+ ) {
209
+ return phases;
210
+ }
211
+ const current =
212
+ record.state.kind === "ready" ||
213
+ record.state.kind === "candidate-pending" ||
214
+ record.state.kind === "blocked"
215
+ ? record.state.phase
216
+ : "red";
217
+ return phases.slice(0, phases.indexOf(current));
218
+ }
219
+
180
220
  function assertAttemptAllowed(record: TaskRecord, attempt: PhaseAttempt): void {
181
221
  switch (record.state.kind) {
182
222
  case "candidate-pending":
@@ -411,25 +451,28 @@ interface OutcomeIdentity {
411
451
  function blockedOutcome(
412
452
  identity: OutcomeIdentity,
413
453
  failure: TaskFailure,
414
- ): ImplementOutcome {
454
+ ): Extract<ImplementOutcome, { kind: "blocked" }> {
415
455
  return { kind: "blocked", ...identity, failure: structuredClone(failure) };
416
456
  }
417
457
 
418
- function cancelledOutcome(identity: OutcomeIdentity): ImplementOutcome {
458
+ function cancelledOutcome(
459
+ identity: OutcomeIdentity,
460
+ ): Extract<ImplementOutcome, { kind: "cancelled" }> {
419
461
  return { kind: "cancelled", ...identity };
420
462
  }
421
463
 
422
464
  function retryOutcome(
423
465
  identity: OutcomeIdentity,
424
466
  scope: "worker" | "checkpoint",
425
- cause: "artifact" | "stale",
467
+ failure: Extract<ChildFailure, { kind: "artifact" | "stale" }>,
426
468
  ): ImplementOutcome {
427
469
  return {
428
470
  kind: "retry",
429
471
  ...identity,
430
472
  scope,
431
- cause,
473
+ cause: failure.kind,
432
474
  remainingAttempts: 1,
475
+ lastFailure: safeFailureDiagnostic(failure),
433
476
  };
434
477
  }
435
478
 
@@ -478,7 +521,11 @@ function childFailureOf(result: InternalFailureResult): ChildFailure {
478
521
  }
479
522
  switch (result.failureClass) {
480
523
  case "artifact":
481
- return { kind: "artifact", code: "invalid-diff" };
524
+ return {
525
+ kind: "artifact",
526
+ code: "invalid-diff",
527
+ stage: "phase-runtime",
528
+ };
482
529
  case "environment":
483
530
  return { kind: "environment", code: "root-unavailable" };
484
531
  default:
@@ -486,6 +533,10 @@ function childFailureOf(result: InternalFailureResult): ChildFailure {
486
533
  kind: "transport",
487
534
  code:
488
535
  result.failureKind === "timed-out" ? "timeout" : "transport-failure",
536
+ stage:
537
+ result.failureKind === "timed-out"
538
+ ? "child-timeout"
539
+ : "child-provider-stream",
489
540
  };
490
541
  }
491
542
  }
@@ -524,20 +575,28 @@ function attemptFailureCause(
524
575
 
525
576
  function attemptsExhaustedFailure(failure: ChildFailure): TaskFailure {
526
577
  const cause = attemptFailureCause(failure);
527
- if (failure.kind !== "transport") {
528
- return { kind: "attempts-exhausted", cause };
529
- }
530
578
  return {
531
579
  kind: "attempts-exhausted",
532
580
  cause,
533
- lastFailure: {
534
- code: failure.code,
535
- stage:
536
- failure.stage ??
537
- (failure.code === "child-timeout" || failure.code === "timeout"
538
- ? "child-timeout"
539
- : "child-provider-stream"),
540
- },
581
+ attemptsUsed: 2,
582
+ lastFailure: safeFailureDiagnostic(
583
+ failure as Extract<
584
+ ChildFailure,
585
+ { kind: "artifact" | "stale" | "transport" }
586
+ >,
587
+ ),
588
+ };
589
+ }
590
+
591
+ function safeFailureDiagnostic(
592
+ failure: Extract<ChildFailure, { kind: "artifact" | "stale" | "transport" }>,
593
+ ): SafeFailureDiagnostic {
594
+ return {
595
+ code: failure.code,
596
+ stage: failure.stage,
597
+ ...("details" in failure && failure.details
598
+ ? { details: structuredClone(failure.details) }
599
+ : {}),
541
600
  };
542
601
  }
543
602
 
@@ -891,6 +950,30 @@ export class Runtime {
891
950
  ) {
892
951
  throw new Error("AGENTS checkpoint target mismatch");
893
952
  }
953
+ const outputFailure = this.unavailableAppliedOutput(
954
+ worker,
955
+ worker.state.finalPhase,
956
+ true,
957
+ );
958
+ if (outputFailure) {
959
+ const failure: TaskFailure = {
960
+ kind: "graph-readiness",
961
+ diagnostic: outputFailure,
962
+ };
963
+ worker.state = {
964
+ kind: "blocked",
965
+ phase: worker.state.finalPhase,
966
+ failure,
967
+ };
968
+ return blockedOutcome(
969
+ {
970
+ requestId: attempt.requestId,
971
+ taskId: attempt.taskId,
972
+ phase: worker.state.phase,
973
+ },
974
+ failure,
975
+ );
976
+ }
894
977
  const request: AgentsCheckpointRequest = {
895
978
  stage: "abel-implement",
896
979
  taskId: worker.boundary.taskId,
@@ -940,6 +1023,8 @@ export class Runtime {
940
1023
  const taskFailure: TaskFailure = {
941
1024
  kind: "checkpoint-attempts-exhausted",
942
1025
  cause: "artifact",
1026
+ attemptsUsed: 2,
1027
+ lastFailure: safeFailureDiagnostic(failure),
943
1028
  };
944
1029
  worker.state = {
945
1030
  kind: "blocked",
@@ -953,13 +1038,15 @@ export class Runtime {
953
1038
  finalPhase: phase,
954
1039
  attemptIndex: 1,
955
1040
  };
956
- return retryOutcome(identity, "checkpoint", "artifact");
1041
+ return retryOutcome(identity, "checkpoint", failure);
957
1042
  }
958
1043
  case "stale": {
959
1044
  if (worker.state.attemptIndex === 1) {
960
1045
  const taskFailure: TaskFailure = {
961
1046
  kind: "checkpoint-attempts-exhausted",
962
1047
  cause: "stale",
1048
+ attemptsUsed: 2,
1049
+ lastFailure: safeFailureDiagnostic(failure),
963
1050
  };
964
1051
  worker.state = {
965
1052
  kind: "blocked",
@@ -973,7 +1060,7 @@ export class Runtime {
973
1060
  finalPhase: phase,
974
1061
  attemptIndex: 1,
975
1062
  };
976
- return retryOutcome(identity, "checkpoint", "stale");
1063
+ return retryOutcome(identity, "checkpoint", failure);
977
1064
  }
978
1065
  case "environment": {
979
1066
  const taskFailure: TaskFailure = failure;
@@ -1102,7 +1189,18 @@ export class Runtime {
1102
1189
  }
1103
1190
  const applyRoot = resolved?.retained.root ?? root;
1104
1191
  const run = this.enqueueParentApply(() =>
1105
- applyRetainedPatch({ root: applyRoot, id, store: this.results, signal }),
1192
+ applyRetainedPatch({
1193
+ root: applyRoot,
1194
+ id,
1195
+ store: this.results,
1196
+ ...(resolved
1197
+ ? {
1198
+ candidateOutputsAvailable: (checkoutRoot: string) =>
1199
+ this.candidateOutputsAvailable(resolved.worker, checkoutRoot),
1200
+ }
1201
+ : {}),
1202
+ signal,
1203
+ }),
1106
1204
  );
1107
1205
  const seq = ++this.applySeq;
1108
1206
  return run.then((result) => {
@@ -1117,6 +1215,33 @@ export class Runtime {
1117
1215
  worker.boundary,
1118
1216
  identity.phase,
1119
1217
  );
1218
+ const outputFailure = this.unavailableAppliedOutput(
1219
+ worker,
1220
+ identity.phase,
1221
+ readyPhase === null && worker.boundary.agents.impact === "none",
1222
+ );
1223
+ if (outputFailure) {
1224
+ const failure: TaskFailure = {
1225
+ kind: "graph-readiness",
1226
+ diagnostic: outputFailure,
1227
+ };
1228
+ worker.state = {
1229
+ kind: "blocked",
1230
+ phase: identity.phase,
1231
+ failure,
1232
+ };
1233
+ return {
1234
+ ...blockedOutcome(
1235
+ {
1236
+ requestId,
1237
+ taskId: identity.taskId,
1238
+ phase: identity.phase,
1239
+ },
1240
+ failure,
1241
+ ),
1242
+ result: applied,
1243
+ } satisfies ImplementOutcome;
1244
+ }
1120
1245
  if (readyPhase) {
1121
1246
  worker.state = {
1122
1247
  kind: "ready",
@@ -1215,12 +1340,9 @@ export class Runtime {
1215
1340
  launchIndex: 1,
1216
1341
  correction: structuredClone(rejection),
1217
1342
  };
1218
- return retryOutcome(outcomeIdentity, "worker", "artifact");
1343
+ return retryOutcome(outcomeIdentity, "worker", rejection);
1219
1344
  }
1220
- const failure: TaskFailure = {
1221
- kind: "attempts-exhausted",
1222
- cause: "artifact",
1223
- };
1345
+ const failure = attemptsExhaustedFailure(rejection);
1224
1346
  worker.state = {
1225
1347
  kind: "blocked",
1226
1348
  phase: identity.phase,
@@ -1289,10 +1411,7 @@ export class Runtime {
1289
1411
  correction: structuredClone(failure),
1290
1412
  };
1291
1413
  } else {
1292
- const taskFailure: TaskFailure = {
1293
- kind: "attempts-exhausted",
1294
- cause: "stale",
1295
- };
1414
+ const taskFailure = attemptsExhaustedFailure(failure);
1296
1415
  worker.state = {
1297
1416
  kind: "blocked",
1298
1417
  phase: identity.phase,
@@ -1300,16 +1419,13 @@ export class Runtime {
1300
1419
  };
1301
1420
  return blockedOutcome(outcomeIdentity, taskFailure);
1302
1421
  }
1303
- return retryOutcome(outcomeIdentity, "worker", "stale");
1422
+ return retryOutcome(outcomeIdentity, "worker", failure);
1304
1423
  }
1305
1424
  case "artifact": {
1306
1425
  this.results.discard(resultId);
1307
1426
  const correctionAvailable = identity.launchIndex === 0;
1308
1427
  if (!correctionAvailable) {
1309
- const taskFailure: TaskFailure = {
1310
- kind: "attempts-exhausted",
1311
- cause: "artifact",
1312
- };
1428
+ const taskFailure = attemptsExhaustedFailure(failure);
1313
1429
  worker.state = {
1314
1430
  kind: "blocked",
1315
1431
  phase: identity.phase,
@@ -1323,7 +1439,7 @@ export class Runtime {
1323
1439
  launchIndex: 1,
1324
1440
  correction: structuredClone(failure),
1325
1441
  };
1326
- return retryOutcome(outcomeIdentity, "worker", "artifact");
1442
+ return retryOutcome(outcomeIdentity, "worker", failure);
1327
1443
  }
1328
1444
  case "cancelled":
1329
1445
  return cancelledOutcome(outcomeIdentity);
@@ -1533,6 +1649,184 @@ export class Runtime {
1533
1649
  };
1534
1650
  }
1535
1651
 
1652
+ private graphFacts(graph: ImplementGraphRecord): ImplementExecutionFacts {
1653
+ const records = this.registry
1654
+ .values()
1655
+ .filter(
1656
+ (record) =>
1657
+ record.workspaceRoot === graph.workspaceRoot &&
1658
+ record.boundary.changeId === graph.boundary.changeId,
1659
+ );
1660
+ return {
1661
+ completedTasks: [
1662
+ ...new Set([
1663
+ ...graph.completedTasks,
1664
+ ...records
1665
+ .filter((record) => record.state.kind === "completed")
1666
+ .map((record) => record.boundary.taskId),
1667
+ ]),
1668
+ ],
1669
+ blockedTasks: [
1670
+ ...new Set([
1671
+ ...graph.blockedTasks,
1672
+ ...records
1673
+ .filter((record) => record.state.kind === "blocked")
1674
+ .map((record) => record.boundary.taskId),
1675
+ ]),
1676
+ ],
1677
+ appliedPhases: records.flatMap((record) =>
1678
+ appliedPhasesOf(record).map((phase) => ({
1679
+ taskId: record.boundary.taskId,
1680
+ phase,
1681
+ })),
1682
+ ),
1683
+ dependencyOwner: graph.workspaceRoot,
1684
+ };
1685
+ }
1686
+
1687
+ private graphFor(worker: TaskRecord): ImplementGraphRecord {
1688
+ const graph = this.registry.getGraph(
1689
+ worker.workspaceRoot,
1690
+ worker.boundary.changeId,
1691
+ );
1692
+ if (!graph) throw new Error("Implement graph is unavailable");
1693
+ return graph;
1694
+ }
1695
+
1696
+ private candidateOutputsAvailable(
1697
+ worker: TaskRecord,
1698
+ checkoutRoot: string,
1699
+ ): boolean {
1700
+ const graph = this.graphFor(worker);
1701
+ const phase = taskPhase(worker);
1702
+ const produced = new Set(
1703
+ graph.boundary.outputs
1704
+ .filter(
1705
+ (output) =>
1706
+ output.producer.taskId === worker.boundary.taskId &&
1707
+ output.producer.phase === phase,
1708
+ )
1709
+ .map((output) => output.id),
1710
+ );
1711
+ if (produced.size === 0) return true;
1712
+ const readiness = assessImplementGraphReadiness(
1713
+ checkoutRoot,
1714
+ graph.boundary,
1715
+ {
1716
+ ...this.graphFacts(graph),
1717
+ candidate: { taskId: worker.boundary.taskId, phase },
1718
+ },
1719
+ );
1720
+ return readiness.outputs.every(
1721
+ (output) =>
1722
+ !produced.has(output.outputId) || output.status === "available",
1723
+ );
1724
+ }
1725
+
1726
+ private unavailableAppliedOutput(
1727
+ worker: TaskRecord,
1728
+ phase: ImplementationPhase,
1729
+ allTaskOutputs: boolean,
1730
+ ): ImplementGraphReadinessDiagnostic | undefined {
1731
+ const graph = this.graphFor(worker);
1732
+ const facts = this.graphFacts(graph);
1733
+ const appliedPhases = [
1734
+ ...(facts.appliedPhases ?? []),
1735
+ { taskId: worker.boundary.taskId, phase },
1736
+ ].filter(
1737
+ (entry, index, entries) =>
1738
+ entries.findIndex(
1739
+ (candidate) =>
1740
+ candidate.taskId === entry.taskId &&
1741
+ candidate.phase === entry.phase,
1742
+ ) === index,
1743
+ );
1744
+ const readiness = assessImplementGraphReadiness(
1745
+ worker.workspaceRoot,
1746
+ graph.boundary,
1747
+ { ...facts, appliedPhases },
1748
+ );
1749
+ const output = readiness.outputs.find(
1750
+ (entry) =>
1751
+ entry.producerTaskId === worker.boundary.taskId &&
1752
+ (allTaskOutputs || entry.producerPhase === phase) &&
1753
+ entry.status !== "available",
1754
+ );
1755
+ if (!output) return undefined;
1756
+ return (
1757
+ output.diagnostic ?? {
1758
+ kind: "graph-readiness",
1759
+ code: "producer-output-unavailable",
1760
+ outputId: output.outputId,
1761
+ producerTaskId: output.producerTaskId,
1762
+ producerPhase: output.producerPhase,
1763
+ }
1764
+ );
1765
+ }
1766
+
1767
+ private admitImplementGraph(
1768
+ request: Extract<ImplementRunRequest, { kind: "admit-graph" }>,
1769
+ workspaceRoot: string,
1770
+ ): ImplementOutcome {
1771
+ if (this.registry.getGraph(workspaceRoot, request.graph.changeId)) {
1772
+ throw new Error("duplicate graph admission protocol error");
1773
+ }
1774
+ const actualHash = hashImplementGraphBoundary(request.graph);
1775
+ if (actualHash !== request.graphHash) {
1776
+ return {
1777
+ kind: "graph-rejected",
1778
+ changeId: request.graph.changeId,
1779
+ graphHash: request.graphHash,
1780
+ diagnostics: [
1781
+ {
1782
+ kind: "graph-readiness",
1783
+ code: "graph-hash-mismatch",
1784
+ },
1785
+ ],
1786
+ };
1787
+ }
1788
+ const readiness = assessImplementGraphReadiness(
1789
+ workspaceRoot,
1790
+ request.graph,
1791
+ {
1792
+ completedTasks: request.state.completedTasks,
1793
+ blockedTasks: request.state.blockedTasks,
1794
+ dependencyOwner: workspaceRoot,
1795
+ },
1796
+ );
1797
+ const unavailableOutputs = readiness.outputs.flatMap((output) =>
1798
+ output.status === "unavailable" && output.diagnostic
1799
+ ? [output.diagnostic]
1800
+ : [],
1801
+ );
1802
+ if (!readiness.closure.executable || unavailableOutputs.length > 0) {
1803
+ return {
1804
+ kind: "graph-rejected",
1805
+ changeId: request.graph.changeId,
1806
+ graphHash: request.graphHash,
1807
+ diagnostics: [...readiness.closure.diagnostics, ...unavailableOutputs],
1808
+ };
1809
+ }
1810
+ this.registry.admitGraph(
1811
+ request.graph,
1812
+ request.graphHash,
1813
+ workspaceRoot,
1814
+ request.state,
1815
+ );
1816
+ const red = readiness.phases.filter((phase) => phase.phase === "red");
1817
+ return {
1818
+ kind: "graph-admitted",
1819
+ changeId: request.graph.changeId,
1820
+ graphHash: request.graphHash,
1821
+ readyTasks: red
1822
+ .filter((phase) => phase.ready)
1823
+ .map((phase) => phase.taskId),
1824
+ blockedTasks: red
1825
+ .filter((phase) => !phase.ready && !phase.completed)
1826
+ .map((phase) => phase.taskId),
1827
+ };
1828
+ }
1829
+
1536
1830
  private async run(
1537
1831
  request: unknown,
1538
1832
  ctx?: RunContext,
@@ -1555,70 +1849,136 @@ export class Runtime {
1555
1849
  let envelope: RequestEnvelope;
1556
1850
  if (isImplementRunRequest(validation.value)) {
1557
1851
  const runRequest = validation.value;
1558
- const attempt = runRequest.attempt;
1559
1852
  const workspaceRoot = canonicalWorkspaceRoot(ctx.cwd);
1560
- if (
1561
- runRequest.kind === "open-task" &&
1562
- workspaceRoot &&
1563
- this.registry.has(
1564
- taskRecordKey(workspaceRoot, attempt.changeId, attempt.taskId),
1565
- )
1566
- ) {
1567
- throw new Error("duplicate task open protocol error");
1568
- }
1569
- if (runRequest.kind === "open-task" && signal?.aborted) {
1570
- return cancelledOutcome({
1571
- taskId: attempt.taskId,
1572
- requestId: attempt.requestId,
1573
- phase: attempt.phase,
1574
- });
1575
- }
1576
1853
  if (!workspaceRoot) {
1577
1854
  throw new Error("implementation workspace is unavailable");
1578
1855
  }
1856
+ if (runRequest.kind === "admit-graph") {
1857
+ if (signal?.aborted) return cancellationError(signal);
1858
+ return this.admitImplementGraph(runRequest, workspaceRoot);
1859
+ }
1860
+
1861
+ const attempt = runRequest.attempt;
1862
+ const graph = this.registry.getGraph(workspaceRoot, attempt.changeId);
1863
+ if (!graph) {
1864
+ throw new Error("Implement graph is not admitted");
1865
+ }
1866
+ const boundary = taskBoundaryOf(graph, attempt.taskId);
1867
+ if (!boundary) throw new Error("task identity mismatch");
1579
1868
  const key = taskRecordKey(
1580
1869
  workspaceRoot,
1581
1870
  attempt.changeId,
1582
1871
  attempt.taskId,
1583
1872
  );
1584
- if (runRequest.kind === "open-task") {
1585
- const phases = runRequest.boundary.phases;
1586
- const readiness = assessVerificationReadiness(
1587
- workspaceRoot,
1588
- [
1589
- phases.red.verification,
1590
- phases.green.verification,
1591
- ...(phases.refactor ? [phases.refactor.verification] : []),
1592
- ],
1873
+ const existing = this.registry.get(key);
1874
+ let existingIdentity: string | undefined;
1875
+ if (existing) {
1876
+ if (!ctx.model) {
1877
+ throw new Error("implementation model is unavailable");
1878
+ }
1879
+ existingIdentity = workerIdentity(ctx.model);
1880
+ if (existing.workerIdentity !== existingIdentity) {
1881
+ throw new Error("provider/model identity mismatch");
1882
+ }
1883
+ }
1884
+ if (existing?.state.kind === "blocked") {
1885
+ if (attempt.phase !== existing.state.phase) {
1886
+ throw new Error("terminal task phase mismatch");
1887
+ }
1888
+ return blockedOutcome(
1593
1889
  {
1594
- allowedMissingInputs: [
1595
- ...new Set([
1596
- ...phases.red.write,
1597
- ...phases.green.write,
1598
- ...(phases.refactor?.write ?? []),
1599
- ]),
1600
- ],
1890
+ taskId: existing.boundary.taskId,
1891
+ requestId: attempt.requestId,
1892
+ phase: existing.state.phase,
1601
1893
  },
1894
+ existing.state.failure,
1602
1895
  );
1603
- const diagnostic = readiness.diagnostics[0];
1604
- if (diagnostic) {
1605
- if (diagnostic.kind === "design-readiness") {
1606
- throw new Error(
1607
- `Implement protocol error: ${diagnostic.code}: ${diagnostic.message}`,
1608
- );
1609
- }
1896
+ }
1897
+ if (existing?.state.kind === "completed") {
1898
+ if (attempt.phase !== existing.state.finalPhase) {
1899
+ throw new Error("terminal task phase mismatch");
1900
+ }
1901
+ return {
1902
+ kind: "completed",
1903
+ taskId: existing.boundary.taskId,
1904
+ requestId: attempt.requestId,
1905
+ finalPhase: existing.state.finalPhase,
1906
+ };
1907
+ }
1908
+ if (graph.completedTasks.has(attempt.taskId)) {
1909
+ const finalPhase = boundary.phases.refactor ? "refactor" : "green";
1910
+ if (attempt.phase !== finalPhase) {
1911
+ throw new Error("terminal task phase mismatch");
1912
+ }
1913
+ return {
1914
+ kind: "completed",
1915
+ taskId: attempt.taskId,
1916
+ requestId: attempt.requestId,
1917
+ finalPhase,
1918
+ };
1919
+ }
1920
+ const attemptReason = validatePhaseAttemptAgainstBoundary(
1921
+ boundary,
1922
+ attempt,
1923
+ );
1924
+ if (attemptReason !== null) throw new Error(attemptReason);
1925
+ if (existing) assertAttemptAllowed(existing, attempt);
1926
+ else if (attempt.phase !== "red") {
1927
+ throw new Error("task must begin with Red");
1928
+ }
1929
+
1930
+ const readiness = assessImplementGraphReadiness(
1931
+ workspaceRoot,
1932
+ graph.boundary,
1933
+ this.graphFacts(graph),
1934
+ );
1935
+ const phaseReadiness = readiness.phases.find(
1936
+ (phase) =>
1937
+ phase.taskId === attempt.taskId && phase.phase === attempt.phase,
1938
+ );
1939
+ if (!phaseReadiness) throw new Error("task phase is not declared");
1940
+ if (!phaseReadiness.ready) {
1941
+ const dependencyDiagnostics = phaseReadiness.diagnostics.filter(
1942
+ (diagnostic) =>
1943
+ diagnostic.kind === "graph-readiness" &&
1944
+ diagnostic.code === "dependency-blocked",
1945
+ );
1946
+ if (dependencyDiagnostics.length > 0) {
1947
+ return {
1948
+ kind: "dependency-blocked",
1949
+ taskId: attempt.taskId,
1950
+ requestId: attempt.requestId,
1951
+ phase: attempt.phase,
1952
+ diagnostics: dependencyDiagnostics,
1953
+ };
1954
+ }
1955
+ const diagnostic = phaseReadiness.diagnostics[0];
1956
+ if (!diagnostic) throw new Error("task phase is not ready");
1957
+ if (diagnostic.kind === "design-readiness") {
1958
+ throw new Error(`Implement protocol error: ${diagnostic.code}`);
1959
+ }
1960
+ if (diagnostic.kind === "verification-adapter") {
1610
1961
  return {
1611
1962
  ok: false,
1612
- error: `${diagnostic.kind}/${diagnostic.code}: ${diagnostic.message}`,
1963
+ error: `${diagnostic.kind}/${diagnostic.code}`,
1613
1964
  failure: {
1614
1965
  kind: diagnostic.kind,
1615
1966
  code: diagnostic.code,
1616
1967
  },
1617
1968
  };
1618
1969
  }
1970
+ return blockedOutcome(
1971
+ {
1972
+ taskId: attempt.taskId,
1973
+ requestId: attempt.requestId,
1974
+ phase: attempt.phase,
1975
+ },
1976
+ { kind: "graph-readiness", diagnostic },
1977
+ );
1619
1978
  }
1979
+
1620
1980
  if (
1621
- runRequest.kind === "open-task" &&
1981
+ !existing &&
1622
1982
  this.registry
1623
1983
  .values()
1624
1984
  .some(
@@ -1626,10 +1986,7 @@ export class Runtime {
1626
1986
  record.workspaceRoot === workspaceRoot &&
1627
1987
  record.state.kind !== "blocked" &&
1628
1988
  record.state.kind !== "completed" &&
1629
- declarationsConflict(
1630
- taskConflictOf(runRequest.boundary),
1631
- record.conflict,
1632
- ),
1989
+ declarationsConflict(taskConflictOf(boundary), record.conflict),
1633
1990
  )
1634
1991
  ) {
1635
1992
  return {
@@ -1642,9 +1999,9 @@ export class Runtime {
1642
1999
  if (!ctx.model) {
1643
2000
  throw new Error("implementation model is unavailable");
1644
2001
  }
1645
- const identity = workerIdentity(ctx.model);
2002
+ const identity = existingIdentity ?? workerIdentity(ctx.model);
1646
2003
  let record: TaskRecord;
1647
- if (runRequest.kind === "open-task") {
2004
+ if (!existing) {
1648
2005
  const endpoint = resolveSubagentEndpoint("implementation-worker", {
1649
2006
  cwd: ctx.cwd,
1650
2007
  });
@@ -1655,7 +2012,7 @@ export class Runtime {
1655
2012
  message: describeInvalidSubagentEndpoint(endpoint),
1656
2013
  } as const;
1657
2014
  record = this.registry.open(
1658
- runRequest.boundary,
2015
+ boundary,
1659
2016
  identity,
1660
2017
  workspaceRoot,
1661
2018
  attempt,
@@ -1677,46 +2034,15 @@ export class Runtime {
1677
2034
  const subagentEndpoint: Readonly<SubagentEndpoint> | null =
1678
2035
  endpoint.kind === "custom" ? endpoint.endpoint : null;
1679
2036
  record = this.registry.open(
1680
- runRequest.boundary,
2037
+ boundary,
1681
2038
  identity,
1682
2039
  workspaceRoot,
1683
2040
  attempt,
1684
2041
  subagentEndpoint,
1685
2042
  );
1686
2043
  } else {
1687
- const existing = this.registry.get(key);
1688
- if (!existing) {
1689
- throw new Error("task identity mismatch or task is not open");
1690
- }
1691
- if (existing.workerIdentity !== identity) {
1692
- throw new Error("provider/model identity mismatch");
1693
- }
1694
- const attemptReason = validatePhaseAttemptAgainstBoundary(
1695
- existing.boundary,
1696
- attempt,
1697
- );
1698
- if (attemptReason !== null) throw new Error(attemptReason);
1699
- assertAttemptAllowed(existing, attempt);
1700
2044
  record = existing;
1701
2045
  }
1702
- if (record.state.kind === "blocked") {
1703
- return blockedOutcome(
1704
- {
1705
- taskId: record.boundary.taskId,
1706
- requestId: attempt.requestId,
1707
- phase: record.state.phase,
1708
- },
1709
- record.state.failure,
1710
- );
1711
- }
1712
- if (record.state.kind === "completed") {
1713
- return {
1714
- kind: "completed",
1715
- taskId: record.boundary.taskId,
1716
- requestId: attempt.requestId,
1717
- finalPhase: record.state.finalPhase,
1718
- };
1719
- }
1720
2046
  if (signal?.aborted) {
1721
2047
  return cancelledOutcome({
1722
2048
  taskId: record.boundary.taskId,
@@ -1728,7 +2054,7 @@ export class Runtime {
1728
2054
  this.taskRecords.set(envelope, record);
1729
2055
  } else {
1730
2056
  if (signal?.aborted) return cancellationError(signal);
1731
- envelope = cloneLegacyEnvelope(validation.value);
2057
+ envelope = cloneNonImplementEnvelope(validation.value);
1732
2058
  }
1733
2059
  const context: StoredRunContext = {
1734
2060
  ctx,
@@ -1986,7 +2312,7 @@ export class Runtime {
1986
2312
  launchIndex: 1,
1987
2313
  correction: structuredClone(firstFailure),
1988
2314
  };
1989
- return finish(retryOutcome(identity, "worker", firstFailure.kind));
2315
+ return finish(retryOutcome(identity, "worker", firstFailure));
1990
2316
  }
1991
2317
 
1992
2318
  task.state = {