@tea-agent/loop-agent 0.42.0-next.8 → 0.42.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 (161) hide show
  1. package/CHANGELOG.md +56 -50
  2. package/dist/application/dag/run-dag.js +41 -0
  3. package/dist/application/task-lifecycle/advance.js +9 -4
  4. package/dist/build-stamp.json +3 -3
  5. package/dist/cli/program.js +1 -1
  6. package/dist/commands/dag-rerun-task.js +2 -0
  7. package/dist/commands/task-source-prepare.js +3 -1
  8. package/dist/executors/dag-pi-executor.js +1809 -213
  9. package/dist/executors/pi-extension-resolver.js +14 -2
  10. package/dist/executors/shell-executor.js +178 -73
  11. package/dist/shared/dag-failure-category.js +6 -0
  12. package/dist/task/contract/apply.js +36 -2
  13. package/dist/task/source-prepare/parse-intent.js +7 -0
  14. package/dist/worker/console/chat/chat-event-store.js +4 -2
  15. package/dist/worker/console/chat/pi-runtime.js +45 -2
  16. package/dist/worker/console/chat/resource-loader.js +4 -1
  17. package/dist/worker/console/chat/routes.js +28 -12
  18. package/dist/worker/console/chat/sdd-data-alignment.js +13 -2
  19. package/dist/worker/console/chat/session-store.js +5 -1
  20. package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
  21. package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
  22. package/dist/worker/console/chat/subagents/index.js +5 -0
  23. package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
  24. package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
  25. package/dist/worker/console/chat/subagents/types.js +13 -0
  26. package/dist/worker/console/chat/tool-preview.js +10 -0
  27. package/dist/worker/console/chat/tools.js +11 -1
  28. package/dist/worker/console/chat/turn-process.js +1 -0
  29. package/dist/worker/console/interview/tools.js +1 -0
  30. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C6n9_m0P.js → abnfDiagram-N423BO3Z-B0Q0nClf.js} +1 -1
  31. package/dist/worker/console/static/assets/{arc-DQh-IfZ1.js → arc-DCPjC19G.js} +1 -1
  32. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-54NnrwUC.js → architectureDiagram-T3A2C74G-CP5n9jmG.js} +1 -1
  33. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-pivRALGK.js → blockDiagram-VBNYF7ZC-CmaWi_Wg.js} +1 -1
  34. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BR7OV2NJ.js → c4Diagram-5PPSVZJV-Ct5tcMfZ.js} +1 -1
  35. package/dist/worker/console/static/assets/channel-DAS07MdS.js +1 -0
  36. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B1Aq6BcK.js → chunk-2GRJ4B5K-C0osm1Zf.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CbdU0rjo.js → chunk-2Q5K7J3B-CDWviED7.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Dqi2GJeD.js → chunk-5RXB4S5H-mtSk1-j7.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BYn1Hu4R.js → chunk-5VM5RSS4-D5J9PHC7.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DRLjDL0k.js → chunk-6Q2QTUOP-Begg4WAa.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CY9Xx2jV.js → chunk-GF5L2VYU-sF1AEy7T.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BNCZs7_z.js → chunk-JWPE2WC7-BqbsWXZb.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DZ8AStLL.js → chunk-KBJHAD2P-BKJrPcJ6.js} +1 -1
  44. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DsxjYrzz.js → chunk-RYQCIY6F-CcwNMRho.js} +1 -1
  45. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Ddx1KC1l.js → chunk-XXDRQBXY-BEJi3iIs.js} +1 -1
  46. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cwk-5jiW.js +1 -0
  47. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cwk-5jiW.js +1 -0
  48. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-W9TveCnK.js → cose-bilkent-JH36ORCC-DRLgTvVu.js} +1 -1
  49. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-l9j_ztZH.js → cynefin-VYW2F7L2-BoAVG3cs.js} +1 -1
  50. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BMJMKi4G.js → cynefinDiagram-MW4NZA55-Bzol06hR.js} +1 -1
  51. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Bb4mG9pH.js → dagre-VZM6K2ZE-UmliJM77.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BMgL_Qv0.js → diagram-7IWD3JNH-BWLcJkFo.js} +1 -1
  53. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bvo2T4OQ.js → diagram-B4RE2ZJO-CgApOm-O.js} +1 -1
  54. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-_5kWRN9c.js → diagram-LBJQPF4R-Bh3RgTLs.js} +1 -1
  55. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DqdMrltM.js → diagram-Q27KOJAE-Dsh-D5nE.js} +1 -1
  56. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CDDYsNkp.js → diagram-UB23O5K3-Dkbbpcpb.js} +1 -1
  57. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-c4nfnZUV.js → ebnfDiagram-BXEA7PRR-3EBaA3t3.js} +1 -1
  58. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DnkUcNMs.js → erDiagram-JOGREHBK-Bq2Dc5ok.js} +1 -1
  59. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Dz0KGLZE.js → flowDiagram-UKHOOZJN-De7y55ha.js} +1 -1
  60. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cj1t1uka.js → ganttDiagram-PKOTCBZU-fLiDbQRh.js} +1 -1
  61. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-tp2FrBHd.js → gitGraphDiagram-DS77QQ5N-Du617mp1.js} +1 -1
  62. package/dist/worker/console/static/assets/index-C0O48S_P.js +449 -0
  63. package/dist/worker/console/static/assets/index-CzKf4U8P.css +1 -0
  64. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DQwJS8DH.js → infoDiagram-6WML65LV-BwmlF-XP.js} +1 -1
  65. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-eimCQWD4.js → ishikawaDiagram-WSZJBQD7-zsGtS59u.js} +1 -1
  66. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Dd9Gbwgv.js → journeyDiagram-NVQOT4AX-DTTPTe3f.js} +1 -1
  67. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Qve3cyft.js → kanban-definition-27J2QSJJ-BkXg9aP-.js} +1 -1
  68. package/dist/worker/console/static/assets/{linear-UXKSe36Z.js → linear-7U2ue5IE.js} +1 -1
  69. package/dist/worker/console/static/assets/{mermaid.core-CWhj4JXN.js → mermaid.core-BUuGHmWO.js} +5 -5
  70. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-yTcwf4SJ.js → mindmap-definition-FAOFIHXS-7q6fX0Sv.js} +1 -1
  71. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-_7qToaZP.js → pegDiagram-VL7TDLO6-CkQPJN55.js} +1 -1
  72. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CXmrKmDm.js → pieDiagram-7S7Q4E2Y-CCal_tfX.js} +1 -1
  73. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-hMmrJyaS.js → quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js} +1 -1
  74. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BXw8tCBe.js → railroadDiagram-AXF67PYL-CJmww7D-.js} +1 -1
  75. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BmjgnLZl.js → requirementDiagram-LRYGKXZP-D6ldWJEv.js} +1 -1
  76. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bj77SPpN.js → sankeyDiagram-W5VNT64P-VGh33I9e.js} +1 -1
  77. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-B2FDVysL.js → sequenceDiagram-SI44F4Z6-CCkDrIjU.js} +1 -1
  78. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-UChNY9ZZ.js → sizeCapture-X5ZJPWSS-NC8f5Otb.js} +1 -1
  79. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-wbAEkbd7.js → stateDiagram-OKZ733FA-_92ezZdF.js} +1 -1
  80. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyJg5gzW.js +1 -0
  81. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DCEkU8HR.js → swimlanes-SLNWSIFB-BdUCGtbP.js} +2 -2
  82. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B1cw_0Uy.js +8 -0
  83. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BGp7H06U.js → timeline-definition-Z64GVDOM-CdD1H8ia.js} +1 -1
  84. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DsCIQuWR.js → vennDiagram-T6HMQDX7-Dkdk3oFo.js} +1 -1
  85. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-cRZLP4Xe.js → wardleyDiagram-T6FBY63Y-CKu2uPPY.js} +1 -1
  86. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CP0xLR9Y.js → xychartDiagram-ELKLHX3M-ClqAnuG8.js} +1 -1
  87. package/dist/worker/console/static/index.html +2 -2
  88. package/dist/worker/console/static-src/chat-view-types.js +1 -1
  89. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +23 -0
  90. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +32 -0
  91. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
  92. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +2 -0
  93. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
  94. package/dist/worker/console/static-src/operator-chat/useChatStream.js +30 -14
  95. package/dist/worker/console/static-src/operator-chat/useChatThread.js +133 -5
  96. package/dist/worker/console/static-src/shell/workspace-route.js +4 -0
  97. package/dist/worker/observe/routes.js +4 -0
  98. package/dist/worker/observe/static/constants.js +22 -22
  99. package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
  100. package/dist/worker/observe/static/dag-history-labels.js +1 -0
  101. package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
  102. package/dist/worker/observe/static/dag-inspector-humanize.js +339 -0
  103. package/dist/worker/observe/static/dag-node-purpose.js +10 -10
  104. package/dist/worker/observe/static/index.html +4 -4
  105. package/dist/worker/observe/static/prompt-restart-candidates.js +4 -2
  106. package/dist/worker/observe/static/relations.js +5 -5
  107. package/dist/worker/observe/static/styles.css +171 -0
  108. package/dist/worker/observe/static/views/dag-graph.js +1 -1
  109. package/dist/worker/observe/static/views/dag-inspector.js +329 -174
  110. package/dist/worker/observe/static/views/dag.js +16 -12
  111. package/dist/worker/observe/static/views/session-timeline.js +5 -3
  112. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +18 -0
  113. package/dist/workflows/dag/backend-test-plan-protocol.js +104 -0
  114. package/dist/workflows/dag/backend-test-scenario-param.js +30 -1
  115. package/dist/workflows/dag/dag-retry-schema.js +3 -0
  116. package/dist/workflows/dag/frontend-contract-facts.js +130 -0
  117. package/dist/workflows/dag/frontend-design-policy.js +101 -16
  118. package/dist/workflows/dag/frontend-implementation-contract.js +410 -41
  119. package/dist/workflows/dag/frontend-plan-render.js +13 -2
  120. package/dist/workflows/dag/frontend-prewrite-gate.js +1 -1
  121. package/dist/workflows/dag/frontend-recovery-controller.js +25 -1
  122. package/dist/workflows/dag/frontend-recovery-plan.js +6 -2
  123. package/dist/workflows/dag/frontend-recovery-run.js +169 -15
  124. package/dist/workflows/dag/frontend-risk.js +2 -0
  125. package/dist/workflows/dag/frontend-shadow-dual-write.js +17 -1
  126. package/dist/workflows/dag/frontend-shape.js +16 -6
  127. package/dist/workflows/dag/frontend-test-execution-evidence.js +48 -0
  128. package/dist/workflows/dag/frontend-typed-event-store.js +3 -0
  129. package/dist/workflows/dag/frontend-verification-trace.js +38 -4
  130. package/dist/workflows/dag/frontend-writer-admission.js +46 -12
  131. package/dist/workflows/dag/init-hybrid.js +59 -29
  132. package/dist/workflows/dag/node-execution.js +277 -85
  133. package/dist/workflows/dag/recovery-lease.js +106 -16
  134. package/dist/workflows/dag/rerun-feedback.js +60 -0
  135. package/dist/workflows/dag/rerun-plan.js +90 -4
  136. package/dist/workflows/dag/rerun-run.js +28 -6
  137. package/dist/workflows/dag/rerun-task.js +224 -15
  138. package/dist/workflows/dag/retry-policy.js +27 -10
  139. package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
  140. package/dist/workflows/dag/runner.js +238 -29
  141. package/dist/workflows/dag/scheduler.js +21 -6
  142. package/dist/workflows/dag/structured-output-repair.js +4 -1
  143. package/dist/workflows/dag/types.js +4 -3
  144. package/dist/workflows/dag/validate.js +10 -8
  145. package/dist/workflows/dag/workspace-checkpoint.js +66 -0
  146. package/docs/operations/README.md +1 -0
  147. package/docs/templates/agent-dag.schema.json +2 -2
  148. package/docs/templates/backend-test-dag.json +6 -4
  149. package/docs/templates/frontend-implementation-contract.schema.json +34 -2
  150. package/harness.json +1 -1
  151. package/package.json +4 -5
  152. package/skills/frontend-plan/SKILL.md +14 -1
  153. package/skills/frontend-plan/references/decision-contract.md +101 -5
  154. package/skills/frontend-plan/references/design-decisions.md +32 -0
  155. package/dist/worker/console/static/assets/channel-DsZxrgqe.js +0 -1
  156. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +0 -1
  157. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +0 -1
  158. package/dist/worker/console/static/assets/index-D9Sc0f0y.js +0 -449
  159. package/dist/worker/console/static/assets/index-DDQc5a50.css +0 -1
  160. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +0 -1
  161. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.js +0 -8
@@ -1,6 +1,8 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
3
+ import { hostname as localHostname } from "node:os";
3
4
  import path from "node:path";
5
+ import { setTimeout as delay } from "node:timers/promises";
4
6
  import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
5
7
  import { DAG_RUNS_DIR } from "./lifecycle.js";
6
8
  function recoveryLeasePath(cwd, parentRunId) {
@@ -23,6 +25,11 @@ function parseLease(raw, parentRunId) {
23
25
  record.kind !== "standalone-task-rerun") ||
24
26
  typeof record.leaseToken !== "string" ||
25
27
  record.leaseToken.length === 0 ||
28
+ typeof record.ownerPid !== "number" ||
29
+ !Number.isInteger(record.ownerPid) ||
30
+ record.ownerPid <= 0 ||
31
+ typeof record.ownerHostname !== "string" ||
32
+ record.ownerHostname.length === 0 ||
26
33
  typeof record.acquiredAt !== "string" ||
27
34
  typeof record.executionRef !== "undefined" &&
28
35
  typeof record.executionRef !== "string") {
@@ -30,6 +37,60 @@ function parseLease(raw, parentRunId) {
30
37
  }
31
38
  return record;
32
39
  }
40
+ const LEASE_MUTATION_LOCK_STALE_MS = 60_000;
41
+ const LEASE_MUTATION_LOCK_RETRIES = 200;
42
+ async function withLeaseMutationLock(leasePath, operation) {
43
+ const lockPath = `${leasePath}.mutation-lock`;
44
+ for (let attempt = 0; attempt < LEASE_MUTATION_LOCK_RETRIES; attempt += 1) {
45
+ try {
46
+ await mkdir(lockPath);
47
+ }
48
+ catch (error) {
49
+ if (!(error && typeof error === "object" && "code" in error && error.code === "EEXIST")) {
50
+ throw error;
51
+ }
52
+ try {
53
+ const lockStat = await stat(lockPath);
54
+ if (Date.now() - lockStat.mtimeMs >= LEASE_MUTATION_LOCK_STALE_MS) {
55
+ await rm(lockPath, { recursive: true, force: true });
56
+ continue;
57
+ }
58
+ }
59
+ catch (statError) {
60
+ if (statError &&
61
+ typeof statError === "object" &&
62
+ "code" in statError &&
63
+ statError.code === "ENOENT") {
64
+ continue;
65
+ }
66
+ throw statError;
67
+ }
68
+ await delay(10);
69
+ continue;
70
+ }
71
+ try {
72
+ return await operation();
73
+ }
74
+ finally {
75
+ await rm(lockPath, { recursive: true, force: true });
76
+ }
77
+ }
78
+ throw new Error("dag recovery lease mutation lock is busy");
79
+ }
80
+ function defaultIsProcessAlive(pid) {
81
+ try {
82
+ process.kill(pid, 0);
83
+ return true;
84
+ }
85
+ catch {
86
+ return false;
87
+ }
88
+ }
89
+ function leaseOwnerIsAlive(input) {
90
+ if (input.lease.ownerHostname !== input.hostname)
91
+ return true;
92
+ return input.isProcessAlive(input.lease.ownerPid);
93
+ }
33
94
  export async function acquireDagRecoveryLease(input) {
34
95
  const leasePath = recoveryLeasePath(input.cwd, input.parentRunId);
35
96
  const lease = {
@@ -38,13 +99,15 @@ export async function acquireDagRecoveryLease(input) {
38
99
  requestId: input.requestId,
39
100
  kind: input.kind,
40
101
  leaseToken: randomBytes(16).toString("hex"),
102
+ ownerPid: process.pid,
103
+ ownerHostname: localHostname(),
41
104
  acquiredAt: (input.now ?? (() => new Date()))().toISOString(),
42
105
  };
43
106
  await mkdir(path.dirname(leasePath), { recursive: true });
44
107
  try {
45
108
  // `wx` is the cross-process compare-and-create primitive. The lease is
46
- // deliberately immutable for a parent run: a later recovery must follow
47
- // the committed child lineage rather than fork another DAG.
109
+ // parent-scoped for the full recovery lineage. Ownership may only be
110
+ // replaced by claimDagRecoveryLease after the recorded owner is proven dead.
48
111
  await writeFile(leasePath, `${JSON.stringify(lease, null, 2)}\n`, {
49
112
  encoding: "utf8",
50
113
  flag: "wx",
@@ -59,22 +122,49 @@ export async function acquireDagRecoveryLease(input) {
59
122
  return { kind: "held", leasePath, lease: existing };
60
123
  }
61
124
  }
125
+ /**
126
+ * Reclaim a lease only after its recorded same-host owner is proven dead.
127
+ * The mutation lock serializes reclaim with execution binding, so a live owner
128
+ * can never race a stale-owner takeover and launch a second recovery.
129
+ */
130
+ export async function claimDagRecoveryLease(input) {
131
+ const hostname = input.hostname ?? localHostname();
132
+ const isProcessAlive = input.isProcessAlive ?? defaultIsProcessAlive;
133
+ return withLeaseMutationLock(input.leasePath, async () => {
134
+ const existing = parseLease(JSON.parse(await readFile(input.leasePath, "utf8")), input.lease.parentRunId);
135
+ if (existing.leaseToken !== input.lease.leaseToken ||
136
+ leaseOwnerIsAlive({ lease: existing, hostname, isProcessAlive })) {
137
+ return { kind: "held", leasePath: input.leasePath, lease: existing };
138
+ }
139
+ const claimed = {
140
+ ...existing,
141
+ leaseToken: randomBytes(16).toString("hex"),
142
+ ownerPid: process.pid,
143
+ ownerHostname: hostname,
144
+ acquiredAt: (input.now ?? (() => new Date()))().toISOString(),
145
+ };
146
+ await writeJsonAtomic(input.leasePath, claimed);
147
+ return { kind: "acquired", leasePath: input.leasePath, lease: claimed };
148
+ });
149
+ }
62
150
  /** Persist the operation/run identity before the caller is allowed to execute it. */
63
151
  export async function bindDagRecoveryLeaseExecution(input) {
64
152
  if (!input.executionRef.trim()) {
65
153
  throw new Error("dag recovery lease executionRef is required");
66
154
  }
67
- const existing = parseLease(JSON.parse(await readFile(input.leasePath, "utf8")), input.lease.parentRunId);
68
- if (existing.leaseToken !== input.lease.leaseToken) {
69
- throw new Error("dag recovery lease ownership drift");
70
- }
71
- if (existing.executionRef && existing.executionRef !== input.executionRef) {
72
- throw new Error("dag recovery lease execution identity drift");
73
- }
74
- const bound = {
75
- ...existing,
76
- executionRef: input.executionRef,
77
- };
78
- await writeJsonAtomic(input.leasePath, bound);
79
- return bound;
155
+ return withLeaseMutationLock(input.leasePath, async () => {
156
+ const existing = parseLease(JSON.parse(await readFile(input.leasePath, "utf8")), input.lease.parentRunId);
157
+ if (existing.leaseToken !== input.lease.leaseToken) {
158
+ throw new Error("dag recovery lease ownership drift");
159
+ }
160
+ if (existing.executionRef && existing.executionRef !== input.executionRef) {
161
+ throw new Error("dag recovery lease execution identity drift");
162
+ }
163
+ const bound = {
164
+ ...existing,
165
+ executionRef: input.executionRef,
166
+ };
167
+ await writeJsonAtomic(input.leasePath, bound);
168
+ return bound;
169
+ });
80
170
  }
@@ -399,6 +399,66 @@ export async function deriveDagRerunFeedback(input) {
399
399
  },
400
400
  });
401
401
  }
402
+ // Preserve actionable writer failures even when the run stopped before a
403
+ // final review node could commit typed findings. This is especially
404
+ // important for provider length exhaustion: the next run must see the
405
+ // stop reason/tool statistics instead of repeating the same prompt.
406
+ const writerNodeId = ["frontend-implement-pi", "implement-pi"].find((nodeId) => {
407
+ const node = input.state.nodes[nodeId];
408
+ return (node?.status === "ERROR" &&
409
+ [
410
+ "empty-output",
411
+ "output-limit",
412
+ "invalid-output",
413
+ "writer-thinking-exhausted",
414
+ "writer-budget-exhausted",
415
+ "incomplete-write-set",
416
+ "partial-success-with-context-overflow",
417
+ ].includes(node.failureCategory ?? ""));
418
+ });
419
+ if (writerNodeId) {
420
+ const writerNode = await readNodeRecord(input.runDir, pass, undefined, writerNodeId, input.state.nodes[writerNodeId]);
421
+ if (writerNode) {
422
+ const writerFeedback = [
423
+ `writer failure category: ${writerNode.failureCategory ?? "unknown"}`,
424
+ writerNode.stopReason ? `provider stopReason: ${writerNode.stopReason}` : "",
425
+ typeof writerNode.writeToolCallCount === "number"
426
+ ? `write tool calls: ${writerNode.writeToolCallCount}`
427
+ : "",
428
+ writerNode.stderr?.trim() ?? "",
429
+ canonicalNodeOutput(writerNode),
430
+ ]
431
+ .filter(Boolean)
432
+ .join("\n\n");
433
+ const parentSourceBindingHash = sourceBindingHash(input.spec);
434
+ return withDigest({
435
+ schemaVersion: 1,
436
+ kind: "unresolved-terminal-feedback",
437
+ parentRunId: input.parentRunId,
438
+ taskId: input.taskId,
439
+ sourceNodeId: writerNodeId,
440
+ verdict: "request-revision",
441
+ failureCategory: writerNode.failureCategory,
442
+ feedbackText: scrubAndBoundFeedbackText(writerFeedback, input.runDir, input.state.cwd),
443
+ evidenceRefs: [
444
+ {
445
+ nodeId: writerNodeId,
446
+ preservedNodeRecordPath: `${writerNodeId}.json`,
447
+ },
448
+ ],
449
+ attemptSummary: { completedPasses: 1, maxPasses: 1 },
450
+ binding: {
451
+ ...(parentSourceBindingHash
452
+ ? { sourceCanonicalHash: parentSourceBindingHash }
453
+ : {}),
454
+ ...(input.spec.taskContractBinding?.canonicalHash
455
+ ? { taskContractCanonicalHash: input.spec.taskContractBinding.canonicalHash }
456
+ : {}),
457
+ status: "unknown",
458
+ },
459
+ });
460
+ }
461
+ }
402
462
  return undefined;
403
463
  }
404
464
  const completedPasses = pass?.pass ?? Math.max(1, input.state.convergence?.currentPass ?? 1);
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { z } from "zod";
4
4
  import { assertTaskContractBindingFresh } from "./task-contract-binding.js";
5
+ import { workspaceDriftedPaths, } from "./workspace-checkpoint.js";
5
6
  import { isSafeReadOnlyPiRetryCandidate, isTransportCrashedExclusiveWriter } from "./retry-policy.js";
6
7
  import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
7
8
  import { composeDagPromptOverridePrompt, MAX_DAG_RERUN_PROMPT_OVERRIDE_CHARS, } from "../../shared/dag-prompt-override.js";
@@ -1110,6 +1111,46 @@ function deriveSuggestedAction(input) {
1110
1111
  }
1111
1112
  return "manual";
1112
1113
  }
1114
+ /**
1115
+ * Gitignore-style-lite matcher for readSet/writeSet path patterns in scoped
1116
+ * workspace-drift evaluation: literal paths match exactly; `**` spans path
1117
+ * segments (a trailing `/**` also matches when nothing follows), `*` stays
1118
+ * within one segment, `?` is one non-separator char.
1119
+ */
1120
+ function pathMatchesWorkspacePattern(pathValue, pattern) {
1121
+ const normalizedPath = pathValue.replace(/\\/g, "/").replace(/^\.\//, "");
1122
+ const trimmed = pattern.trim().replace(/\\/g, "/").replace(/^\.\//, "");
1123
+ if (!trimmed || trimmed === "/")
1124
+ return false;
1125
+ if (trimmed === normalizedPath)
1126
+ return true;
1127
+ let source = "";
1128
+ for (let index = 0; index < trimmed.length; index += 1) {
1129
+ const char = trimmed[index];
1130
+ if (char === "*") {
1131
+ if (trimmed[index + 1] === "*") {
1132
+ if (trimmed[index + 2] === "/") {
1133
+ source += "(?:.*/)?";
1134
+ index += 2;
1135
+ }
1136
+ else {
1137
+ source += ".*";
1138
+ index += 1;
1139
+ }
1140
+ }
1141
+ else {
1142
+ source += "[^/]*";
1143
+ }
1144
+ }
1145
+ else if (char === "?") {
1146
+ source += "[^/]";
1147
+ }
1148
+ else {
1149
+ source += char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1150
+ }
1151
+ }
1152
+ return new RegExp(`^${source}$`).test(normalizedPath);
1153
+ }
1113
1154
  function deriveRisk(reasonCodes, resetCount) {
1114
1155
  if (reasonCodes.includes("restart-subgraph-contains-writer") ||
1115
1156
  reasonCodes.includes("restart-subgraph-contains-unsafe-shell") ||
@@ -1213,7 +1254,14 @@ export async function evaluateDagRerunPlan(input) {
1213
1254
  blockedReasons.push("parent-lifecycle-ineligible");
1214
1255
  }
1215
1256
  else if (input.parentState.status !== "failed" &&
1216
- input.parentState.status !== "partial_failed") {
1257
+ input.parentState.status !== "partial_failed" &&
1258
+ // A superseded parent is a terminal operator judgment about the run as
1259
+ // a whole, not about its deterministic read-only compute: its FINISHED
1260
+ // upstream nodes are still valid fact donors for the imported set, and
1261
+ // every unsafe case (writers in the reset subgraph, decision gates,
1262
+ // stale checkpoints, unresolved ERROR nodes outside the closure) keeps
1263
+ // its own dedicated block below.
1264
+ input.parentState.status !== "superseded") {
1217
1265
  reasonCodes.push("parent-lifecycle-ineligible");
1218
1266
  blockedReasons.push("parent-lifecycle-ineligible");
1219
1267
  }
@@ -1241,8 +1289,36 @@ export async function evaluateDagRerunPlan(input) {
1241
1289
  else if (input.currentWorkspace?.fingerprint &&
1242
1290
  input.parentTerminalWorkspace.fingerprint !==
1243
1291
  input.currentWorkspace.fingerprint) {
1244
- reasonCodes.push("workspace-drift");
1245
- blockedReasons.push("workspace-drift");
1292
+ // Whole-fingerprint drift is refined to content-level paths: the rerun
1293
+ // re-derives (readSet) and rewrites (writeSet) the reset subgraph's own
1294
+ // paths, so drift confined to those paths is recomputed by the rerun
1295
+ // itself and must not force a full standalone rerun. Drift anywhere
1296
+ // else — especially inputs of imported fact-reuse nodes — keeps the
1297
+ // strict block. Indeterminate checkpoints fail closed.
1298
+ const driftedPaths = workspaceDriftedPaths(input.parentTerminalWorkspace, input.currentWorkspace);
1299
+ const importedReadPaths = importedNodeIds.flatMap((nodeId) => {
1300
+ const task = tasksByIdForPlan.get(nodeId);
1301
+ return task?.readSet ?? [];
1302
+ });
1303
+ const scopedDrift = driftedPaths !== undefined &&
1304
+ driftedPaths.length > 0 &&
1305
+ driftedPaths.every((driftPath) => !importedReadPaths.some((pattern) => pathMatchesWorkspacePattern(driftPath, pattern)) &&
1306
+ resetNodeIds.some((nodeId) => {
1307
+ const task = tasksByIdForPlan.get(nodeId);
1308
+ if (!task)
1309
+ return false;
1310
+ return [
1311
+ ...(task.writeSet ?? []),
1312
+ ...(task.readSet ?? []),
1313
+ ].some((pattern) => pathMatchesWorkspacePattern(driftPath, pattern));
1314
+ }));
1315
+ if (scopedDrift) {
1316
+ reasonCodes.push("workspace-drift-scoped");
1317
+ }
1318
+ else {
1319
+ reasonCodes.push("workspace-drift");
1320
+ blockedReasons.push("workspace-drift");
1321
+ }
1246
1322
  }
1247
1323
  else if (!input.currentWorkspace?.fingerprint) {
1248
1324
  reasonCodes.push("workspace-checkpoint-missing");
@@ -1293,8 +1369,18 @@ export async function evaluateDagRerunPlan(input) {
1293
1369
  }
1294
1370
  for (const nodeId of importedNodeIds) {
1295
1371
  const record = input.parentState.nodes[nodeId];
1296
- if (!record)
1372
+ if (!record) {
1373
+ reasonCodes.push("parent-facts-invalid");
1374
+ blockedReasons.push("parent-facts-invalid");
1375
+ blockingNodes.push({ nodeId, reasonCode: "parent-facts-invalid" });
1297
1376
  continue;
1377
+ }
1378
+ if (record.status === "PENDING" || record.status === "RUNNING") {
1379
+ reasonCodes.push("parent-facts-invalid");
1380
+ blockedReasons.push("parent-facts-invalid");
1381
+ blockingNodes.push({ nodeId, reasonCode: "parent-facts-invalid" });
1382
+ continue;
1383
+ }
1298
1384
  if (record.status === "ERROR") {
1299
1385
  reasonCodes.push("parent-facts-invalid");
1300
1386
  blockedReasons.push("parent-facts-invalid");
@@ -88,11 +88,18 @@ const operatorRequestEnvelopeSchema = z
88
88
  })
89
89
  .strict();
90
90
  async function readExistingOperatorRequest(cwd, requestId) {
91
+ const requestPath = operatorRequestPath(cwd, requestId);
91
92
  try {
92
- const raw = JSON.parse(await readFile(operatorRequestPath(cwd, requestId), "utf-8"));
93
+ const raw = JSON.parse(await readFile(requestPath, "utf-8"));
93
94
  const parsed = operatorRequestEnvelopeSchema.safeParse(raw);
94
- if (!parsed.success)
95
- return undefined;
95
+ if (!parsed.success) {
96
+ throw new Error("REQUEST_ID_REUSE_CONFLICT: persisted operator request envelope is invalid or belongs to a different request kind");
97
+ }
98
+ if (parsed.data.requestId !== requestId ||
99
+ parsed.data.parentRunId !== parsed.data.result.plan.parentRunId ||
100
+ parsed.data.newRunId !== parsed.data.result.newRunId) {
101
+ throw new Error("REQUEST_ID_REUSE_CONFLICT: persisted operator request envelope binding mismatch");
102
+ }
96
103
  const result = parsed.data.result;
97
104
  return {
98
105
  ...result,
@@ -100,10 +107,18 @@ async function readExistingOperatorRequest(cwd, requestId) {
100
107
  idempotentReplay: true,
101
108
  };
102
109
  }
103
- catch {
104
- // not found or unreadable
110
+ catch (error) {
111
+ if (error &&
112
+ typeof error === "object" &&
113
+ "code" in error &&
114
+ error.code === "ENOENT") {
115
+ return undefined;
116
+ }
117
+ if (error instanceof Error && error.message.startsWith("REQUEST_ID_REUSE_CONFLICT:")) {
118
+ throw error;
119
+ }
120
+ throw new Error(`REQUEST_ID_REUSE_CONFLICT: invalid persisted operator request envelope: ${error instanceof Error ? error.message : String(error)}`);
105
121
  }
106
- return undefined;
107
122
  }
108
123
  /**
109
124
  * Idempotent replay of a previously persisted operator request, if any.
@@ -769,6 +784,13 @@ async function stageContinuationRun(input) {
769
784
  if (!parentRecord) {
770
785
  throw new Error(`parent node record missing for imported node: ${nodeId}`);
771
786
  }
787
+ if (parentRecord.status !== "FINISHED" &&
788
+ !(parentRecord.status === "SKIPPED" &&
789
+ (parentRecord.skippedReason?.includes("condition") ||
790
+ parentRecord.skippedReason?.includes("runIf") ||
791
+ parentRecord.skippedReason?.includes("run-if")))) {
792
+ throw new Error(`parent node is not a settled fact donor for imported node: ${nodeId}`);
793
+ }
772
794
  const { manifestNode, importedRecord } = await importNodeFacts({
773
795
  parentRunDir: input.parentRunDir,
774
796
  newRunDir: runDir,