@tea-agent/loop-agent 0.39.0-beta.7 → 0.39.0-beta.9

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 (164) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +23 -5
  3. package/README.md +2 -2
  4. package/dist/application/task-lifecycle/advance.js +12 -0
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/cli/command-definitions.js +7 -0
  7. package/dist/cli/program.js +5 -0
  8. package/dist/cli/update/runtime-activity.js +1 -29
  9. package/dist/commands/dag-follow-up.js +138 -0
  10. package/dist/commands/dag-rerun.js +1 -1
  11. package/dist/commands/init.js +26 -0
  12. package/dist/executors/dag-pi-executor.js +326 -8
  13. package/dist/executors/pi-read-budget-policy.js +166 -0
  14. package/dist/executors/shell-executor.js +26 -30
  15. package/dist/shared/dag-prompt-override.js +4 -5
  16. package/dist/shared/update/npm-client.js +33 -11
  17. package/dist/task/config-types.js +15 -0
  18. package/dist/task/contract/apply.js +11 -1
  19. package/dist/task/contract/constants.js +2 -0
  20. package/dist/task/contract/hash.js +3 -0
  21. package/dist/task/contract/observe.js +25 -4
  22. package/dist/task/contract/paths.js +2 -1
  23. package/dist/task/contract/project.js +15 -1
  24. package/dist/task/contract/schema.js +2 -0
  25. package/dist/task/contract/transaction.js +12 -0
  26. package/dist/task/frontend-project-capability.js +123 -4
  27. package/dist/task/source-prepare/completeness.js +188 -0
  28. package/dist/task/source-prepare/fragment-inventory.js +477 -0
  29. package/dist/task/source-prepare/index.js +3 -0
  30. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  31. package/dist/task/source-prepare/ledger-review.js +214 -0
  32. package/dist/task/source-prepare/ledger.js +545 -0
  33. package/dist/task/source-prepare/prepare.js +262 -1
  34. package/dist/task/source-prepare/semantic-intake.js +25 -5
  35. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  36. package/dist/task/source-prepare/types.js +22 -0
  37. package/dist/worker/cli.js +12 -26
  38. package/dist/worker/console/console-handoff.js +82 -0
  39. package/dist/worker/console/console-update-and-init.js +115 -0
  40. package/dist/worker/console/console-update-runtime.js +84 -0
  41. package/dist/worker/console/draft-store.js +49 -0
  42. package/dist/worker/console/operation-store.js +1 -1
  43. package/dist/worker/console/operator-actions.js +129 -4
  44. package/dist/worker/console/operator-surface-health.js +1 -0
  45. package/dist/worker/console/prd-intake-bridge.js +13 -0
  46. package/dist/worker/console/routes.js +3 -0
  47. package/dist/worker/console/server.js +73 -5
  48. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-bPer1waP.js → abnfDiagram-N423BO3Z-D1-Z-BBa.js} +1 -1
  49. package/dist/worker/console/static/assets/{arc-BLdkwucW.js → arc-q8qAYm5m.js} +1 -1
  50. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Bw4wo4I1.js → architectureDiagram-T3A2C74G-CAu4_lve.js} +1 -1
  51. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DUUkc0gr.js → blockDiagram-VBNYF7ZC-D5GhQvHm.js} +1 -1
  52. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BC3O0nob.js → c4Diagram-5PPSVZJV-ClQjeLkO.js} +1 -1
  53. package/dist/worker/console/static/assets/channel-ClAuhSUL.js +1 -0
  54. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Dd4g2nti.js → chunk-2GRJ4B5K-DLqWlFbo.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-s2EDm7-n.js → chunk-2Q5K7J3B-DkRT_usL.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-GqtHM2yQ.js → chunk-5RXB4S5H-Cqp7Chi3.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BzRFrN8j.js → chunk-5VM5RSS4-CedEdFB9.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Bw9nFjCJ.js → chunk-6Q2QTUOP-BHM8vP5x.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B4n-03Qm.js → chunk-GF5L2VYU-D7EFr377.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-Dt75ZdBD.js → chunk-JWPE2WC7-DR38mTWV.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-B_afMhHB.js → chunk-KBJHAD2P-BFv2LsR3.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Bd8Rqqg9.js → chunk-RYQCIY6F-BAvpJS5z.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-9boOMji4.js → chunk-XXDRQBXY-eKI9TO72.js} +1 -1
  64. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-cBgeCpVU.js +1 -0
  65. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-cBgeCpVU.js +1 -0
  66. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BtCycfCc.js → cose-bilkent-JH36ORCC-BMJXpgo4.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-eTOts9L1.js → cynefin-VYW2F7L2-CKJ2ofZx.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Cibd2mjg.js → cynefinDiagram-MW4NZA55-CsKnsazm.js} +1 -1
  69. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CDjDZtDV.js → dagre-VZM6K2ZE-DVQ46vIw.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Btexn-JJ.js → diagram-7IWD3JNH-D_RuonYS.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cyl8sdpM.js → diagram-B4RE2ZJO-D2Mt0nMu.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Ev-83E1t.js → diagram-LBJQPF4R-DjBDuye6.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-41LqE0Mc.js → diagram-Q27KOJAE-8VZkqpE_.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BC39T5R3.js → diagram-UB23O5K3-D0izYxia.js} +1 -1
  75. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DzcNRuri.js → ebnfDiagram-BXEA7PRR-CncG2I95.js} +1 -1
  76. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-B5Frvvnz.js → erDiagram-JOGREHBK-DiBUUXR8.js} +1 -1
  77. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BapxROIM.js → flowDiagram-UKHOOZJN-Dtc0fArb.js} +1 -1
  78. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cd4d4wQ0.js → ganttDiagram-PKOTCBZU-BPNFP5OZ.js} +1 -1
  79. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CrL6buFW.js → gitGraphDiagram-DS77QQ5N-CsgpxEEd.js} +1 -1
  80. package/dist/worker/console/static/assets/{index-DBh_x_Ek.js → index-Bai8Z25q.js} +81 -81
  81. package/dist/worker/console/static/assets/index-CH0Tr5hY.css +1 -0
  82. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BrSiNxyd.js → infoDiagram-6WML65LV-kHGxkJUS.js} +1 -1
  83. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-gvlvY-n9.js → ishikawaDiagram-WSZJBQD7-CbLe9wrr.js} +1 -1
  84. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-D-YaPzRQ.js → journeyDiagram-NVQOT4AX-BOkwxAcP.js} +1 -1
  85. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BsmDv9E_.js → kanban-definition-27J2QSJJ-BHYQ2rDu.js} +1 -1
  86. package/dist/worker/console/static/assets/{linear-UWaoDN3h.js → linear-CHT4ujob.js} +1 -1
  87. package/dist/worker/console/static/assets/{mermaid.core-BlvpGVG8.js → mermaid.core-yL8sfqnY.js} +5 -5
  88. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CUCIP5CP.js → mindmap-definition-FAOFIHXS-DZx1coV-.js} +1 -1
  89. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CB1aQUAK.js → pegDiagram-VL7TDLO6-DStWRCI8.js} +1 -1
  90. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BwehHxhf.js → pieDiagram-7S7Q4E2Y-DSKZkwzK.js} +1 -1
  91. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CFz30APB.js → quadrantDiagram-CIZ2JOQS-CmoEGOuW.js} +1 -1
  92. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwNmw1i6.js → railroadDiagram-AXF67PYL-BLqzhKyq.js} +1 -1
  93. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-jg-tPXlI.js → requirementDiagram-LRYGKXZP-gbHeUabM.js} +1 -1
  94. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BlyReDKl.js → sankeyDiagram-W5VNT64P-CaKkBCKO.js} +1 -1
  95. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DBUzW1f3.js → sequenceDiagram-SI44F4Z6-PUbi3Upu.js} +1 -1
  96. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BYcI2RAr.js → sizeCapture-X5ZJPWSS-dm7S9Qfe.js} +1 -1
  97. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-D7AiyJdo.js → stateDiagram-OKZ733FA-C7WSzv4p.js} +1 -1
  98. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-sQDv5AP3.js +1 -0
  99. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BHGt7Kkv.js → swimlanes-SLNWSIFB-CBKCiQwd.js} +2 -2
  100. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BXwsmcPs.js +8 -0
  101. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-OPjooWcX.js → timeline-definition-Z64GVDOM-CvnUlylk.js} +1 -1
  102. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-ggsWDvrO.js → vennDiagram-T6HMQDX7-xM0StY1O.js} +1 -1
  103. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DjNe-BjZ.js → wardleyDiagram-T6FBY63Y-CWtJMagy.js} +1 -1
  104. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-Byp63Hei.js → xychartDiagram-ELKLHX3M-BO_-MeK8.js} +1 -1
  105. package/dist/worker/console/static/index.html +2 -2
  106. package/dist/worker/console/static-src/app/console-types.js +40 -1
  107. package/dist/worker/console/static-src/app/useOperatorActions.js +5 -0
  108. package/dist/worker/console/static-src/app/useTaskWizard.js +73 -1
  109. package/dist/worker/observability/init-runtime-activity.js +26 -0
  110. package/dist/worker/observe/node-input.js +88 -2
  111. package/dist/worker/observe/routes.js +188 -0
  112. package/dist/worker/observe/static/api.js +3 -3
  113. package/dist/worker/observe/static/format.js +37 -0
  114. package/dist/worker/observe/static/index.html +5 -0
  115. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  116. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  117. package/dist/worker/observe/static/state.js +2 -2
  118. package/dist/worker/observe/static/styles.css +331 -35
  119. package/dist/worker/observe/static/views/dag-inspector.js +509 -78
  120. package/dist/worker/observe/static/views/task.js +55 -0
  121. package/dist/workflows/dag/frontend-design-policy.js +19 -9
  122. package/dist/workflows/dag/frontend-implementation-contract.js +117 -50
  123. package/dist/workflows/dag/frontend-plan-render.js +27 -0
  124. package/dist/workflows/dag/frontend-prewrite-gate.js +45 -1
  125. package/dist/workflows/dag/frontend-recovery-controller.js +1 -0
  126. package/dist/workflows/dag/frontend-review-context.js +102 -15
  127. package/dist/workflows/dag/frontend-review-findings.js +42 -10
  128. package/dist/workflows/dag/frontend-shadow-dual-write.js +36 -2
  129. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  130. package/dist/workflows/dag/frontend-worktree-diff.js +154 -0
  131. package/dist/workflows/dag/frontend-writer-admission.js +12 -0
  132. package/dist/workflows/dag/init-hybrid.js +297 -65
  133. package/dist/workflows/dag/interrupt-request.js +7 -0
  134. package/dist/workflows/dag/node-execution.js +111 -1
  135. package/dist/workflows/dag/prompt.js +34 -1
  136. package/dist/workflows/dag/recovery-lease.js +80 -0
  137. package/dist/workflows/dag/rerun-feedback.js +31 -8
  138. package/dist/workflows/dag/rerun-run.js +23 -0
  139. package/dist/workflows/dag/rerun-task.js +29 -0
  140. package/dist/workflows/dag/retry-policy.js +34 -9
  141. package/dist/workflows/dag/runner.js +83 -16
  142. package/dist/workflows/dag/types.js +67 -11
  143. package/docs/README.md +1 -0
  144. package/docs/architecture/evolution.md +45 -0
  145. package/docs/init-surface.manifest.json +4 -0
  146. package/docs/templates/README.md +2 -1
  147. package/docs/templates/frontend-implementation-contract.schema.json +684 -24
  148. package/docs/templates/spec-registry.schema.json +45 -0
  149. package/harness.json +3 -3
  150. package/package.json +1 -1
  151. package/skills/frontend-bounded-implement/SKILL.md +8 -14
  152. package/skills/frontend-design-review/SKILL.md +20 -41
  153. package/skills/frontend-implementation/SKILL.md +2 -3
  154. package/skills/frontend-implementation/references/design-spec.md +4 -3
  155. package/skills/frontend-implementation/references/node-contracts.md +4 -3
  156. package/skills/frontend-review/SKILL.md +22 -14
  157. package/skills/frontend-review/references/review-findings.md +6 -7
  158. package/skills/loop-agent/references/hybrid-dag.md +7 -0
  159. package/dist/worker/console/static/assets/channel-AbFtVnFm.js +0 -1
  160. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-n1yf2Jcv.js +0 -1
  161. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-n1yf2Jcv.js +0 -1
  162. package/dist/worker/console/static/assets/index-DwOGauxU.css +0 -1
  163. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BBSH5c8m.js +0 -1
  164. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D2F7-U4H.js +0 -8
@@ -410,6 +410,13 @@ export function startInterruptWatcher(input) {
410
410
  let watcher;
411
411
  try {
412
412
  watcher = watch(path.join(input.runDir, ".runtime"), () => tick());
413
+ // fs.watch can report EMFILE/ENOSPC asynchronously after watch() returns.
414
+ // Polling below is the reliable path, so watcher errors must never escape
415
+ // as an unhandled EventEmitter error and orphan the DAG runner.
416
+ watcher.on("error", () => {
417
+ watcher?.close();
418
+ watcher = undefined;
419
+ });
413
420
  watcher.unref?.();
414
421
  }
415
422
  catch {
@@ -3,6 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { pathMatchesPattern } from "../../shared/git-progress.js";
6
+ import { redactSecrets, truncateUtf8Preview } from "../../shared/preview.js";
6
7
  import { recordDecisionEnvelopeForNode, shouldPauseOnHumanEscalation, writeHumanEscalationArtifacts, } from "./decision-envelope.js";
7
8
  import { FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT, FRONTEND_WRITER_NODE_IDS, isFrontendWriterAuthorized, readFrontendPrewriteResult, } from "./scheduler.js";
8
9
  import { writeNodeRecord, writeNodeSkillArtifacts } from "./run-store.js";
@@ -164,6 +165,45 @@ function frontendStructuredArtifactRetryGuidance(schemaId) {
164
165
  ];
165
166
  }
166
167
  const FRONTEND_PLAN_NODE_ID = "frontend-plan-pi";
168
+ const COMPACT_RETRY_SECTION_MAX_CHARS = 1_200;
169
+ const COMPACT_RETRY_TASK_MAX_CHARS = 4_800;
170
+ function compactRetryText(text, maxChars) {
171
+ if (text.length <= maxChars)
172
+ return text;
173
+ const headChars = Math.floor(maxChars * 0.78);
174
+ const tailChars = Math.max(0, maxChars - headChars - 52);
175
+ return `${text.slice(0, headChars)}\n… [omitted after context-overflow] …\n${text.slice(-tailChars)}`;
176
+ }
177
+ function compactRetrySection(prompt, name, maxChars) {
178
+ const match = prompt.match(new RegExp(`<${name}>([\\s\\S]*?)</${name}>`));
179
+ return compactRetryText(match?.[1]?.trim() ?? "(not available)", maxChars);
180
+ }
181
+ /**
182
+ * A context-overflow retry must not resend the full resolved skills, governance
183
+ * payload, repeated source excerpts, and upstream previews that caused the
184
+ * previous session to overflow. Keep the immutable node contract plus bounded
185
+ * objective/criteria/task excerpts; canonical artifacts remain readable by
186
+ * pointer from the retained task instructions.
187
+ */
188
+ export function buildContextOverflowRetryPrompt(task, basePrompt) {
189
+ return [
190
+ "<compact_retry_context>",
191
+ "This is a fresh, compact retry after provider context-overflow. The original full prompt remains audit evidence but is intentionally not resent.",
192
+ "Do not re-read task sources, upstream stdout, skills, or full diff artifacts that are already represented by canonical artifacts. Start from the smallest retained artifact and expand only the exact file or diff fragment needed.",
193
+ "</compact_retry_context>",
194
+ `<dag_objective>\n${compactRetrySection(basePrompt, "dag_objective", COMPACT_RETRY_SECTION_MAX_CHARS)}\n</dag_objective>`,
195
+ `<success_criteria>\n${compactRetrySection(basePrompt, "success_criteria", COMPACT_RETRY_SECTION_MAX_CHARS)}\n</success_criteria>`,
196
+ `<global_constraints>\n${compactRetrySection(basePrompt, "global_constraints", COMPACT_RETRY_SECTION_MAX_CHARS)}\n</global_constraints>`,
197
+ `<node_contract>\n${compactRetrySection(basePrompt, "node_contract", COMPACT_RETRY_SECTION_MAX_CHARS)}\n</node_contract>`,
198
+ `<upstream_context>\n${compactRetrySection(basePrompt, "upstream_context", COMPACT_RETRY_SECTION_MAX_CHARS)}\n</upstream_context>`,
199
+ [
200
+ "<task>",
201
+ `Original task id: ${task.id}`,
202
+ compactRetrySection(basePrompt, "task", COMPACT_RETRY_TASK_MAX_CHARS),
203
+ "</task>",
204
+ ].join("\n"),
205
+ ].join("\n\n");
206
+ }
167
207
  function isFrontendPlanLadderTask(task) {
168
208
  return task.id === FRONTEND_PLAN_NODE_ID;
169
209
  }
@@ -184,9 +224,25 @@ async function readFrontendPlanCommittedSnapshot(runDir, nodeId) {
184
224
  const hasTerminalFact = committed.some((record) => record.fact.kind === "finalize_plan");
185
225
  return { digest, hasTerminalFact };
186
226
  }
187
- function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics, frontendPlanRetryStep) {
227
+ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics, frontendPlanRetryStep) {
188
228
  if (attemptNumber <= 1)
189
229
  return basePrompt;
230
+ // Context overflow is a transport/session failure, not a plan-protocol
231
+ // failure. It must take precedence over the plan retry ladder so every
232
+ // retry, including a repeated plan overflow, starts from the compact
233
+ // envelope rather than re-sending the original prompt.
234
+ if (previousFailureCategory === "context-overflow") {
235
+ return [
236
+ buildContextOverflowRetryPrompt(task, basePrompt),
237
+ "",
238
+ "<retry_instruction>",
239
+ "The previous attempt exceeded the provider context window. This retry starts a fresh session: keep the evidence surface narrow.",
240
+ "Do not re-read task sources, upstream stdout, skills, or full diff artifacts that are already represented by the canonical contract/review context. Read the smallest relevant artifact first, then only the specific source file or diff fragment needed to decide a finding.",
241
+ "Do not read artifacts/diff_patch.patch in full. For frontend review, read the bounded diff summary and then at most the specific per-file diff fragments it identifies, in part order.",
242
+ "Use concise tool calls and finish the required terminal/output protocol as soon as the evidence is sufficient.",
243
+ "</retry_instruction>",
244
+ ].join("\n");
245
+ }
190
246
  if (frontendPlanRetryStep === "compact-terminal-first") {
191
247
  return [
192
248
  basePrompt,
@@ -239,6 +295,16 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
239
295
  ].join("\n");
240
296
  }
241
297
  if (previousFailureCategory === "read-burst") {
298
+ if (task.id !== FRONTEND_PLAN_NODE_ID) {
299
+ return [
300
+ basePrompt,
301
+ "",
302
+ "<retry_instruction>",
303
+ "The previous attempt exceeded its read budget. Start a fresh, evidence-minimal pass: do not re-read task sources, upstream stdout, skills, or full diff artifacts already represented by a canonical context artifact.",
304
+ "Read the smallest relevant summary first, then only the specific source file or diff fragment needed for the required decision. Finish the required terminal/output protocol as soon as evidence is sufficient.",
305
+ "</retry_instruction>",
306
+ ].join("\n");
307
+ }
242
308
  return [
243
309
  basePrompt,
244
310
  "",
@@ -430,6 +496,35 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
430
496
  "</retry_instruction>",
431
497
  ].join("\n");
432
498
  }
499
+ const PROMPT_RESTART_CANDIDATE_MAX_BYTES = 64 * 1024;
500
+ function promptRestartCandidateForAttempt(input) {
501
+ if (input.attemptPrompt === input.baseRuntimePrompt) {
502
+ return input.taskPrompt;
503
+ }
504
+ const appendedPrefix = `${input.baseRuntimePrompt}\n\n`;
505
+ if (input.attemptPrompt.startsWith(appendedPrefix)) {
506
+ return [
507
+ input.taskPrompt,
508
+ input.attemptPrompt.slice(appendedPrefix.length),
509
+ ]
510
+ .filter(Boolean)
511
+ .join("\n\n");
512
+ }
513
+ return input.attemptPrompt;
514
+ }
515
+ async function persistFailedPromptRestartCandidate(input) {
516
+ const redacted = redactSecrets(input.prompt);
517
+ const preview = truncateUtf8Preview(redacted, PROMPT_RESTART_CANDIDATE_MAX_BYTES);
518
+ await writeDagNodeJsonArtifact(input.runDir, input.nodeId, `prompt-candidate-attempt-${input.attempt}.json`, {
519
+ schemaVersion: 1,
520
+ attempt: input.attempt,
521
+ failureCategory: input.failureCategory ?? "unknown",
522
+ prompt: preview,
523
+ truncated: preview !== redacted,
524
+ maxBytes: PROMPT_RESTART_CANDIDATE_MAX_BYTES,
525
+ sha256: createHash("sha256").update(redacted).digest("hex"),
526
+ });
527
+ }
433
528
  export async function buildNodePromptWithResolvedSkillInstructions(spec, task, upstream, cwd, options) {
434
529
  const policy = resolveContextPolicy(spec);
435
530
  const skillNames = policy.resolveSkills(spec, task);
@@ -904,6 +999,7 @@ export async function executeDagNode(input) {
904
999
  // exactly this attempt's segment (B1: earlier attempts legitimately use
905
1000
  // ls/find and must not be attributed to the repair attempt).
906
1001
  const attemptSessionEventsOffset = await sessionEventsByteLength(runDir, nodeId);
1002
+ let promptRestartCandidate = task.subtask_prompt;
907
1003
  let result;
908
1004
  try {
909
1005
  validateRepairArtifactGateBeforeShell({
@@ -974,6 +1070,11 @@ export async function executeDagNode(input) {
974
1070
  previousReason: previousProtocolReason,
975
1071
  });
976
1072
  }
1073
+ promptRestartCandidate = promptRestartCandidateForAttempt({
1074
+ taskPrompt: task.subtask_prompt,
1075
+ baseRuntimePrompt: prompt,
1076
+ attemptPrompt,
1077
+ });
977
1078
  result = await executeNode({
978
1079
  task,
979
1080
  cwd,
@@ -1101,6 +1202,15 @@ export async function executeDagNode(input) {
1101
1202
  const attemptWallDurationMs = durationBetween(attemptStartedAt, attemptFinishedAt);
1102
1203
  totalAttemptWallDurationMs += attemptWallDurationMs;
1103
1204
  const rawFailureCategory = result.rawFailureCategory ?? result.failureCategory;
1205
+ if (task.executor === "pi" && !result.ok) {
1206
+ await persistFailedPromptRestartCandidate({
1207
+ runDir,
1208
+ nodeId,
1209
+ attempt: attemptNumber,
1210
+ failureCategory: result.failureCategory,
1211
+ prompt: promptRestartCandidate,
1212
+ });
1213
+ }
1104
1214
  const attemptRecord = {
1105
1215
  attempt: attemptNumber,
1106
1216
  startedAt: attemptStartedAt,
@@ -45,9 +45,42 @@ function formatResolvedSkillInstructions(instructions) {
45
45
  if (!instructions || instructions.length === 0)
46
46
  return "(none)";
47
47
  return instructions
48
- .map((instruction) => instruction.promptText)
48
+ .map((instruction) => formatSkillChecklist(instruction))
49
49
  .join("\n\n---\n\n");
50
50
  }
51
+ /** Keep skill provenance, but turn bounded inline manuals into a small action list. */
52
+ function formatSkillChecklist(instruction) {
53
+ const metadata = [
54
+ `Skill: ${instruction.name}`,
55
+ `Resolution: ${instruction.resolution}`,
56
+ ...(instruction.path ? [`Path: ${instruction.path}`] : []),
57
+ ...(instruction.sha256 ? [`Sha256: ${instruction.sha256}`] : []),
58
+ "Injection mode: checklist",
59
+ ];
60
+ if (instruction.resolution !== "resolved")
61
+ return [...metadata, instruction.promptText].join("\n");
62
+ const source = instruction.promptText.split("Instructions:\n")[1] ?? "";
63
+ const candidates = source
64
+ .split(/\r?\n/)
65
+ .map((line) => line.trim())
66
+ .filter((line) => /^(?:#{1,3}\s+|[-*]\s+|\d+[.)]\s+)/.test(line) || line.length > 0)
67
+ .map((line) => line.replace(/^#{1,3}\s+/, "").replace(/^[-*]\s+/, "").replace(/^\d+[.)]\s+/, ""))
68
+ .filter((line) => line.length > 0);
69
+ const checklist = [];
70
+ let chars = 0;
71
+ for (const item of candidates) {
72
+ const bounded = item.slice(0, 240);
73
+ if (checklist.length >= 10 || chars + bounded.length > 1_600)
74
+ break;
75
+ checklist.push(`- ${bounded}`);
76
+ chars += bounded.length;
77
+ }
78
+ return [
79
+ ...metadata,
80
+ "Checklist (apply these actions; consult the immutable resolved file only when a listed action requires detail):",
81
+ ...(checklist.length > 0 ? checklist : ["- Follow the resolved skill's declared workflow."]),
82
+ ].join("\n");
83
+ }
51
84
  export function formatOutputLanguageBlock(language = DEFAULT_DAG_OUTPUT_LANGUAGE) {
52
85
  if (language === "en") {
53
86
  return [
@@ -0,0 +1,80 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
5
+ import { DAG_RUNS_DIR } from "./lifecycle.js";
6
+ function recoveryLeasePath(cwd, parentRunId) {
7
+ // Never derive a filesystem name directly from an externally supplied legacy
8
+ // run id. The content remains auditable while the filename is traversal-safe.
9
+ const key = createHash("sha256").update(parentRunId).digest("hex");
10
+ return path.join(cwd, DAG_RUNS_DIR, ".recovery-leases", `${key}.json`);
11
+ }
12
+ function parseLease(raw, parentRunId) {
13
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
14
+ throw new Error("invalid dag recovery lease: expected object");
15
+ }
16
+ const record = raw;
17
+ if (record.schemaVersion !== 1 ||
18
+ record.parentRunId !== parentRunId ||
19
+ typeof record.requestId !== "string" ||
20
+ record.requestId.length === 0 ||
21
+ (record.kind !== "auto-frontend-recovery" &&
22
+ record.kind !== "rerun-from-node" &&
23
+ record.kind !== "standalone-task-rerun") ||
24
+ typeof record.leaseToken !== "string" ||
25
+ record.leaseToken.length === 0 ||
26
+ typeof record.acquiredAt !== "string" ||
27
+ typeof record.executionRef !== "undefined" &&
28
+ typeof record.executionRef !== "string") {
29
+ throw new Error("invalid dag recovery lease: schema mismatch");
30
+ }
31
+ return record;
32
+ }
33
+ export async function acquireDagRecoveryLease(input) {
34
+ const leasePath = recoveryLeasePath(input.cwd, input.parentRunId);
35
+ const lease = {
36
+ schemaVersion: 1,
37
+ parentRunId: input.parentRunId,
38
+ requestId: input.requestId,
39
+ kind: input.kind,
40
+ leaseToken: randomBytes(16).toString("hex"),
41
+ acquiredAt: (input.now ?? (() => new Date()))().toISOString(),
42
+ };
43
+ await mkdir(path.dirname(leasePath), { recursive: true });
44
+ try {
45
+ // `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.
48
+ await writeFile(leasePath, `${JSON.stringify(lease, null, 2)}\n`, {
49
+ encoding: "utf8",
50
+ flag: "wx",
51
+ });
52
+ return { kind: "acquired", leasePath, lease };
53
+ }
54
+ catch (error) {
55
+ if (!(error && typeof error === "object" && "code" in error && error.code === "EEXIST")) {
56
+ throw error;
57
+ }
58
+ const existing = parseLease(JSON.parse(await readFile(leasePath, "utf8")), input.parentRunId);
59
+ return { kind: "held", leasePath, lease: existing };
60
+ }
61
+ }
62
+ /** Persist the operation/run identity before the caller is allowed to execute it. */
63
+ export async function bindDagRecoveryLeaseExecution(input) {
64
+ if (!input.executionRef.trim()) {
65
+ throw new Error("dag recovery lease executionRef is required");
66
+ }
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;
80
+ }
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { parseJsonReviewVerdict } from "./output-protocol.js";
5
- import { FRONTEND_REVIEW_NODE_ID, readCommittedReviewRequestFact, } from "./frontend-review-findings.js";
5
+ import { FRONTEND_DESIGN_REVIEW_NODE_ID, FRONTEND_REVIEW_NODE_ID, readCommittedDesignRequestFact, readCommittedReviewRequestFact, } from "./frontend-review-findings.js";
6
6
  export const MAX_RERUN_FEEDBACK_CHARS = 12_000;
7
7
  const RERUN_FEEDBACK_TRUNCATION_MARKER = "\n\n...[上一轮反馈已按长度上限截断]...\n\n";
8
8
  const MAX_EVIDENCE_REFS = 8;
@@ -183,6 +183,24 @@ async function readTypedReviewRequestFact(runDir) {
183
183
  },
184
184
  };
185
185
  }
186
+ /**
187
+ * Read the committed frontend design request fact (typed terminal channel).
188
+ * Returns { nodeId, fact } when the design review node committed
189
+ * `request_design_changes` — the design analog of request_review_changes.
190
+ */
191
+ async function readTypedDesignRequestFact(runDir) {
192
+ const fact = await readCommittedDesignRequestFact({ runDir });
193
+ if (!fact)
194
+ return undefined;
195
+ return {
196
+ nodeId: FRONTEND_DESIGN_REVIEW_NODE_ID,
197
+ fact: {
198
+ issueCategory: fact.issueCategory,
199
+ evidenceRefs: fact.evidenceRefs,
200
+ findings: fact.findings,
201
+ },
202
+ };
203
+ }
186
204
  /**
187
205
  * Derive only the latest unresolved verify/review feedback from canonical
188
206
  * parent-run facts. Provider-only failures intentionally return undefined.
@@ -201,12 +219,15 @@ export async function deriveDagRerunFeedback(input) {
201
219
  }
202
220
  }
203
221
  if (!sourceNodeId || !sourceNode) {
204
- // Typed terminal channel: the frontend review commits its verdict as a
205
- // typed fact (request_review_changes) instead of VERDICT text, and the
206
- // frontend DAG has no convergence passHistory. Fall back to reading the
207
- // review node's typed event store so findings still flow into the next
208
- // round as unresolved feedback.
209
- const typedFact = await readTypedReviewRequestFact(input.runDir);
222
+ // Typed terminal channel: the frontend review/design review commit their
223
+ // verdicts as typed facts (request_review_changes / request_design_changes)
224
+ // instead of VERDICT text, and the frontend DAG has no convergence
225
+ // passHistory. Fall back to reading the typed event stores so findings
226
+ // still flow into the next round as unresolved feedback. Prefer review;
227
+ // if absent, fall through to the design review (a design rejection also
228
+ // carries actionable findings the next plan round must consume).
229
+ const typedFact = (await readTypedReviewRequestFact(input.runDir)) ??
230
+ (await readTypedDesignRequestFact(input.runDir));
210
231
  if (typedFact) {
211
232
  const parentSourceBindingHash = sourceBindingHash(input.spec);
212
233
  return withDigest({
@@ -216,7 +237,9 @@ export async function deriveDagRerunFeedback(input) {
216
237
  taskId: input.taskId,
217
238
  sourceNodeId: typedFact.nodeId,
218
239
  verdict: "request-revision",
219
- failureCategory: "review-request-changes",
240
+ failureCategory: typedFact.nodeId === FRONTEND_REVIEW_NODE_ID
241
+ ? "review-request-changes"
242
+ : "design-request-changes",
220
243
  feedbackText: scrubAndBoundFeedbackText(formatTypedReviewRequestFeedback(typedFact.fact), input.runDir, input.state.cwd),
221
244
  evidenceRefs: [
222
245
  {
@@ -12,6 +12,7 @@ import { prepareActiveRunDir, writeNodeRecord, writeRunSpec, writeRunState, } fr
12
12
  import { cloneParentSkillSnapshotForContinuation, readSkillSnapshot, } from "./skill-snapshot.js";
13
13
  import { topoSortToRanks } from "./topo.js";
14
14
  import { captureWorkspaceCheckpoint, readWorkspaceCheckpoint, WORKSPACE_CHECKPOINT_START_REL, WORKSPACE_CHECKPOINT_TERMINAL_REL, writeWorkspaceCheckpoint, } from "./workspace-checkpoint.js";
15
+ import { acquireDagRecoveryLease, bindDagRecoveryLeaseExecution, } from "./recovery-lease.js";
15
16
  export const RERUN_PLAN_REL_PATH = ".runtime/rerun-plan.json";
16
17
  export const IMPORT_MANIFEST_REL_PATH = ".runtime/import-manifest.json";
17
18
  export const WORKER_ASSOCIATION_REL_PATH = ".runtime/worker-association.json";
@@ -572,6 +573,20 @@ export async function executeDagRerun(input) {
572
573
  }
573
574
  const parentSpec = await readDagRunSpec(located.runDir);
574
575
  const parentState = await readDagRunState(located.runDir);
576
+ const recoveryLease = await acquireDagRecoveryLease({
577
+ cwd: input.cwd,
578
+ parentRunId: input.parentRunId,
579
+ requestId: input.requestId,
580
+ kind: "rerun-from-node",
581
+ });
582
+ if (recoveryLease.kind === "held") {
583
+ return {
584
+ ok: false,
585
+ plan,
586
+ errorCode: "RECOVERY_LEASE_HELD",
587
+ message: `parent run recovery is already claimed by ${recoveryLease.lease.kind} request ${recoveryLease.lease.requestId}`,
588
+ };
589
+ }
575
590
  const staged = await stageContinuationRun({
576
591
  cwd: input.cwd,
577
592
  parentRunDir: located.runDir,
@@ -583,6 +598,14 @@ export async function executeDagRerun(input) {
583
598
  requestId: input.requestId,
584
599
  promptOverrides: input.promptOverrides,
585
600
  });
601
+ // Stage is non-executing; bind the child identity immediately before handing
602
+ // it to the runner so no recovery DAG can start without durable parent
603
+ // lineage ownership.
604
+ await bindDagRecoveryLeaseExecution({
605
+ leasePath: recoveryLease.leasePath,
606
+ lease: recoveryLease.lease,
607
+ executionRef: staged.state.runId,
608
+ });
586
609
  const continuationOptions = {
587
610
  cwd: input.cwd,
588
611
  spec: staged.spec,
@@ -4,6 +4,7 @@ import { generateTaskDagUseCase } from "../../application/dag/generate-task-dag.
4
4
  import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
5
5
  import { DAG_RUNS_DIR, isTerminalDagRunStatus, locateDagRun, readDagRunSpec, readDagRunState, } from "./lifecycle.js";
6
6
  import { deriveDagRerunFeedback } from "./rerun-feedback.js";
7
+ import { acquireDagRecoveryLease, bindDagRecoveryLeaseExecution, } from "./recovery-lease.js";
7
8
  import { parseDagSpec, } from "./types.js";
8
9
  export const RERUN_TASK_LINEAGE_REL_PATH = ".runtime/rerun-task-lineage.json";
9
10
  export function parseDagRerunTaskArgs(args) {
@@ -369,6 +370,34 @@ export async function rerunStandaloneTask(input) {
369
370
  }
370
371
  }
371
372
  }
373
+ const recoveryLease = execute
374
+ ? await acquireDagRecoveryLease({
375
+ cwd: input.cwd,
376
+ parentRunId: input.parentRunId,
377
+ requestId: input.requestId,
378
+ kind: "standalone-task-rerun",
379
+ })
380
+ : undefined;
381
+ if (recoveryLease?.kind === "held") {
382
+ return blockedResult({
383
+ parentRunId: input.parentRunId,
384
+ reason: input.reason,
385
+ requestId: input.requestId,
386
+ blockedReason: "recovery-lease-held",
387
+ note: `parent run recovery is already claimed by ${recoveryLease.lease.kind} request ${recoveryLease.lease.requestId}`,
388
+ taskId: taskResolution.taskId,
389
+ });
390
+ }
391
+ if (recoveryLease?.kind === "acquired") {
392
+ // Persist a non-empty execution identity before generateTaskDagUseCase can
393
+ // execute its new DAG. The generated run id is not available yet, so the
394
+ // request id is the stable lineage handle for this entry point.
395
+ await bindDagRecoveryLeaseExecution({
396
+ leasePath: recoveryLease.leasePath,
397
+ lease: recoveryLease.lease,
398
+ executionRef: `standalone-task-rerun:${input.requestId}`,
399
+ });
400
+ }
372
401
  const generateResult = await generateTaskDagUseCase({
373
402
  repoRoot: input.cwd,
374
403
  taskId: taskResolution.taskId,
@@ -132,13 +132,24 @@ export const dagRetryPolicySchema = z
132
132
  /**
133
133
  * The default retry policy applied to safe generated read-only Pi nodes.
134
134
  * Total attempts: 3, exponential backoff with cap.
135
+ *
136
+ * Includes `context-overflow`: a read-only node that blew the context window
137
+ * (400 request too large, e.g. a review node accumulating too many reads) is a
138
+ * transient, safe-to-retry failure — the retry re-runs the node with a
139
+ * compact-context instruction instead of failing the whole run. Previously
140
+ * only writer transport policies had context-overflow; contract/scout/
141
+ * design-review/review fell straight to ERROR without a retry attempt.
135
142
  */
136
143
  export const DEFAULT_READ_ONLY_PI_RETRY_POLICY = {
137
144
  maxAttempts: 3,
138
145
  backoff: "exponential",
139
146
  initialDelayMs: 2000,
140
147
  maxDelayMs: 30000,
141
- retryCategories: [...DEFAULT_DAG_RETRY_CATEGORIES],
148
+ retryCategories: [
149
+ ...DEFAULT_DAG_RETRY_CATEGORIES,
150
+ READ_BURST_RETRY_CATEGORY,
151
+ CONTEXT_OVERFLOW_RETRY_CATEGORY,
152
+ ],
142
153
  };
143
154
  /**
144
155
  * Structured planner nodes may retry with a compact-output instruction
@@ -148,7 +159,11 @@ export const DEFAULT_READ_ONLY_PI_RETRY_POLICY = {
148
159
  */
149
160
  export const STRUCTURED_REQUIRED_PI_RETRY_POLICY = {
150
161
  ...DEFAULT_READ_ONLY_PI_RETRY_POLICY,
151
- retryCategories: [...STRUCTURED_REQUIRED_DAG_RETRY_CATEGORIES],
162
+ retryCategories: [
163
+ ...STRUCTURED_REQUIRED_DAG_RETRY_CATEGORIES,
164
+ READ_BURST_RETRY_CATEGORY,
165
+ CONTEXT_OVERFLOW_RETRY_CATEGORY,
166
+ ],
152
167
  };
153
168
  /**
154
169
  * Planner read-only nodes (standard DAG contract-pi / plan-pi) may retry with
@@ -159,7 +174,12 @@ export const STRUCTURED_REQUIRED_PI_RETRY_POLICY = {
159
174
  */
160
175
  export const PLANNER_OUTPUT_LIMIT_RETRY_POLICY = {
161
176
  ...DEFAULT_READ_ONLY_PI_RETRY_POLICY,
162
- retryCategories: [...DEFAULT_DAG_RETRY_CATEGORIES, STRUCTURED_OUTPUT_RETRY_CATEGORY],
177
+ retryCategories: [
178
+ ...DEFAULT_DAG_RETRY_CATEGORIES,
179
+ STRUCTURED_OUTPUT_RETRY_CATEGORY,
180
+ READ_BURST_RETRY_CATEGORY,
181
+ CONTEXT_OVERFLOW_RETRY_CATEGORY,
182
+ ],
163
183
  };
164
184
  /**
165
185
  * Default retry for reviewer / recovery nodes that declare outputProtocol.
@@ -180,6 +200,8 @@ export const FRONTEND_REVIEW_TERMINAL_RETRY_POLICY = {
180
200
  ...DEFAULT_READ_ONLY_PI_RETRY_POLICY,
181
201
  retryCategories: [
182
202
  ...DEFAULT_DAG_RETRY_CATEGORIES,
203
+ READ_BURST_RETRY_CATEGORY,
204
+ CONTEXT_OVERFLOW_RETRY_CATEGORY,
183
205
  REVIEW_TERMINAL_MISSING_RETRY_CATEGORY,
184
206
  ],
185
207
  };
@@ -536,14 +558,16 @@ function reasonMandatedFrontendPlanRetryStep(reason) {
536
558
  }
537
559
  }
538
560
  /**
539
- * Opt-in retry policy mounted on the frontend-plan-pi node. maxAttempts=5 is
540
- * the schema cap, matching the four-rung ladder plus one non-converging stop.
541
- * `invalid-output` (typed-fact schema violations, e.g. verification targets
542
- * referencing undeclared UI states) retries at the `normal` rung with the
543
- * contractCheck reason injected via `<retry_instruction>` (§5.1 ladder).
561
+ * Opt-in retry policy mounted on the frontend-plan-pi node. maxAttempts=3
562
+ * bounds plan retry cost: the four-rung ladder is only partially traversable
563
+ * before the run fails non-converging (rungs: normal bounded-tool-only
564
+ * compact-terminal-first backup-model). `invalid-output` (typed-fact schema
565
+ * violations, e.g. verification targets referencing undeclared UI states)
566
+ * retries at the `normal` rung with the contractCheck reason injected via
567
+ * `<retry_instruction>` (§5.1 ladder).
544
568
  */
545
569
  export const FRONTEND_PLAN_LADDER_RETRY_POLICY = {
546
- maxAttempts: 5,
570
+ maxAttempts: 3,
547
571
  backoff: "exponential",
548
572
  initialDelayMs: 2000,
549
573
  maxDelayMs: 30000,
@@ -551,5 +575,6 @@ export const FRONTEND_PLAN_LADDER_RETRY_POLICY = {
551
575
  ...DEFAULT_DAG_RETRY_CATEGORIES,
552
576
  STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY,
553
577
  READ_BURST_RETRY_CATEGORY,
578
+ CONTEXT_OVERFLOW_RETRY_CATEGORY,
554
579
  ],
555
580
  };