@tea-agent/loop-agent 0.38.2 → 0.38.4

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 (192) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +171 -96
  3. package/README.md +11 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/advance.js +4 -0
  6. package/dist/build-stamp.json +3 -3
  7. package/dist/cli/command-definitions.js +16 -5
  8. package/dist/cli/program.js +16 -2
  9. package/dist/cli/update/notifier.js +2 -2
  10. package/dist/cli.js +2 -1
  11. package/dist/commands/dag-rerun.js +55 -1
  12. package/dist/commands/init-model-catalog.js +464 -0
  13. package/dist/commands/init-upgrade.js +265 -97
  14. package/dist/commands/init.js +428 -27
  15. package/dist/commands/inspect-next.js +8 -0
  16. package/dist/commands/task-advance.js +31 -0
  17. package/dist/executors/dag-pi-executor.js +76 -10
  18. package/dist/executors/pi-event-serializer.js +42 -1
  19. package/dist/executors/pi-executor.js +51 -0
  20. package/dist/executors/pi-extension-resolver.js +233 -0
  21. package/dist/executors/pi-playwright-cli-tool.js +51 -12
  22. package/dist/executors/pi-sdk-executor.js +187 -59
  23. package/dist/executors/shell-executor.js +265 -15
  24. package/dist/executors/shell-write-guard.js +7 -0
  25. package/dist/shared/dag-prompt-override.js +28 -0
  26. package/dist/shared/operator/capabilities.js +32 -0
  27. package/dist/shared/playwright-cli-command-policy.js +15 -0
  28. package/dist/shared/update/console-notifier.js +100 -0
  29. package/dist/{cli → shared}/update/npm-client.js +41 -8
  30. package/dist/{cli → shared}/update/state.js +41 -13
  31. package/dist/task/contract/constants.js +1 -0
  32. package/dist/task/contract/diff.js +26 -0
  33. package/dist/task/contract/project.js +5 -0
  34. package/dist/task/contract/schema.js +6 -1
  35. package/dist/task/source-prepare/build-draft.js +15 -0
  36. package/dist/task/source-prepare/parse-intent.js +24 -4
  37. package/dist/worker/cli.js +31 -1
  38. package/dist/worker/console/chat/routes.js +1 -0
  39. package/dist/worker/console/dag-execution-receipt.js +33 -0
  40. package/dist/worker/console/operator-actions.js +116 -0
  41. package/dist/worker/console/operator-user-error.js +2 -2
  42. package/dist/worker/console/pi-readiness.js +4 -4
  43. package/dist/worker/console/routes.js +43 -2
  44. package/dist/worker/console/security.js +63 -9
  45. package/dist/worker/console/server.js +36 -5
  46. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4bmo3iY.js → abnfDiagram-N423BO3Z-gauhD9kD.js} +1 -1
  47. package/dist/worker/console/static/assets/{arc-SAD7McOS.js → arc-pm3c2Rsl.js} +1 -1
  48. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DAVLUdK1.js → architectureDiagram-T3A2C74G-D_JAg3wH.js} +1 -1
  49. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CPPdv4vj.js → blockDiagram-VBNYF7ZC-Dk7-ex52.js} +1 -1
  50. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-D1GRGZax.js → c4Diagram-5PPSVZJV-Uval9oRI.js} +1 -1
  51. package/dist/worker/console/static/assets/channel-CVrnUdZl.js +1 -0
  52. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D_Bq2ZqK.js → chunk-2GRJ4B5K-BSYjHs-v.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CPLVtamK.js → chunk-2Q5K7J3B-KGUuBXI8.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DnaZWcL3.js → chunk-5RXB4S5H-JqAtxD9N.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BRTmFgnq.js → chunk-5VM5RSS4-D6EJOVQK.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BRlfYZoH.js → chunk-6Q2QTUOP-B1JriSjm.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CjNybD4u.js → chunk-GF5L2VYU-D62b96Jd.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D3e-74fG.js → chunk-JWPE2WC7-BsNKEC_r.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Br3ir6f2.js → chunk-KBJHAD2P-B56v6-ZP.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-1NgVuRbh.js → chunk-RYQCIY6F-D2KPnd8K.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BTYuvuWN.js → chunk-XXDRQBXY-kzKSCaaL.js} +1 -1
  62. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-aX91WTya.js +1 -0
  63. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-aX91WTya.js +1 -0
  64. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BC6Bgosf.js → cose-bilkent-JH36ORCC-C6N6KNJd.js} +1 -1
  65. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DV-HypfL.js → cynefin-VYW2F7L2-DiyymT2D.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DhTaddO-.js → cynefinDiagram-MW4NZA55-BeKAXvf3.js} +1 -1
  67. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BnEcOmiI.js → dagre-VZM6K2ZE-TkIdXfPF.js} +1 -1
  68. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-xphGRVKp.js → diagram-7IWD3JNH-BFmSG3ks.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CrdD1mRG.js → diagram-B4RE2ZJO-DqpJT2Fs.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-D4YdLsaF.js → diagram-LBJQPF4R-r5ATN43t.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-LCKyGZ5U.js → diagram-Q27KOJAE-DEcSCMuR.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BeiTe7t0.js → diagram-UB23O5K3-VOvYRLVc.js} +1 -1
  73. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BO2cq8GS.js → ebnfDiagram-BXEA7PRR-D7f4Cb1u.js} +1 -1
  74. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CmWww3Nt.js → erDiagram-JOGREHBK-ziUf3U0G.js} +1 -1
  75. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C2kkUYg5.js → flowDiagram-UKHOOZJN-BoRdQ-1y.js} +1 -1
  76. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DY_JZ2iQ.js → ganttDiagram-PKOTCBZU-B1-GncNm.js} +1 -1
  77. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BizhCSw9.js → gitGraphDiagram-DS77QQ5N-CTW-ph8W.js} +1 -1
  78. package/dist/worker/console/static/assets/index-C8rOIAJr.css +1 -0
  79. package/dist/worker/console/static/assets/{index-BXN89VPX.js → index-DEbyrEfW.js} +147 -68
  80. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Dlo2eK_d.js → infoDiagram-6WML65LV-BFA337tZ.js} +1 -1
  81. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js → ishikawaDiagram-WSZJBQD7-BtEujgRR.js} +1 -1
  82. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DiMDe5Yh.js → journeyDiagram-NVQOT4AX-C8ZwD2Jp.js} +1 -1
  83. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-sN553oIN.js → kanban-definition-27J2QSJJ-Ck-z8B11.js} +1 -1
  84. package/dist/worker/console/static/assets/{linear-D9Py31Ld.js → linear-CAyXjQjN.js} +1 -1
  85. package/dist/worker/console/static/assets/{mermaid.core-D7Occzk_.js → mermaid.core-CcWKn6tS.js} +5 -5
  86. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DjDRboiR.js → mindmap-definition-FAOFIHXS-C2NuDO7E.js} +1 -1
  87. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Cgc4qlzp.js → pegDiagram-VL7TDLO6-BFub3PDz.js} +1 -1
  88. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DxZj8pKd.js → pieDiagram-7S7Q4E2Y-BJx5THZ5.js} +1 -1
  89. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CJ50MngR.js → quadrantDiagram-CIZ2JOQS-DhalkDWV.js} +1 -1
  90. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BY-OD8Tr.js → railroadDiagram-AXF67PYL-CVcBM36o.js} +1 -1
  91. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CQFButfK.js → requirementDiagram-LRYGKXZP-D_vy4n0w.js} +1 -1
  92. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BB_QUK_b.js → sankeyDiagram-W5VNT64P-DGYdJL1D.js} +1 -1
  93. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CqhXZjyx.js → sequenceDiagram-SI44F4Z6-C1xJFg3k.js} +1 -1
  94. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-2l1U4rgJ.js → sizeCapture-X5ZJPWSS-D_Fc43df.js} +1 -1
  95. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BX1FbrhJ.js → stateDiagram-OKZ733FA-BhGbIjbu.js} +1 -1
  96. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DUt1lyFL.js +1 -0
  97. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DOSnT7hb.js → swimlanes-SLNWSIFB-DVkNcO-V.js} +2 -2
  98. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BM6SGr7j.js +8 -0
  99. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DVJQA7AN.js → timeline-definition-Z64GVDOM-BQU9ufV0.js} +1 -1
  100. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-74uxl9gx.js → vennDiagram-T6HMQDX7-BCQhRmYB.js} +1 -1
  101. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DsBxlcg_.js → wardleyDiagram-T6FBY63Y-DWerOyHO.js} +1 -1
  102. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-snuW9Mjw.js → xychartDiagram-ELKLHX3M-DxLeebQU.js} +1 -1
  103. package/dist/worker/console/static/index.html +2 -2
  104. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  105. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  106. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  107. package/dist/worker/observe/node-input.js +74 -3
  108. package/dist/worker/observe/node-process.js +377 -0
  109. package/dist/worker/observe/routes.js +36 -3
  110. package/dist/worker/observe/shell-handler-keys.js +34 -0
  111. package/dist/worker/observe/static/api.js +66 -0
  112. package/dist/worker/observe/static/constants.js +8 -1
  113. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  114. package/dist/worker/observe/static/format.js +23 -0
  115. package/dist/worker/observe/static/markdown-render.js +20 -1
  116. package/dist/worker/observe/static/operator-chrome.js +4 -0
  117. package/dist/worker/observe/static/state.js +13 -2
  118. package/dist/worker/observe/static/styles.css +73 -0
  119. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  120. package/dist/worker/observe/static/views/dag.js +8 -1
  121. package/dist/worker/observe/static/views/failures.js +5 -2
  122. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  123. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +76 -6
  124. package/dist/workflows/dag/backend-test-scenario-partitions.js +73 -1
  125. package/dist/workflows/dag/contract-output-registry.js +15 -0
  126. package/dist/workflows/dag/failure-category.js +12 -0
  127. package/dist/workflows/dag/frontend-implementation-contract.js +145 -40
  128. package/dist/workflows/dag/frontend-prewrite-gate.js +87 -135
  129. package/dist/workflows/dag/frontend-repair.js +14 -0
  130. package/dist/workflows/dag/frontend-risk.js +15 -2
  131. package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
  132. package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
  133. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  134. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  135. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  136. package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
  137. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  138. package/dist/workflows/dag/frontend-verification-trace.js +29 -4
  139. package/dist/workflows/dag/init-hybrid.js +196 -117
  140. package/dist/workflows/dag/node-execution.js +139 -10
  141. package/dist/workflows/dag/prompt.js +15 -1
  142. package/dist/workflows/dag/rerun-plan.js +134 -3
  143. package/dist/workflows/dag/rerun-run.js +58 -6
  144. package/dist/workflows/dag/retry-policy.js +91 -0
  145. package/dist/workflows/dag/structured-output-repair.js +712 -0
  146. package/dist/workflows/dag/types.js +32 -1
  147. package/dist/workflows/dag/validate.js +41 -1
  148. package/docs/init-surface.manifest.json +5 -0
  149. package/docs/operations/README.md +1 -1
  150. package/docs/operations/local-development-environment.md +1 -5
  151. package/docs/skills/vetted-skill-registry.md +14 -0
  152. package/docs/templates/README.md +1 -1
  153. package/docs/templates/agent-dag.schema.json +44 -0
  154. package/docs/templates/backend-test-dag.json +2 -2
  155. package/docs/templates/frontend-test-dag.json +8 -10
  156. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  157. package/docs/templates/init-managed-agents.md +13 -3
  158. package/harness.json +2 -2
  159. package/package.json +12 -4
  160. package/scripts/next-info.mjs +356 -0
  161. package/scripts/next-publish-gate.mjs +238 -0
  162. package/scripts/release-source-binding.mjs +251 -0
  163. package/skills/analyze-product-requirements/SKILL.md +8 -5
  164. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  165. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  166. package/skills/analyze-product-requirements/references/example.md +50 -0
  167. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  168. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  169. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  170. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  171. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  172. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  173. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  174. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  175. package/skills/codebase-scout/SKILL.md +1 -1
  176. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  177. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  178. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  179. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  180. package/skills/improve-codebase-architecture/deepening.md +37 -0
  181. package/skills/improve-codebase-architecture/html-report.md +123 -0
  182. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  183. package/skills/improve-codebase-architecture/language.md +53 -0
  184. package/skills/loop-agent/references/command-reference.md +11 -4
  185. package/skills/playwright-cli/SKILL.md +22 -0
  186. package/skills/playwright-cli-case-generator/SKILL.md +20 -1
  187. package/dist/worker/console/static/assets/channel-R6oQIDkw.js +0 -1
  188. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +0 -1
  189. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +0 -1
  190. package/dist/worker/console/static/assets/index-CuH5CNFu.css +0 -1
  191. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +0 -1
  192. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +0 -8
@@ -10,11 +10,13 @@ import { resolveContextPolicy } from "./context-policy.js";
10
10
  import { buildDagNodePromptEnvelope, formatConvergenceFeedbackBlock, } from "./prompt.js";
11
11
  import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
12
12
  import { buildOutputLimitRecoverySection, loadBackendTestWriterProgressForRetry, } from "./backend-test-writer-completeness.js";
13
- import { computeBackoffDelayMs, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, } from "./retry-policy.js";
13
+ import { computeBackoffDelayMs, isCanonicalFinalVerifyShellRetryCandidate, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, VERIFY_SHELL_RETRY_POLICY, } from "./retry-policy.js";
14
14
  import { applyNodeActivity, evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
15
15
  import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, parseJsonReviewVerdict, validateOutputProtocol, } from "./output-protocol.js";
16
16
  import { getStructuredContractValidator } from "./contract-output-registry.js";
17
17
  import "./contract-validator-registrations.js";
18
+ import { allowedRepairReadPaths, auditRepairAttemptToolUse, buildStructuredOutputRepairPrompt, freezeStructuredOutputRepairContext, hasNonEmptyStructuredCandidate, isFrontendStructuredRepairSchemaId, isStructuredRepairableFailureCategory, persistStructuredAttemptRaw, sessionEventsByteLength, GOVERNANCE_BLOCKED_CATEGORY, STRUCTURED_REPAIR_EXHAUSTED_CATEGORY, } from "./structured-output-repair.js";
19
+ import { readFrontendCanonicalCandidate } from "./frontend-implementation-contract.js";
18
20
  import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
19
21
  import { buildProjectGovernanceContext, readCompletedWriterChangeManifests, writeProjectGovernanceContext, } from "./project-governance-context.js";
20
22
  import { assertSkillSnapshotCoversSpec, buildNodePromptFromSnapshot, isDagSkillSnapshotIntegrityError, readSkillSnapshot, } from "./skill-snapshot.js";
@@ -135,6 +137,11 @@ function frontendStructuredArtifactRetryGuidance(schemaId) {
135
137
  "Return exactly this artifact and nothing else: one short lead-in line, then exactly one fenced json block containing only the editable RFC 7386 plan patch, immediately followed by exactly one fenced openspec-citations block.",
136
138
  "The runtime applies your patch to its protected contract skeleton. Do not emit schemaVersion, sourceBinding, riskLevel, targets.files, or mockApi.productionDefaultOff; do not emit the full contract.",
137
139
  "Do not emit Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
140
+ "The openspec-citations fenced block contains one JSON object per line, each with path/section/line — for example:",
141
+ "```openspec-citations",
142
+ '{"path":".harness/tasks/<task>/source/需求.md","section":"Objective","line":5}',
143
+ '{"path":".harness/tasks/<task>/source/references/product-requirement.md","section":"FR-1","line":16}',
144
+ "```",
138
145
  ];
139
146
  }
140
147
  if (schemaId === "frontend-implementation-contract-revision-patch-v1") {
@@ -142,6 +149,11 @@ function frontendStructuredArtifactRetryGuidance(schemaId) {
142
149
  "Return exactly this artifact and nothing else: one short lead-in line, then exactly one fenced json block containing the RFC 7386 merge-patch delta, immediately followed by exactly one fenced openspec-citations block.",
143
150
  "The delta contains only the fields you change; null deletes a key, arrays and scalars replace, and plain objects merge recursively. Do not emit a full contract or a schemaVersion/targets section.",
144
151
  "Do not emit Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
152
+ "The openspec-citations fenced block contains one JSON object per line, each with path/section/line — for example:",
153
+ "```openspec-citations",
154
+ '{"path":".harness/tasks/<task>/source/需求.md","section":"Objective","line":5}',
155
+ '{"path":".harness/tasks/<task>/source/references/product-requirement.md","section":"FR-1","line":16}',
156
+ "```",
145
157
  ];
146
158
  }
147
159
  return [
@@ -248,6 +260,16 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
248
260
  "</retry_instruction>",
249
261
  ].join("\n");
250
262
  }
263
+ if (previousFailureCategory === "writer-clean-timeout") {
264
+ return [
265
+ basePrompt,
266
+ "",
267
+ "<retry_instruction>",
268
+ "Previous attempt ended in a clean provider/transport failure with zero attributed workspace writes and zero write/edit tool calls.",
269
+ "Do not invent existing implementation progress. Continue from the original task only if the workspace is still unchanged.",
270
+ "</retry_instruction>",
271
+ ].join("\n");
272
+ }
251
273
  if (previousFailureCategory !== "output-too-large") {
252
274
  return basePrompt;
253
275
  }
@@ -684,12 +706,18 @@ export async function executeDagNode(input) {
684
706
  // Invalid or missing harness preserves the previous no-override behavior.
685
707
  }
686
708
  }
687
- const retryPolicy = task.retryPolicy &&
709
+ const specRetryPolicy = task.retryPolicy &&
688
710
  (isSafeReadOnlyPiRetryCandidate(task) ||
689
711
  isWriterEmptyDiffRetryCandidate(task) ||
690
712
  isWriterTransportRetryCandidate(task))
691
713
  ? task.retryPolicy
692
714
  : undefined;
715
+ const implicitVerifyRetryPolicy = specRetryPolicy === undefined &&
716
+ isCanonicalFinalVerifyShellRetryCandidate(task) &&
717
+ isTargetTemplateTransientRetryNode(task)
718
+ ? VERIFY_SHELL_RETRY_POLICY
719
+ : undefined;
720
+ const retryPolicy = specRetryPolicy ?? implicitVerifyRetryPolicy;
693
721
  const maxAttempts = retryPolicy?.maxAttempts ?? 1;
694
722
  const attempts = [];
695
723
  let totalAttemptWallDurationMs = 0;
@@ -731,10 +759,17 @@ export async function executeDagNode(input) {
731
759
  let terminalResult;
732
760
  let previousFailureCategory;
733
761
  let previousProtocolReason;
762
+ let pendingStructuredArtifact;
763
+ let lastRepairAllowlist = [];
734
764
  for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
735
765
  const attemptStartedAt = new Date().toISOString();
736
766
  node.currentAttempt = attemptNumber;
737
767
  node.livenessStatus = "active";
768
+ // Capture the session-events.jsonl length BEFORE the executor appends this
769
+ // attempt's events, so the post-attempt repair tool audit can be scoped to
770
+ // exactly this attempt's segment (B1: earlier attempts legitimately use
771
+ // ls/find and must not be attributed to the repair attempt).
772
+ const attemptSessionEventsOffset = await sessionEventsByteLength(runDir, nodeId);
738
773
  let result;
739
774
  try {
740
775
  validateRepairArtifactGateBeforeShell({
@@ -757,12 +792,53 @@ export async function executeDagNode(input) {
757
792
  (acc[i.path] ??= []).push(i.detail);
758
793
  return acc;
759
794
  }, {});
795
+ let attemptPrompt = buildAttemptPrompt(task, prompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics);
796
+ if (attemptNumber > 1 &&
797
+ isFrontendStructuredRepairSchemaId(task.structuredContractOutput?.schemaId) &&
798
+ isStructuredRepairableFailureCategory(previousFailureCategory) &&
799
+ (await hasNonEmptyStructuredCandidate({
800
+ runDir,
801
+ nodeId,
802
+ beforeAttempt: attemptNumber,
803
+ }))) {
804
+ const originalCanonical = task.structuredContractOutput?.schemaId ===
805
+ "frontend-implementation-contract-revision-patch-v1"
806
+ ? (await readFrontendCanonicalCandidate(runDir, "frontend-plan-pi"))
807
+ : undefined;
808
+ const frozen = await freezeStructuredOutputRepairContext({
809
+ runDir,
810
+ runId: state.runId,
811
+ taskId: spec.sourceBinding?.taskId ?? task.id,
812
+ nodeId,
813
+ nodeAttempt: attemptNumber,
814
+ targetSchemaId: task.structuredContractOutput.schemaId,
815
+ skeleton: task.structuredContractOutput?.skeleton,
816
+ originalCanonical,
817
+ latestStopReason: previousFailureCategory === "structured-output-truncated"
818
+ ? "length"
819
+ : previousFailureCategory,
820
+ });
821
+ lastRepairAllowlist = allowedRepairReadPaths({
822
+ runDir,
823
+ context: frozen.context,
824
+ contextPath: frozen.relativePath,
825
+ });
826
+ attemptPrompt = await buildStructuredOutputRepairPrompt({
827
+ runDir,
828
+ context: frozen.context,
829
+ contextPath: frozen.relativePath,
830
+ contextSha256: frozen.sha256,
831
+ maxAttempts,
832
+ schemaGuidance: frontendStructuredArtifactRetryGuidance(task.structuredContractOutput.schemaId),
833
+ previousReason: previousProtocolReason,
834
+ });
835
+ }
760
836
  result = await executeNode({
761
837
  task,
762
838
  cwd,
763
839
  model,
764
840
  ...(thinking ? { thinking } : {}),
765
- prompt: buildAttemptPrompt(task, prompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics),
841
+ prompt: attemptPrompt,
766
842
  attempt: attemptNumber,
767
843
  reportActivity,
768
844
  timeoutMs: livenessPolicy.absoluteMaxWallClockMs,
@@ -778,6 +854,35 @@ export async function executeDagNode(input) {
778
854
  durationMs: 0,
779
855
  };
780
856
  }
857
+ if (isFrontendStructuredRepairSchemaId(task.structuredContractOutput?.schemaId)) {
858
+ const rawText = canonicalNodeOutput(result);
859
+ if (rawText.trim().length > 0) {
860
+ await persistStructuredAttemptRaw({
861
+ runDir,
862
+ nodeId,
863
+ attempt: attemptNumber,
864
+ text: rawText,
865
+ });
866
+ }
867
+ if (attemptNumber > 1 && lastRepairAllowlist.length > 0) {
868
+ const audit = await auditRepairAttemptToolUse({
869
+ runDir,
870
+ nodeId,
871
+ allowedPaths: lastRepairAllowlist,
872
+ fromByteOffset: attemptSessionEventsOffset,
873
+ });
874
+ if (!audit.ok) {
875
+ result = {
876
+ ...result,
877
+ ok: false,
878
+ failureCategory: GOVERNANCE_BLOCKED_CATEGORY,
879
+ stderr: [result.stderr, audit.reason]
880
+ .filter(Boolean)
881
+ .join("\n"),
882
+ };
883
+ }
884
+ }
885
+ }
781
886
  // R0: executor ok=true still fails closed when outputProtocol is violated.
782
887
  // Valid semantic results (e.g. VERDICT: request-revision) pass validation.
783
888
  if (result.ok && task.outputProtocol) {
@@ -814,16 +919,17 @@ export async function executeDagNode(input) {
814
919
  structuredContractOutput: task.structuredContractOutput,
815
920
  });
816
921
  if (!contractCheck.ok) {
817
- // A provider stopReason=length means the response was cut before the
818
- // JSON contract could complete; separate it from an ordinary bad
819
- // contract so the retry switches to a JSON-only output strategy.
820
922
  const attemptStopReason = result.stopReason;
923
+ const failureCategory = contractCheck.classification === "governance-blocked"
924
+ ? GOVERNANCE_BLOCKED_CATEGORY
925
+ : attemptStopReason === "length" ||
926
+ contractCheck.classification === "truncated"
927
+ ? "structured-output-truncated"
928
+ : "invalid-output";
821
929
  result = {
822
930
  ...result,
823
931
  ok: false,
824
- failureCategory: attemptStopReason === "length"
825
- ? "structured-output-truncated"
826
- : "invalid-output",
932
+ failureCategory,
827
933
  stderr: [result.stderr, contractCheck.reason]
828
934
  .filter(Boolean)
829
935
  .join("\n"),
@@ -838,6 +944,9 @@ export async function executeDagNode(input) {
838
944
  stdout: contractCheck.normalizedText,
839
945
  };
840
946
  }
947
+ if (contractCheck.artifact) {
948
+ pendingStructuredArtifact = contractCheck.artifact;
949
+ }
841
950
  previousProtocolReason = undefined;
842
951
  }
843
952
  }
@@ -850,6 +959,7 @@ export async function executeDagNode(input) {
850
959
  const attemptFinishedAt = new Date().toISOString();
851
960
  const attemptWallDurationMs = durationBetween(attemptStartedAt, attemptFinishedAt);
852
961
  totalAttemptWallDurationMs += attemptWallDurationMs;
962
+ const rawFailureCategory = result.rawFailureCategory ?? result.failureCategory;
853
963
  const attemptRecord = {
854
964
  attempt: attemptNumber,
855
965
  startedAt: attemptStartedAt,
@@ -861,6 +971,7 @@ export async function executeDagNode(input) {
861
971
  stderr: result.stderr,
862
972
  assistantText: result.assistantText,
863
973
  failureCategory: result.failureCategory,
974
+ ...(rawFailureCategory ? { rawFailureCategory } : {}),
864
975
  backend: result.backend,
865
976
  sdkAttempted: result.sdkAttempted,
866
977
  tokensUsed: result.tokensUsed,
@@ -908,13 +1019,26 @@ export async function executeDagNode(input) {
908
1019
  break;
909
1020
  const canRetry = retryPolicy !== undefined && attemptNumber < maxAttempts;
910
1021
  const retryable = retryPolicy !== undefined &&
1022
+ result.failureCategory !== GOVERNANCE_BLOCKED_CATEGORY &&
911
1023
  (isRetryablePiFailureCategory(result.failureCategory, {
912
1024
  retryCategories: retryPolicy.retryCategories,
913
1025
  }) ||
914
1026
  (result.failureCategory === "protocol-invalid" &&
915
1027
  task.outputProtocol?.retryOnInvalid === true));
916
- if (!canRetry || !retryable)
1028
+ if (!canRetry || !retryable) {
1029
+ if (!canRetry &&
1030
+ isFrontendStructuredRepairSchemaId(task.structuredContractOutput?.schemaId) &&
1031
+ isStructuredRepairableFailureCategory(result.failureCategory)) {
1032
+ result = {
1033
+ ...result,
1034
+ failureCategory: STRUCTURED_REPAIR_EXHAUSTED_CATEGORY,
1035
+ };
1036
+ terminalResult = result;
1037
+ previousFailureCategory = result.failureCategory;
1038
+ node.failureCategory = result.failureCategory;
1039
+ }
917
1040
  break;
1041
+ }
918
1042
  const delayMs = computeBackoffDelayMs(attemptNumber + 1, retryPolicy);
919
1043
  if (delayMs > 0) {
920
1044
  const backoffStartedAt = new Date().toISOString();
@@ -1063,6 +1187,11 @@ export async function executeDagNode(input) {
1063
1187
  node.structuredArtifactSchemaId = "backend-test-result-v1";
1064
1188
  }
1065
1189
  }
1190
+ else if (pendingStructuredArtifact) {
1191
+ node.structuredArtifactPath = pendingStructuredArtifact.path;
1192
+ node.structuredArtifactSha256 = pendingStructuredArtifact.sha256;
1193
+ node.structuredArtifactSchemaId = pendingStructuredArtifact.schemaId;
1194
+ }
1066
1195
  }
1067
1196
  const finishedAt = new Date().toISOString();
1068
1197
  node.finishedAt = finishedAt;
@@ -116,7 +116,21 @@ export function buildUpstreamContext(task, upstream, maxChars = MAX_UPSTREAM_CHA
116
116
  ].join("\n"));
117
117
  continue;
118
118
  }
119
- if (!record || record.status !== "FINISHED" || !upstreamText)
119
+ if (!record || record.status !== "FINISHED")
120
+ continue;
121
+ if (record.structuredArtifactPath &&
122
+ record.structuredArtifactSchemaId ===
123
+ "frontend-implementation-contract-v1") {
124
+ sections.push([
125
+ `## Upstream output: ${depId}`,
126
+ "Validated structured artifact (read-only runner evidence — use read tool to fetch; do not edit). Do not infer the contract from stdout.",
127
+ `- path: ${record.structuredArtifactPath}`,
128
+ `- schema: ${record.structuredArtifactSchemaId}`,
129
+ `- sha256: ${record.structuredArtifactSha256 ?? "unknown"}`,
130
+ ].join("\n"));
131
+ continue;
132
+ }
133
+ if (!upstreamText)
120
134
  continue;
121
135
  const artifactKind = stdout ? "stdout" : "assistant";
122
136
  const artifactPath = stdout
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
3
3
  import { assertTaskContractBindingFresh } from "./task-contract-binding.js";
4
4
  import { isSafeReadOnlyPiRetryCandidate, isTransportCrashedExclusiveWriter } from "./retry-policy.js";
5
5
  import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
6
+ import { composeDagPromptOverridePrompt, MAX_DAG_RERUN_PROMPT_OVERRIDE_CHARS, } from "../../shared/dag-prompt-override.js";
6
7
  const REVIEW_VERDICT_RECOVERY_NODE_ID = "review-verdict-recovery-pi";
7
8
  const SAFE_SHELL_GATE_KEYS = [
8
9
  "verdictGate",
@@ -306,6 +307,9 @@ export function computeResetClosure(spec, effectiveFromNodeId) {
306
307
  ...collectReachableDescendants(spec, effectiveFromNodeId),
307
308
  ].sort();
308
309
  }
310
+ function isNeverStartedNodeStatus(status) {
311
+ return status === undefined || status === "SKIPPED" || status === "PENDING";
312
+ }
309
313
  export function assessResetClosureSafety(spec, resetNodeIds, options = {}) {
310
314
  const tasks = taskById(spec);
311
315
  const blockingNodes = [];
@@ -314,9 +318,17 @@ export function assessResetClosureSafety(spec, resetNodeIds, options = {}) {
314
318
  const task = tasks.get(nodeId);
315
319
  if (!task)
316
320
  continue;
321
+ if (options.parentNodes !== undefined &&
322
+ isNeverStartedNodeStatus(options.parentNodes[nodeId]?.status)) {
323
+ continue;
324
+ }
317
325
  if (isWriterTask(task, spec)) {
318
326
  if (nodeId === options.allowedTransportWriterNodeId)
319
327
  continue;
328
+ if (nodeId === options.allowedExclusiveShellFromNodeId &&
329
+ task.executor === "shell") {
330
+ continue;
331
+ }
320
332
  blockingNodes.push({
321
333
  nodeId,
322
334
  reasonCode: "restart-subgraph-contains-writer",
@@ -370,6 +382,100 @@ export function assessResetClosureSafety(spec, resetNodeIds, options = {}) {
370
382
  reasonCodes: [...new Set(reasonCodes)],
371
383
  };
372
384
  }
385
+ /**
386
+ * Experimental prompt-override admission (fail-closed):
387
+ * - requires the internal switch (see shared/dag-prompt-override.ts);
388
+ * - the target must be a failed (ERROR) node inside the reset closure, so an
389
+ * override can only adjust how an already-failed node retries — never a
390
+ * finished, skipped, or not-yet-started node;
391
+ * - only `subtask_prompt` semantics change: "append" keeps the frozen base
392
+ * and adds a marked addendum block, "replace" swaps the whole prompt;
393
+ * base/final sha256 are recorded for provenance and bound into planHash.
394
+ */
395
+ function assessPromptOverrides(input) {
396
+ const blockingNodes = [];
397
+ const reasonCodes = [];
398
+ const tasks = taskById(input.spec);
399
+ const resetSet = new Set(input.resetNodeIds);
400
+ const seen = new Set();
401
+ const entries = [];
402
+ if (!input.enabled) {
403
+ reasonCodes.push("prompt-override-disabled");
404
+ }
405
+ for (const override of input.overrides) {
406
+ const nodeId = override.nodeId?.trim() ?? "";
407
+ if (seen.has(nodeId)) {
408
+ blockingNodes.push({
409
+ nodeId,
410
+ reasonCode: "prompt-override-duplicate-node",
411
+ });
412
+ reasonCodes.push("prompt-override-duplicate-node");
413
+ continue;
414
+ }
415
+ seen.add(nodeId);
416
+ const task = tasks.get(nodeId);
417
+ if (!task) {
418
+ blockingNodes.push({
419
+ nodeId,
420
+ reasonCode: "prompt-override-node-missing",
421
+ });
422
+ reasonCodes.push("prompt-override-node-missing");
423
+ continue;
424
+ }
425
+ if (!resetSet.has(nodeId)) {
426
+ blockingNodes.push({
427
+ nodeId,
428
+ reasonCode: "prompt-override-outside-reset-closure",
429
+ });
430
+ reasonCodes.push("prompt-override-outside-reset-closure");
431
+ continue;
432
+ }
433
+ if (input.state.nodes[nodeId]?.status !== "ERROR") {
434
+ blockingNodes.push({
435
+ nodeId,
436
+ reasonCode: "prompt-override-node-not-failed",
437
+ });
438
+ reasonCodes.push("prompt-override-node-not-failed");
439
+ continue;
440
+ }
441
+ const mode = override.mode === "append" ? "append" : "replace";
442
+ const prompt = override.prompt ?? "";
443
+ if (!prompt.trim() ||
444
+ prompt.length > MAX_DAG_RERUN_PROMPT_OVERRIDE_CHARS) {
445
+ blockingNodes.push({
446
+ nodeId,
447
+ reasonCode: "prompt-override-invalid-prompt",
448
+ });
449
+ reasonCodes.push("prompt-override-invalid-prompt");
450
+ continue;
451
+ }
452
+ const basePromptSha256 = createHash("sha256")
453
+ .update(task.subtask_prompt, "utf8")
454
+ .digest("hex");
455
+ const finalPrompt = composeDagPromptOverridePrompt({
456
+ basePrompt: task.subtask_prompt,
457
+ text: prompt,
458
+ mode,
459
+ });
460
+ const promptSha256 = createHash("sha256")
461
+ .update(finalPrompt, "utf8")
462
+ .digest("hex");
463
+ if (promptSha256 === basePromptSha256) {
464
+ blockingNodes.push({
465
+ nodeId,
466
+ reasonCode: "prompt-override-unchanged",
467
+ });
468
+ reasonCodes.push("prompt-override-unchanged");
469
+ continue;
470
+ }
471
+ entries.push({ nodeId, mode, basePromptSha256, promptSha256 });
472
+ }
473
+ return {
474
+ entries: entries.length > 0 ? entries : undefined,
475
+ blockingNodes,
476
+ reasonCodes: [...new Set(reasonCodes)],
477
+ };
478
+ }
373
479
  export function computePlanHash(planWithoutHash) {
374
480
  const canonical = JSON.stringify(sortKeysDeep(planWithoutHash));
375
481
  return createHash("sha256").update(canonical, "utf8").digest("hex");
@@ -594,9 +700,17 @@ export async function evaluateDagRerunPlan(input) {
594
700
  blockedReasons.push("ambiguous-skipped-rewrite");
595
701
  }
596
702
  }
597
- const closureSafety = assessResetClosureSafety(input.parentSpec, resetNodeIds, transportWriterRestart && effectiveFromNodeId
598
- ? { allowedTransportWriterNodeId: effectiveFromNodeId }
599
- : {});
703
+ const closureSafety = assessResetClosureSafety(input.parentSpec, resetNodeIds, {
704
+ ...(transportWriterRestart && effectiveFromNodeId
705
+ ? { allowedTransportWriterNodeId: effectiveFromNodeId }
706
+ : {}),
707
+ parentNodes: input.parentState.nodes,
708
+ ...(effectiveFromNodeId &&
709
+ effectiveTask?.executor === "shell" &&
710
+ effectiveRecord?.status === "ERROR"
711
+ ? { allowedExclusiveShellFromNodeId: effectiveFromNodeId }
712
+ : {}),
713
+ });
600
714
  blockingNodes.push(...closureSafety.blockingNodes);
601
715
  for (const code of closureSafety.reasonCodes) {
602
716
  reasonCodes.push(code);
@@ -629,6 +743,22 @@ export async function evaluateDagRerunPlan(input) {
629
743
  blockingNodes.push({ nodeId, reasonCode: "parent-facts-invalid" });
630
744
  }
631
745
  }
746
+ let promptOverrideEntries;
747
+ if (input.promptOverrides !== undefined && input.promptOverrides.length > 0) {
748
+ const overrideAssessment = assessPromptOverrides({
749
+ spec: input.parentSpec,
750
+ state: input.parentState,
751
+ resetNodeIds,
752
+ overrides: input.promptOverrides,
753
+ enabled: input.promptOverrideEnabled === true,
754
+ });
755
+ blockingNodes.push(...overrideAssessment.blockingNodes);
756
+ for (const code of overrideAssessment.reasonCodes) {
757
+ reasonCodes.push(code);
758
+ blockedReasons.push(code);
759
+ }
760
+ promptOverrideEntries = overrideAssessment.entries;
761
+ }
632
762
  const uniqueReasonCodes = [...new Set(reasonCodes)];
633
763
  const uniqueBlockedReasons = [...new Set(blockedReasons)];
634
764
  const eligible = uniqueBlockedReasons.length === 0 &&
@@ -672,6 +802,7 @@ export async function evaluateDagRerunPlan(input) {
672
802
  parentLifecycle: input.parentLifecycle,
673
803
  workerManaged: input.workerManaged,
674
804
  }),
805
+ ...(promptOverrideEntries ? { promptOverrides: promptOverrideEntries } : {}),
675
806
  };
676
807
  return {
677
808
  ...planWithoutHash,
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { access, copyFile, mkdir, readdir, readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
5
+ import { composeDagPromptOverridePrompt, isDagRerunPromptOverrideEnabled, } from "../../shared/dag-prompt-override.js";
5
6
  import { resolveRunningControllerIdentity } from "../../shared/package-metadata.js";
6
7
  import { readControllerIdentityArtifact, captureControllerIdentity } from "./controller-identity.js";
7
8
  import { DAG_RUNS_DIR, getDagRunDir, isDagNonExecutionLifecycle, locateDagRun, readDagRunSpec, readDagRunState, } from "./lifecycle.js";
@@ -190,6 +191,8 @@ export async function planDagRerun(input) {
190
191
  parentSpec: nonCompletedSpec,
191
192
  parentState: nonCompletedState,
192
193
  parentLifecycle,
194
+ promptOverrides: input.promptOverrides,
195
+ promptOverrideEnabled: isDagRerunPromptOverrideEnabled(),
193
196
  workerManaged: nonCompletedWorkerHints.workerManaged,
194
197
  workerAssociation: nonCompletedWorkerHints.workerManaged
195
198
  ? {
@@ -229,6 +232,8 @@ export async function planDagRerun(input) {
229
232
  parentTerminalWorkspace,
230
233
  currentWorkspace,
231
234
  skillSnapshotOk,
235
+ promptOverrides: input.promptOverrides,
236
+ promptOverrideEnabled: isDagRerunPromptOverrideEnabled(),
232
237
  workerManaged: workerHints.workerManaged,
233
238
  workerAssociation: workerHints.workerManaged
234
239
  ? {
@@ -384,15 +389,57 @@ function buildPendingNodeRecord(task) {
384
389
  ...(task.outputMode ? { outputMode: task.outputMode } : {}),
385
390
  };
386
391
  }
392
+ /**
393
+ * Stage the continuation spec with operator prompt overrides applied. The
394
+ * parent spec stays frozen; every override must match the plan entry (sha256
395
+ * of the composed final prompt and of the parent base prompt) so the
396
+ * two-phase planHash remains the single source of truth for what changed.
397
+ * "append" entries keep the frozen base and append the operator addendum in a
398
+ * marked block; "replace" entries swap the whole prompt.
399
+ */
400
+ function applyPromptOverridesToSpec(input) {
401
+ const planned = new Map((input.plan.promptOverrides ?? []).map((entry) => [entry.nodeId, entry]));
402
+ const spec = structuredClone(input.spec);
403
+ for (const override of input.overrides) {
404
+ const entry = planned.get(override.nodeId);
405
+ if (!entry) {
406
+ throw new Error(`prompt override is not present in the rerun plan: ${override.nodeId}`);
407
+ }
408
+ const task = spec.tasks.find((candidate) => candidate.id === override.nodeId);
409
+ if (!task) {
410
+ throw new Error(`prompt override node missing from spec: ${override.nodeId}`);
411
+ }
412
+ if (sha256Hex(task.subtask_prompt) !== entry.basePromptSha256) {
413
+ throw new Error(`prompt override base prompt drifted from plan: ${override.nodeId}`);
414
+ }
415
+ const finalPrompt = composeDagPromptOverridePrompt({
416
+ basePrompt: task.subtask_prompt,
417
+ text: override.prompt,
418
+ mode: override.mode === "append" ? "append" : "replace",
419
+ });
420
+ if (sha256Hex(finalPrompt) !== entry.promptSha256) {
421
+ throw new Error(`prompt override text does not match the planned sha256: ${override.nodeId}`);
422
+ }
423
+ task.subtask_prompt = finalPrompt;
424
+ }
425
+ return spec;
426
+ }
387
427
  async function stageContinuationRun(input) {
388
- const { ranks } = topoSortToRanks(input.parentSpec);
389
- const runId = await buildDagRunId(input.parentSpec, { cwd: input.cwd });
428
+ const specForRun = input.promptOverrides !== undefined && input.promptOverrides.length > 0
429
+ ? applyPromptOverridesToSpec({
430
+ spec: input.parentSpec,
431
+ plan: input.plan,
432
+ overrides: input.promptOverrides,
433
+ })
434
+ : input.parentSpec;
435
+ const { ranks } = topoSortToRanks(specForRun);
436
+ const runId = await buildDagRunId(specForRun, { cwd: input.cwd });
390
437
  const runDir = getDagRunDir(input.cwd, "active", runId);
391
438
  await prepareActiveRunDir(runDir);
392
- await writeRunSpec(runDir, input.parentSpec);
439
+ await writeRunSpec(runDir, specForRun);
393
440
  const resetSet = new Set(input.plan.resetNodeIds);
394
441
  const importedSet = new Set(input.plan.importedNodeIds);
395
- const tasksById = new Map(input.parentSpec.tasks.map((task) => [task.id, task]));
442
+ const tasksById = new Map(specForRun.tasks.map((task) => [task.id, task]));
396
443
  const importManifestNodes = [];
397
444
  const nodes = {};
398
445
  for (const nodeId of input.plan.importedNodeIds) {
@@ -433,6 +480,9 @@ async function stageContinuationRun(input) {
433
480
  reason: input.reason,
434
481
  requestId: input.requestId,
435
482
  planHash: input.plan.planHash,
483
+ ...(input.plan.promptOverrides
484
+ ? { promptOverrides: input.plan.promptOverrides }
485
+ : {}),
436
486
  createdAt,
437
487
  };
438
488
  const state = {
@@ -482,7 +532,7 @@ async function stageContinuationRun(input) {
482
532
  await writeJsonAtomic(path.join(runDir, IMPORT_MANIFEST_REL_PATH), importManifest);
483
533
  await writeJsonAtomic(path.join(runDir, RERUN_PLAN_REL_PATH), input.plan);
484
534
  await writeRunState(runDir, state);
485
- return { runId, runDir, state };
535
+ return { runId, runDir, state, spec: specForRun };
486
536
  }
487
537
  export async function executeDagRerun(input) {
488
538
  const existing = await readExistingOperatorRequest(input.cwd, input.requestId);
@@ -493,6 +543,7 @@ export async function executeDagRerun(input) {
493
543
  cwd: input.cwd,
494
544
  parentRunId: input.parentRunId,
495
545
  selectedNodeId: input.selectedNodeId,
546
+ promptOverrides: input.promptOverrides,
496
547
  });
497
548
  if (plan.planHash !== input.planHash) {
498
549
  return {
@@ -530,10 +581,11 @@ export async function executeDagRerun(input) {
530
581
  plan,
531
582
  reason: input.reason,
532
583
  requestId: input.requestId,
584
+ promptOverrides: input.promptOverrides,
533
585
  });
534
586
  const continuationOptions = {
535
587
  cwd: input.cwd,
536
- spec: parentSpec,
588
+ spec: staged.spec,
537
589
  state: staged.state,
538
590
  runDir: staged.runDir,
539
591
  ...(input.maxConcurrent !== undefined