@tea-agent/loop-agent 0.39.0-next.8 → 0.39.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 (254) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +264 -67
  3. package/README.md +11 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/dag/generate-task-dag.js +9 -0
  6. package/dist/application/task-lifecycle/advance.js +4 -0
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +16 -5
  9. package/dist/cli/program.js +16 -2
  10. package/dist/cli/update/notifier.js +2 -2
  11. package/dist/cli.js +2 -1
  12. package/dist/commands/dag-rerun.js +55 -1
  13. package/dist/commands/init-model-catalog.js +464 -0
  14. package/dist/commands/init-upgrade.js +265 -97
  15. package/dist/commands/init.js +428 -27
  16. package/dist/commands/inspect-next.js +8 -0
  17. package/dist/commands/task-advance.js +31 -0
  18. package/dist/executors/dag-pi-executor.js +124 -18
  19. package/dist/executors/pi-event-serializer.js +42 -1
  20. package/dist/executors/pi-executor.js +51 -0
  21. package/dist/executors/pi-extension-resolver.js +233 -0
  22. package/dist/executors/pi-playwright-cli-tool.js +74 -28
  23. package/dist/executors/pi-sdk-executor.js +187 -59
  24. package/dist/executors/shell-executor.js +554 -33
  25. package/dist/executors/shell-write-guard.js +7 -0
  26. package/dist/shared/dag-failure-category.js +138 -0
  27. package/dist/shared/dag-prompt-override.js +28 -0
  28. package/dist/shared/operator/capabilities.js +181 -27
  29. package/dist/shared/playwright-cli-command-policy.js +15 -0
  30. package/dist/shared/update/console-notifier.js +100 -0
  31. package/dist/{cli → shared}/update/npm-client.js +41 -8
  32. package/dist/{cli → shared}/update/state.js +41 -13
  33. package/dist/task/config-types.js +28 -0
  34. package/dist/task/contract/constants.js +1 -0
  35. package/dist/task/contract/diff.js +26 -0
  36. package/dist/task/contract/project.js +5 -0
  37. package/dist/task/contract/schema.js +6 -1
  38. package/dist/task/source-prepare/build-draft.js +15 -0
  39. package/dist/task/source-prepare/parse-intent.js +24 -4
  40. package/dist/worker/cli.js +31 -1
  41. package/dist/worker/console/chat/deferred-turn.js +91 -0
  42. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  43. package/dist/worker/console/chat/operation-card.js +8 -1
  44. package/dist/worker/console/chat/pi-console-config.js +81 -10
  45. package/dist/worker/console/chat/pi-runtime.js +165 -83
  46. package/dist/worker/console/chat/repo-walk.js +10 -1
  47. package/dist/worker/console/chat/routes.js +480 -55
  48. package/dist/worker/console/chat/semantic-activity.js +20 -8
  49. package/dist/worker/console/chat/session-store.js +146 -2
  50. package/dist/worker/console/chat/turn-process.js +10 -10
  51. package/dist/worker/console/dag-execution-receipt.js +33 -0
  52. package/dist/worker/console/inspect-split.js +18 -0
  53. package/dist/worker/console/operation-run-facts.js +19 -3
  54. package/dist/worker/console/operator-actions.js +195 -4
  55. package/dist/worker/console/operator-user-error.js +2 -2
  56. package/dist/worker/console/pi-readiness.js +5 -4
  57. package/dist/worker/console/recovery-cta.js +4 -4
  58. package/dist/worker/console/recovery-error-copy.js +4 -4
  59. package/dist/worker/console/routes.js +43 -2
  60. package/dist/worker/console/security.js +63 -9
  61. package/dist/worker/console/server.js +42 -6
  62. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BLXFe4qI.js → abnfDiagram-N423BO3Z-Br92EGb7.js} +1 -1
  63. package/dist/worker/console/static/assets/{arc-7iOJeqho.js → arc-oulImtQq.js} +1 -1
  64. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CYoyj5K7.js → architectureDiagram-T3A2C74G-tyHc6aqO.js} +1 -1
  65. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-fK01JNlR.js → blockDiagram-VBNYF7ZC-CY86BSpT.js} +1 -1
  66. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-bzbw5D_Z.js → c4Diagram-5PPSVZJV-BKKsDmNs.js} +1 -1
  67. package/dist/worker/console/static/assets/channel-TY36zt-i.js +1 -0
  68. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Cf6hJFFG.js → chunk-2GRJ4B5K-CXZdjl3B.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DSFXR-et.js → chunk-2Q5K7J3B-B-aIFDQ5.js} +1 -1
  70. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BagJechl.js → chunk-5RXB4S5H-BDgAW_B2.js} +1 -1
  71. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-E7QNMFaW.js → chunk-5VM5RSS4-DABa4UPR.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BG_eOBrC.js → chunk-6Q2QTUOP-aGbysVOx.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bo42dqol.js → chunk-GF5L2VYU-B8MSZDVs.js} +1 -1
  74. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-dibM-87v.js → chunk-JWPE2WC7-CpelV73g.js} +1 -1
  75. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Bytrp3Oe.js → chunk-KBJHAD2P-BE04ty3z.js} +1 -1
  76. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DBABILrr.js → chunk-RYQCIY6F-BUxTkaaR.js} +1 -1
  77. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DCUk5Pf9.js → chunk-XXDRQBXY-CYYBVKfX.js} +1 -1
  78. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +1 -0
  79. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +1 -0
  80. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BlGE9v6E.js → cose-bilkent-JH36ORCC-CEQF_xmQ.js} +1 -1
  81. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DGl5ewph.js → cynefin-VYW2F7L2-CqpxN5IT.js} +1 -1
  82. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DsJ_SVH_.js → cynefinDiagram-MW4NZA55-B3PzanVi.js} +1 -1
  83. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BIhjFdzv.js → dagre-VZM6K2ZE-BSUGCNx1.js} +1 -1
  84. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dmy1ctTb.js → diagram-7IWD3JNH-CdLevU5T.js} +1 -1
  85. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DYCtWUF3.js → diagram-B4RE2ZJO-BjpF_Kok.js} +1 -1
  86. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BMZFHVd2.js → diagram-LBJQPF4R-C4I6NJny.js} +1 -1
  87. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-b_U8gfaO.js → diagram-Q27KOJAE-C8hecCr5.js} +1 -1
  88. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dzy_bn-I.js → diagram-UB23O5K3-Daod6tOS.js} +1 -1
  89. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DJCiMNEI.js → ebnfDiagram-BXEA7PRR-BvvmazGv.js} +1 -1
  90. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-28lD016B.js → erDiagram-JOGREHBK-DeN_vfV4.js} +1 -1
  91. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DGyHJmIw.js → flowDiagram-UKHOOZJN-D6dq1t9z.js} +1 -1
  92. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CzdwUjrO.js → ganttDiagram-PKOTCBZU-DziIdNY4.js} +1 -1
  93. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-D9bGwuo8.js → gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js} +1 -1
  94. package/dist/worker/console/static/assets/index-C3GgHg-g.css +1 -0
  95. package/dist/worker/console/static/assets/index-Ci8ksvBm.js +406 -0
  96. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-LJgJUQFC.js → infoDiagram-6WML65LV-Czv5eYx2.js} +1 -1
  97. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-98LaB8zb.js → ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js} +1 -1
  98. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-D-ISzwlQ.js → journeyDiagram-NVQOT4AX-DxGmNj5K.js} +1 -1
  99. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-gfYOI-EL.js → kanban-definition-27J2QSJJ-CYCFqkO_.js} +1 -1
  100. package/dist/worker/console/static/assets/{linear-CyntZgKV.js → linear-B0YzRwrd.js} +1 -1
  101. package/dist/worker/console/static/assets/{mermaid.core-BBOR7dg0.js → mermaid.core-t3EZtIGh.js} +5 -5
  102. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CQAgwPMk.js → mindmap-definition-FAOFIHXS-BTXpTv0T.js} +1 -1
  103. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DkMiZFuV.js → pegDiagram-VL7TDLO6-DU8UkKqA.js} +1 -1
  104. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DLNXa_0J.js → pieDiagram-7S7Q4E2Y-DP4y8ohp.js} +1 -1
  105. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Dc7bdzjm.js → quadrantDiagram-CIZ2JOQS-B07vNaYR.js} +1 -1
  106. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BxMB0QqW.js → railroadDiagram-AXF67PYL-xDWktb2i.js} +1 -1
  107. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-FjumoKn4.js → requirementDiagram-LRYGKXZP-BLNMKqeS.js} +1 -1
  108. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CkhKc_E3.js → sankeyDiagram-W5VNT64P-n0adalxH.js} +1 -1
  109. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DoBWqTTH.js → sequenceDiagram-SI44F4Z6-BbnS-2DL.js} +1 -1
  110. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CQeh_YeE.js → sizeCapture-X5ZJPWSS-B4uPpCLu.js} +1 -1
  111. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-LuHmFW-I.js → stateDiagram-OKZ733FA--MMbB0fX.js} +1 -1
  112. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +1 -0
  113. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CHwbDctY.js → swimlanes-SLNWSIFB-BXUgp2QY.js} +2 -2
  114. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.js +8 -0
  115. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CoMngk0X.js → timeline-definition-Z64GVDOM-CsaMxki7.js} +1 -1
  116. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CURYbtbO.js → vennDiagram-T6HMQDX7-C2IuG9iH.js} +1 -1
  117. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BdqSECjU.js → wardleyDiagram-T6FBY63Y-CmEWOXCW.js} +1 -1
  118. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DbMUYuAg.js → xychartDiagram-ELKLHX3M-D_9LE0MG.js} +1 -1
  119. package/dist/worker/console/static/index.html +2 -2
  120. package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
  121. package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
  122. package/dist/worker/console/static-src/app/useRecoveryConsole.js +38 -2
  123. package/dist/worker/console/static-src/app/useRunProgress.js +46 -0
  124. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  125. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +74 -26
  126. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  127. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  128. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  129. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  130. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
  131. package/dist/worker/console/static-src/operator-chat/turn-submission.js +54 -0
  132. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
  133. package/dist/worker/console/static-src/operator-chat/useChatStream.js +237 -11
  134. package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
  135. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  136. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  137. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  138. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  139. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  140. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  141. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  142. package/dist/worker/observability/read-model.js +22 -0
  143. package/dist/worker/observe/node-input.js +74 -3
  144. package/dist/worker/observe/node-process.js +377 -0
  145. package/dist/worker/observe/routes.js +36 -3
  146. package/dist/worker/observe/shell-handler-keys.js +34 -0
  147. package/dist/worker/observe/static/api.js +66 -0
  148. package/dist/worker/observe/static/constants.js +8 -1
  149. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  150. package/dist/worker/observe/static/format.js +23 -0
  151. package/dist/worker/observe/static/markdown-render.js +20 -1
  152. package/dist/worker/observe/static/operator-chrome.js +4 -0
  153. package/dist/worker/observe/static/state.js +13 -2
  154. package/dist/worker/observe/static/styles.css +73 -0
  155. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  156. package/dist/worker/observe/static/views/dag.js +8 -1
  157. package/dist/worker/observe/static/views/failures.js +5 -2
  158. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  159. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +827 -53
  160. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  161. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  162. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  163. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  164. package/dist/workflows/dag/backend-test-layout.js +133 -0
  165. package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
  166. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  167. package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
  168. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  169. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  170. package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
  171. package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
  172. package/dist/workflows/dag/contract-output-registry.js +15 -0
  173. package/dist/workflows/dag/contract-validator-registrations.js +3 -1
  174. package/dist/workflows/dag/convergence/controller.js +141 -0
  175. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  176. package/dist/workflows/dag/dynamic-runtime/map.js +25 -9
  177. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  178. package/dist/workflows/dag/failure-category.js +7 -116
  179. package/dist/workflows/dag/frontend-implementation-contract.js +377 -4
  180. package/dist/workflows/dag/frontend-plan-render.js +1 -1
  181. package/dist/workflows/dag/frontend-prewrite-gate.js +96 -84
  182. package/dist/workflows/dag/frontend-repair.js +14 -0
  183. package/dist/workflows/dag/frontend-risk.js +15 -2
  184. package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
  185. package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
  186. package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
  187. package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
  188. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  189. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  190. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  191. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  192. package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
  193. package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
  194. package/dist/workflows/dag/frontend-verification-trace.js +29 -4
  195. package/dist/workflows/dag/init-hybrid.js +894 -205
  196. package/dist/workflows/dag/node-execution.js +183 -15
  197. package/dist/workflows/dag/prompt.js +60 -1
  198. package/dist/workflows/dag/rerun-feedback.js +212 -0
  199. package/dist/workflows/dag/rerun-plan.js +224 -27
  200. package/dist/workflows/dag/rerun-run.js +61 -6
  201. package/dist/workflows/dag/rerun-task.js +61 -2
  202. package/dist/workflows/dag/retry-policy.js +109 -0
  203. package/dist/workflows/dag/structured-output-repair.js +712 -0
  204. package/dist/workflows/dag/types.js +143 -1
  205. package/dist/workflows/dag/validate.js +64 -20
  206. package/docs/init-surface.manifest.json +5 -0
  207. package/docs/operations/README.md +1 -1
  208. package/docs/operations/local-development-environment.md +1 -5
  209. package/docs/skills/vetted-skill-registry.md +14 -0
  210. package/docs/templates/README.md +1 -1
  211. package/docs/templates/agent-dag.schema.json +44 -0
  212. package/docs/templates/backend-test-dag.json +60 -35
  213. package/docs/templates/frontend-implementation-contract.schema.json +1 -1
  214. package/docs/templates/frontend-test-case-checklist.md +2 -2
  215. package/docs/templates/frontend-test-dag.json +8 -10
  216. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  217. package/docs/templates/init-managed-agents.md +13 -3
  218. package/harness.json +6 -4
  219. package/package.json +12 -4
  220. package/scripts/next-info.mjs +356 -0
  221. package/scripts/next-publish-gate.mjs +238 -0
  222. package/scripts/release-source-binding.mjs +251 -0
  223. package/skills/analyze-product-requirements/SKILL.md +8 -5
  224. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  225. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  226. package/skills/analyze-product-requirements/references/example.md +50 -0
  227. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  228. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  229. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  230. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  231. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  232. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  233. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  234. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  235. package/skills/codebase-scout/SKILL.md +1 -1
  236. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  237. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  238. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  239. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  240. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  241. package/skills/improve-codebase-architecture/deepening.md +37 -0
  242. package/skills/improve-codebase-architecture/html-report.md +123 -0
  243. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  244. package/skills/improve-codebase-architecture/language.md +53 -0
  245. package/skills/loop-agent/references/command-reference.md +11 -4
  246. package/skills/playwright-cli/SKILL.md +23 -1
  247. package/skills/playwright-cli-case-generator/SKILL.md +29 -8
  248. package/dist/worker/console/static/assets/channel-BBOTJHvD.js +0 -1
  249. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cm8Yf8tD.js +0 -1
  250. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cm8Yf8tD.js +0 -1
  251. package/dist/worker/console/static/assets/index-B-qWGThd.js +0 -325
  252. package/dist/worker/console/static/assets/index-C-xRgY_0.css +0 -1
  253. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DZihfdKc.js +0 -1
  254. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CKnXCjHX.js +0 -8
@@ -1,6 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from "react";
2
- import { parseAssistantContent } from "../../chat/assistant-content.js";
3
- import { buildTurnProcessGroups } from "../../chat/turn-process.js";
2
+ import { buildTurnProcessGroups, hydrateSessionThread, selectRecentTurnGroups, } from "../../chat/turn-process.js";
4
3
  import { createChatSseEventApplier, } from "./chat-sse-events.js";
5
4
  import { taskContextUnchanged } from "./format.js";
6
5
  const EMPTY_USAGE = {
@@ -32,6 +31,8 @@ export function useChatThread(params) {
32
31
  const [contextLoading, setContextLoading] = useState(false);
33
32
  const [usage, setUsage] = useState(EMPTY_USAGE);
34
33
  const [streaming, setStreaming] = useState(false);
34
+ const [turnPhase, setTurnPhase] = useState("idle");
35
+ const [deferredTurn, setDeferredTurn] = useState(null);
35
36
  /** Assistant bubble id for the in-flight turn; only this row shows the streaming cursor. */
36
37
  const [streamingAssistantId, setStreamingAssistantId] = useState(null);
37
38
  const [showSystemInMessages] = useState(false);
@@ -45,6 +46,8 @@ export function useChatThread(params) {
45
46
  setHumanGateCards,
46
47
  setArtifactCards,
47
48
  setStreaming,
49
+ setTurnPhase,
50
+ setDeferredTurn,
48
51
  setStreamingAssistantId,
49
52
  setUnread,
50
53
  setOperationCards,
@@ -121,9 +124,7 @@ export function useChatThread(params) {
121
124
  }, 350);
122
125
  return () => window.clearTimeout(timer);
123
126
  }, [sessionId, operationCards, humanGateCards, interview, loadTaskContext]);
124
- const visibleMessages = useMemo(() => messages
125
- .filter((m) => m.role !== "system" || showSystemInMessages)
126
- .slice(-historyLimit), [messages, showSystemInMessages, historyLimit]);
127
+ const timelineMessages = useMemo(() => messages.filter((m) => m.role !== "system" || showSystemInMessages), [messages, showSystemInMessages]);
127
128
  useEffect(() => {
128
129
  const onVisibility = () => {
129
130
  if (!document.hidden)
@@ -158,12 +159,30 @@ export function useChatThread(params) {
158
159
  statusKind,
159
160
  };
160
161
  }, [toolCalls]);
161
- const turnGroups = useMemo(() => buildTurnProcessGroups({
162
- messages: visibleMessages,
162
+ const turnGroups = useMemo(() => {
163
+ const all = buildTurnProcessGroups({
164
+ messages: timelineMessages,
165
+ toolCalls,
166
+ streaming,
167
+ activeAssistantId: streamingAssistantId,
168
+ });
169
+ return selectRecentTurnGroups(all, historyLimit);
170
+ }, [
171
+ timelineMessages,
163
172
  toolCalls,
164
173
  streaming,
165
- activeAssistantId: streamingAssistantId,
166
- }), [visibleMessages, toolCalls, streaming, streamingAssistantId]);
174
+ streamingAssistantId,
175
+ historyLimit,
176
+ ]);
177
+ const visibleMessages = useMemo(() => {
178
+ const next = [];
179
+ for (const group of turnGroups) {
180
+ if (group.user)
181
+ next.push(group.user);
182
+ next.push(...group.assistants);
183
+ }
184
+ return next;
185
+ }, [turnGroups]);
167
186
  /** Reset all session-scoped projections before adopting a fresh session. */
168
187
  const resetForNewSession = useCallback(() => {
169
188
  setMessages([]);
@@ -175,6 +194,8 @@ export function useChatThread(params) {
175
194
  setTaskContext(null);
176
195
  setUsage(EMPTY_USAGE);
177
196
  setStreaming(false);
197
+ setTurnPhase("idle");
198
+ setDeferredTurn(null);
178
199
  setStreamingAssistantId(null);
179
200
  setUnread(0);
180
201
  setHistoryLimit(50);
@@ -192,6 +213,8 @@ export function useChatThread(params) {
192
213
  setUsage(EMPTY_USAGE);
193
214
  refs.usageResponseKeysRef.current = new Set();
194
215
  setStreaming(next.streaming);
216
+ setTurnPhase(next.turnPhase ?? (next.streaming ? "running" : "idle"));
217
+ setDeferredTurn(next.deferredTurn ?? null);
195
218
  setStreamingAssistantId(null);
196
219
  setUnread(0);
197
220
  setHistoryLimit(50);
@@ -213,6 +236,8 @@ export function useChatThread(params) {
213
236
  const activeTurnId = body.session.activeTurnId ?? null;
214
237
  refs.activeTurnIdRef.current = activeTurnId;
215
238
  refs.streamingRef.current = Boolean(activeTurnId);
239
+ setTurnPhase(body.session.turnPhase ?? (activeTurnId ? "running" : "idle"));
240
+ setDeferredTurn(body.session.deferredTurn ?? null);
216
241
  // Resume the events stream from the snapshot tail so the next
217
242
  // reconnect never immediately re-expires (AC-003). The refs below
218
243
  // are reset so any follow-up tail replay re-applies cleanly.
@@ -220,18 +245,9 @@ export function useChatThread(params) {
220
245
  refs.seenEventIdsRef.current = new Set();
221
246
  refs.turnAssistantIdsRef.current = new Map();
222
247
  refs.sealedAssistantIdsRef.current = new Set();
223
- setMessages((body.session.messages ?? [])
224
- .filter((message) => message.role === "user" || message.role === "assistant")
225
- .map((message) => {
226
- const content = parseAssistantContent(message.content);
227
- return {
228
- id: message.id,
229
- role: message.role,
230
- text: message.text,
231
- createdAt: Date.parse(message.at) || Date.now(),
232
- ...(content && content.length > 0 ? { content } : {}),
233
- };
234
- }));
248
+ const hydrated = hydrateSessionThread(body.session.messages ?? []);
249
+ setMessages(hydrated.messages);
250
+ setToolCalls(hydrated.toolCalls);
235
251
  setStreamingAssistantId(null);
236
252
  return true;
237
253
  }
@@ -240,7 +256,15 @@ export function useChatThread(params) {
240
256
  // trigger another reconcile and retry (best-effort recovery).
241
257
  return false;
242
258
  }
243
- }, [origin, refs, setMessages, setStreamingAssistantId]);
259
+ }, [
260
+ origin,
261
+ refs,
262
+ setMessages,
263
+ setToolCalls,
264
+ setStreamingAssistantId,
265
+ setTurnPhase,
266
+ setDeferredTurn,
267
+ ]);
244
268
  return {
245
269
  messages,
246
270
  setMessages,
@@ -259,6 +283,10 @@ export function useChatThread(params) {
259
283
  usage,
260
284
  streaming,
261
285
  setStreaming,
286
+ turnPhase,
287
+ setTurnPhase,
288
+ deferredTurn,
289
+ setDeferredTurn,
262
290
  streamingAssistantId,
263
291
  setStreamingAssistantId,
264
292
  visibleMessages,
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } f
2
2
  import { filterSlashCommands, mergeSlashCommands, } from "../../chat/shortcuts.js";
3
3
  import { confirmationToken } from "./format.js";
4
4
  import { clampShortcutIndex } from "./slash-palette-nav.js";
5
+ import { createInputHistoryState, navigateInputHistory, recordInputHistoryEntry, resetInputHistoryBrowsing, } from "./input-history.js";
5
6
  import { attachRuntimeSnapshotController } from "./useRuntimeSnapshot.js";
6
7
  /** Composer state: input text, @file palette, image attachments, /shortcuts,
7
8
  * IME composition guards, auto-grow and server-side draft sync. */
@@ -31,6 +32,8 @@ export function useComposer(params) {
31
32
  const [dynamicCommandsError, setDynamicCommandsError] = useState(null);
32
33
  const [commandsEpoch, setCommandsEpoch] = useState(0);
33
34
  const textareaRef = useRef(null);
35
+ /** Arrow-key input history; unsent drafts survive history browsing. */
36
+ const historyRef = useRef(createInputHistoryState());
34
37
  /** Active session generation for /commands; bumped immediately on switch (AC-2). */
35
38
  const commandsSessionRef = useRef(sessionId);
36
39
  const commandsGenerationRef = useRef(0);
@@ -38,6 +41,33 @@ export function useComposer(params) {
38
41
  setInput(value);
39
42
  setShortcutOpen(value.startsWith("/"));
40
43
  setShortcutIndex(0);
44
+ // Manual typing edits a recalled entry in place → back to present draft.
45
+ historyRef.current = resetInputHistoryBrowsing(historyRef.current);
46
+ }, []);
47
+ /** Recall an earlier input via ↑/↓; returns true when the key was consumed. */
48
+ const navigateHistory = useCallback((direction) => {
49
+ const textarea = textareaRef.current;
50
+ const decision = navigateInputHistory(historyRef.current, direction, {
51
+ text: input,
52
+ caretStart: textarea?.selectionStart ?? input.length,
53
+ caretEnd: textarea?.selectionEnd ?? input.length,
54
+ });
55
+ if (!decision.handled)
56
+ return false;
57
+ historyRef.current = decision.state;
58
+ setInput(decision.text);
59
+ const caret = decision.text.length;
60
+ queueMicrotask(() => {
61
+ const el = textareaRef.current;
62
+ if (el && el.value === decision.text) {
63
+ el.setSelectionRange(caret, caret);
64
+ }
65
+ });
66
+ return true;
67
+ }, [input, setInput]);
68
+ /** Record a submitted text into history (consecutive duplicates collapse). */
69
+ const recordHistory = useCallback((text) => {
70
+ historyRef.current = recordInputHistoryEntry(historyRef.current, text);
41
71
  }, []);
42
72
  // Immediate invalidation on A→B even when B palette never opens (AC-2).
43
73
  useLayoutEffect(() => {
@@ -234,6 +264,8 @@ export function useComposer(params) {
234
264
  input,
235
265
  setInput,
236
266
  handleInputChange,
267
+ navigateHistory,
268
+ recordHistory,
237
269
  fileOptions,
238
270
  pendingImages,
239
271
  setPendingImages,
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useState } from "react";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { ACTIVE_CHAT_SESSION_STORAGE_KEY } from "../../chat/workspace-landing.js";
3
3
  /** Left-rail repo tree + file preview tabs state. */
4
4
  export function useRepoBrowser(params) {
@@ -9,6 +9,8 @@ export function useRepoBrowser(params) {
9
9
  const [previews, setPreviews] = useState([]);
10
10
  const [activePreviewPath, setActivePreviewPath] = useState(null);
11
11
  const [previewLoading, setPreviewLoading] = useState(false);
12
+ const expandedDirsRef = useRef(new Set());
13
+ expandedDirsRef.current = expandedDirs;
12
14
  const loadTree = useCallback(async (directory = "") => {
13
15
  try {
14
16
  setTreeError(null);
@@ -28,6 +30,28 @@ export function useRepoBrowser(params) {
28
30
  useEffect(() => {
29
31
  void loadTree();
30
32
  }, [loadTree]);
33
+ /** pi-web parity: re-fetch the root + every expanded directory. Bump
34
+ * `refreshTick` to trigger; cached entries are NOT reused, so newly
35
+ * created/deleted files appear without a manual refresh. */
36
+ const [refreshTick, setRefreshTick] = useState(0);
37
+ const refreshTree = useCallback(() => {
38
+ setRefreshTick((tick) => tick + 1);
39
+ }, []);
40
+ useEffect(() => {
41
+ const dirs = ["", ...expandedDirsRef.current];
42
+ for (const dir of dirs)
43
+ void loadTree(dir);
44
+ }, [loadTree, refreshTick]);
45
+ // Also refresh when the tab regains focus: external tools (editor, git,
46
+ // CLI) may have changed the tree while the console was hidden.
47
+ useEffect(() => {
48
+ const onVisibility = () => {
49
+ if (!document.hidden)
50
+ refreshTree();
51
+ };
52
+ document.addEventListener("visibilitychange", onVisibility);
53
+ return () => document.removeEventListener("visibilitychange", onVisibility);
54
+ }, [refreshTree]);
31
55
  const openPreview = useCallback(async (file) => {
32
56
  setPreviewLoading(true);
33
57
  try {
@@ -116,6 +140,7 @@ export function useRepoBrowser(params) {
116
140
  previewLoading,
117
141
  setPreviews,
118
142
  loadTree,
143
+ refreshTree,
119
144
  openPreview,
120
145
  closePreview,
121
146
  toggleDir,
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Pure decision core for the TasksPage run-id resolver (2026-08-23).
3
+ *
4
+ * Extracted from the React effect so the full lifecycle — wizard first run,
5
+ * rerun dispatch, task switch, foreign shared selection, baseline-gated
6
+ * lookup — is unit-testable as state transitions without mounting React.
7
+ */
8
+ export function initialRunResolutionState() {
9
+ return { baseline: null, ownedRunId: "", pendingSelection: "" };
10
+ }
11
+ export function observeRunResolution(state, input) {
12
+ const firstObservation = state.task === undefined;
13
+ const taskChanged = !firstObservation && state.task !== input.task;
14
+ const dispatchChanged = !firstObservation && state.dispatch !== input.dispatchId;
15
+ const next = {
16
+ ...state,
17
+ task: input.task,
18
+ dispatch: input.dispatchId,
19
+ pendingSelection: "",
20
+ };
21
+ if (taskChanged) {
22
+ next.baseline = null;
23
+ next.ownedRunId = "";
24
+ return { state: next, observation: { action: "reset-selection" } };
25
+ }
26
+ if (dispatchChanged) {
27
+ next.ownedRunId = "";
28
+ if (input.selectedRunId) {
29
+ next.baseline = input.selectedRunId;
30
+ return { state: next, observation: { action: "invalidate-selection" } };
31
+ }
32
+ // Nothing shown yet: keep the existing baseline and keep probing.
33
+ }
34
+ const selected = input.selectedRunId;
35
+ if (selected && selected !== next.ownedRunId) {
36
+ next.pendingSelection = selected;
37
+ return { state: next, observation: { action: "verify-ownership" } };
38
+ }
39
+ return {
40
+ state: next,
41
+ observation: { action: selected ? "idle" : "probe" },
42
+ };
43
+ }
44
+ export function markBaselineUnknown(state) {
45
+ return { ...state, baseline: null };
46
+ }
47
+ export function applyBaselineSnapshot(state, dagRunId) {
48
+ return { ...state, baseline: dagRunId ?? "" };
49
+ }
50
+ /** Accept an observeLink lookup: only a run of this task that differs from
51
+ * the pre-dispatch baseline may be adopted — never the previous run, and
52
+ * never anything while the baseline is still unknown. */
53
+ export function applyRunLookup(state, outcome) {
54
+ if (!outcome.ok || !outcome.dagRunId) {
55
+ return { state, acceptedRunId: null };
56
+ }
57
+ if (state.baseline === null || outcome.dagRunId === state.baseline) {
58
+ return { state, acceptedRunId: null };
59
+ }
60
+ return {
61
+ state: { ...state, ownedRunId: outcome.dagRunId },
62
+ acceptedRunId: outcome.dagRunId,
63
+ };
64
+ }
65
+ /** Apply an authoritative ownership answer for the pending selection.
66
+ * `clearSelection` tells the adapter to drop the shared selection and
67
+ * snapshot a fresh baseline; owning marks it as resolver-owned. */
68
+ export function applyOwnership(state, owns) {
69
+ if (owns) {
70
+ return {
71
+ state: { ...state, ownedRunId: state.pendingSelection, pendingSelection: "" },
72
+ clearSelection: false,
73
+ };
74
+ }
75
+ return {
76
+ state: { ...state, ownedRunId: "", pendingSelection: "", baseline: null },
77
+ clearSelection: true,
78
+ };
79
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Ownership re-check backoff for an unverified shared DAG-run selection
3
+ * (2026-08-23): a transient candidates-lookup failure must neither leave
4
+ * another task's run displayed indefinitely (waiting for a state change
5
+ * that may never come) nor hammer the API.
6
+ */
7
+ const OWNERSHIP_RETRY_DELAYS_MS = [
8
+ 2_000, 2_000, 4_000, 4_000, 8_000, 8_000, 15_000, 15_000, 30_000, 30_000,
9
+ ];
10
+ /** `owns`: true = same task, false = foreign task, null = check unavailable. */
11
+ export function planOwnershipVerification(owns, attempt) {
12
+ if (owns === true)
13
+ return { kind: "own" };
14
+ if (owns === false)
15
+ return { kind: "clear" };
16
+ const delayMs = OWNERSHIP_RETRY_DELAYS_MS[attempt];
17
+ return delayMs === undefined
18
+ ? { kind: "give-up" }
19
+ : { kind: "retry", delayMs };
20
+ }
21
+ /** Total bounded re-verification window before giving up (keeps the last
22
+ * selection; the next state change restarts verification). */
23
+ export const OWNERSHIP_RETRY_WINDOW_MS = OWNERSHIP_RETRY_DELAYS_MS.reduce((total, delay) => total + delay, 0);
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Pure projection from a DAG run detail payload (GET /api/dag-runs/:id) into
3
+ * the structured run progress shown on the wizard RunPanel: node counts,
4
+ * current node + elapsed time, failure pointer, and stall signal. The panel
5
+ * used to show only a raw SSE log tail — node/phase information drowned in
6
+ * log noise during 20-40 minute runs.
7
+ */
8
+ export function classifyRunProgressNodeStatus(raw) {
9
+ const status = (raw ?? "").trim().toLowerCase();
10
+ if (!status)
11
+ return "pending";
12
+ if (status.includes("running") || status === "in_progress" || status === "active") {
13
+ return "running";
14
+ }
15
+ if (status.includes("fail") || status.includes("error") || status === "blocked") {
16
+ return "failed";
17
+ }
18
+ if (status.includes("finish") ||
19
+ status.includes("success") ||
20
+ status === "done" ||
21
+ status === "ok") {
22
+ return "finished";
23
+ }
24
+ if (status.includes("skip"))
25
+ return "skipped";
26
+ return "pending";
27
+ }
28
+ function toProgressNode(node) {
29
+ const nodeId = String(node.nodeId ?? node.id ?? "").trim();
30
+ return {
31
+ nodeId,
32
+ label: String((node.label ?? node.name ?? nodeId) || "未命名节点"),
33
+ status: String(node.status ?? ""),
34
+ rank: typeof node.rank === "string" ? node.rank : undefined,
35
+ startedAt: typeof node.startedAt === "string" ? node.startedAt : undefined,
36
+ durationMs: typeof node.durationMs === "number" ? node.durationMs : undefined,
37
+ errorPreview: typeof node.errorPreview === "string" ? node.errorPreview : undefined,
38
+ };
39
+ }
40
+ function elapsedSince(iso, now) {
41
+ if (!iso)
42
+ return null;
43
+ const started = Date.parse(iso);
44
+ if (Number.isNaN(started))
45
+ return null;
46
+ const elapsed = now - started;
47
+ return elapsed >= 0 ? elapsed : null;
48
+ }
49
+ export function summarizeRunProgress(dag, now = Date.now()) {
50
+ if (!dag) {
51
+ return {
52
+ total: 0,
53
+ finished: 0,
54
+ running: null,
55
+ failed: null,
56
+ terminal: false,
57
+ effectiveStatus: null,
58
+ stalled: false,
59
+ currentNodeElapsedMs: null,
60
+ runElapsedMs: null,
61
+ };
62
+ }
63
+ const rawNodes = Array.isArray(dag.nodes) ? dag.nodes : [];
64
+ const nodes = rawNodes.map(toProgressNode);
65
+ const classes = nodes.map((node) => classifyRunProgressNodeStatus(node.status));
66
+ const finished = classes.filter((cls) => cls === "finished" || cls === "skipped").length;
67
+ const runningIndex = classes.indexOf("running");
68
+ const running = runningIndex >= 0 ? nodes[runningIndex] : null;
69
+ const failedIndex = classes.indexOf("failed");
70
+ const failed = failedIndex >= 0 ? nodes[failedIndex] : null;
71
+ const effectiveStatus = String(dag.effectiveStatus ??
72
+ dag.status ??
73
+ "").toLowerCase();
74
+ const terminal = TERMINAL_RUN_PROGRESS_STATUSES.has(effectiveStatus);
75
+ const runStartedAt = typeof dag.startedAt === "string" ? dag.startedAt : undefined;
76
+ const runDurationMs = typeof dag.durationMs === "number" ? dag.durationMs : undefined;
77
+ return {
78
+ total: nodes.length,
79
+ finished,
80
+ running,
81
+ failed,
82
+ terminal,
83
+ effectiveStatus: effectiveStatus || null,
84
+ stalled: effectiveStatus === "running-suspected-stall",
85
+ currentNodeElapsedMs: running?.durationMs ?? elapsedSince(running?.startedAt, now),
86
+ runElapsedMs: terminal
87
+ ? (runDurationMs ?? elapsedSince(runStartedAt, now))
88
+ : elapsedSince(runStartedAt, now),
89
+ };
90
+ }
91
+ const TERMINAL_RUN_PROGRESS_STATUSES = new Set([
92
+ "finished",
93
+ "failed",
94
+ "partial_failed",
95
+ "superseded",
96
+ "abandoned",
97
+ ]);
98
+ export function formatDurationMs(ms) {
99
+ if (ms == null || !Number.isFinite(ms) || ms < 0)
100
+ return "—";
101
+ const totalSeconds = Math.floor(ms / 1000);
102
+ const hours = Math.floor(totalSeconds / 3600);
103
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
104
+ const seconds = totalSeconds % 60;
105
+ if (hours > 0)
106
+ return `${hours} 时 ${minutes} 分`;
107
+ if (minutes > 0)
108
+ return `${minutes} 分 ${seconds} 秒`;
109
+ return `${seconds} 秒`;
110
+ }
@@ -292,9 +292,13 @@ function assertControllerIdentityUnchangedBeforeSpawn(identity) {
292
292
  }
293
293
  }
294
294
  function controllerIdentityDriftError(message, cause) {
295
- return new Error(`controller identity changed before spawn: ${message}`, {
296
- cause,
297
- });
295
+ // Preserve the raw drift detail verbatim; the actionable suffix serves
296
+ // every consumer surface — operation errorMessage, SSE events, and agent/
297
+ // API callers that never reach the UI translation layer — with the exact
298
+ // recovery step: the running Console holds a stale controller fingerprint
299
+ // after rebuilds or commits touching fingerprint-scoped paths
300
+ // (package.json, bin/**, dist/**, skills/**).
301
+ return new Error(`controller identity changed before spawn: ${message}. Fix: restart the Console process so it re-resolves the current build, then retry the operation; avoid rebuilding or committing fingerprint-scoped files (package.json, bin/**, dist/**, skills/**) mid-session.`, { cause });
298
302
  }
299
303
  /**
300
304
  * Detect whether loopAgentBin is a Node-like executable and baseArgs
@@ -129,11 +129,14 @@ export async function resolveRevisionDigestsForTaskSpec(input) {
129
129
  };
130
130
  }
131
131
  /** Union TaskSpec allowed_paths with workflow-owned write roots. */
132
- export function expandAllowedPathsForWorkflow(workflow, allowedPaths) {
132
+ export function expandAllowedPathsForWorkflow(workflow, allowedPaths, options) {
133
133
  const extras = workflow === "backend-test"
134
134
  ? ["testcase/**", "docs/test-reports/**"]
135
135
  : workflow === "frontend-test"
136
- ? ["testcase/frontend/**", "docs/test-reports/**"]
136
+ ? [
137
+ `${options?.frontendTestRoot ?? "testcase/frontend"}/**`,
138
+ "docs/test-reports/**",
139
+ ]
137
140
  : [];
138
141
  return Array.from(new Set([...allowedPaths, ...extras]));
139
142
  }
@@ -1483,6 +1483,9 @@ function mergeDagRun(existing, incoming) {
1483
1483
  computeDurationMs(startedAt, finishedAt);
1484
1484
  return {
1485
1485
  dagRunId: incoming.dagRunId,
1486
+ ...((incoming.taskId ?? existing.taskId)
1487
+ ? { taskId: incoming.taskId ?? existing.taskId }
1488
+ : {}),
1486
1489
  status: incoming.status ?? existing.status,
1487
1490
  lifecycle: incoming.lifecycle ?? existing.lifecycle,
1488
1491
  executionMode: incoming.executionMode ?? existing.executionMode,
@@ -1914,8 +1917,10 @@ async function parseDagStateFile(statePath, now) {
1914
1917
  }
1915
1918
  : undefined;
1916
1919
  const failureCategory = readString(parsed, "failureCategory");
1920
+ const boundTaskId = await readRunSpecTaskBinding(runSpecPath);
1917
1921
  return {
1918
1922
  dagRunId,
1923
+ ...(boundTaskId ? { taskId: boundTaskId } : {}),
1919
1924
  status,
1920
1925
  ...(lifecycle ? { lifecycle } : {}),
1921
1926
  ...(executionMode ? { executionMode } : {}),
@@ -2435,6 +2440,23 @@ async function safeReadJson(filePath) {
2435
2440
  return undefined;
2436
2441
  return parsed;
2437
2442
  }
2443
+ /** Task binding for a DAG run from run.json — same precedence as
2444
+ * console/resolve-dag-run-for-task (taskContractBinding → sourceBinding →
2445
+ * Hybrid DAG title). */
2446
+ async function readRunSpecTaskBinding(runSpecPath) {
2447
+ const parsed = await safeReadJson(runSpecPath);
2448
+ if (!parsed)
2449
+ return undefined;
2450
+ const binding = parsed.taskContractBinding;
2451
+ const source = parsed.sourceBinding;
2452
+ const fromBinding = (typeof binding?.taskId === "string" && binding.taskId.trim()) ||
2453
+ (typeof source?.taskId === "string" && source.taskId.trim());
2454
+ if (fromBinding)
2455
+ return fromBinding;
2456
+ const title = typeof parsed.title === "string" ? parsed.title.trim() : "";
2457
+ const hybrid = /^Hybrid DAG:\s*(.+)$/i.exec(title);
2458
+ return hybrid?.[1]?.trim() || undefined;
2459
+ }
2438
2460
  function safeParseJson(raw) {
2439
2461
  try {
2440
2462
  const parsed = JSON.parse(raw);