@tea-agent/loop-agent 0.39.0-beta.0 → 0.39.0-beta.10

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 (331) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +248 -95
  3. package/README.md +9 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/advance.js +12 -0
  6. package/dist/application/task-lifecycle/recommendations.js +13 -2
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +23 -5
  9. package/dist/cli/program.js +21 -2
  10. package/dist/cli/update/notifier.js +2 -2
  11. package/dist/cli/update/runtime-activity.js +1 -29
  12. package/dist/cli.js +2 -1
  13. package/dist/commands/client-recovery.js +3 -0
  14. package/dist/commands/dag-follow-up.js +138 -0
  15. package/dist/commands/dag-rerun.js +55 -1
  16. package/dist/commands/init-model-catalog.js +464 -0
  17. package/dist/commands/init-upgrade.js +265 -97
  18. package/dist/commands/init.js +455 -28
  19. package/dist/commands/inspect-next.js +8 -0
  20. package/dist/commands/task-advance.js +19 -0
  21. package/dist/executors/dag-pi-executor.js +2306 -61
  22. package/dist/executors/pi-executor.js +7 -0
  23. package/dist/executors/pi-extension-resolver.js +233 -0
  24. package/dist/executors/pi-playwright-cli-tool.js +74 -28
  25. package/dist/executors/pi-read-budget-policy.js +239 -0
  26. package/dist/executors/pi-sdk-executor.js +180 -33
  27. package/dist/executors/shell-executor.js +1249 -224
  28. package/dist/executors/shell-write-guard.js +14 -0
  29. package/dist/executors/workspace-write-snapshot.js +68 -0
  30. package/dist/infrastructure/harness/atomic-write.js +23 -0
  31. package/dist/shared/dag-failure-category.js +150 -0
  32. package/dist/shared/dag-prompt-override.js +27 -0
  33. package/dist/shared/openspec-spec.js +70 -4
  34. package/dist/shared/operator/capabilities.js +39 -0
  35. package/dist/shared/playwright-cli-command-policy.js +15 -0
  36. package/dist/shared/update/console-notifier.js +100 -0
  37. package/dist/{cli → shared}/update/npm-client.js +70 -15
  38. package/dist/{cli → shared}/update/state.js +41 -13
  39. package/dist/task/config-types.js +50 -5
  40. package/dist/task/contract/apply.js +11 -1
  41. package/dist/task/contract/constants.js +2 -0
  42. package/dist/task/contract/hash.js +3 -0
  43. package/dist/task/contract/observe.js +25 -4
  44. package/dist/task/contract/paths.js +2 -1
  45. package/dist/task/contract/project.js +18 -1
  46. package/dist/task/contract/schema.js +4 -1
  47. package/dist/task/contract/transaction.js +12 -0
  48. package/dist/task/frontend-project-capability.js +199 -18
  49. package/dist/task/source-prepare/completeness.js +188 -0
  50. package/dist/task/source-prepare/fragment-inventory.js +477 -0
  51. package/dist/task/source-prepare/index.js +3 -0
  52. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  53. package/dist/task/source-prepare/ledger-review.js +214 -0
  54. package/dist/task/source-prepare/ledger.js +545 -0
  55. package/dist/task/source-prepare/parse-intent.js +24 -4
  56. package/dist/task/source-prepare/prepare.js +298 -1
  57. package/dist/task/source-prepare/semantic-intake.js +25 -5
  58. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  59. package/dist/task/source-prepare/types.js +22 -0
  60. package/dist/task/source-references.js +22 -1
  61. package/dist/worker/cli.js +18 -2
  62. package/dist/worker/console/chat/chat-event-store.js +62 -4
  63. package/dist/worker/console/chat/context-panel.js +7 -0
  64. package/dist/worker/console/chat/deferred-turn.js +12 -0
  65. package/dist/worker/console/chat/operation-card.js +8 -1
  66. package/dist/worker/console/chat/pi-console-config.js +81 -10
  67. package/dist/worker/console/chat/pi-runtime.js +264 -16
  68. package/dist/worker/console/chat/provider-error.js +98 -0
  69. package/dist/worker/console/chat/routes.js +298 -32
  70. package/dist/worker/console/chat/session-stats.js +179 -0
  71. package/dist/worker/console/chat/session-store.js +93 -63
  72. package/dist/worker/console/chat/todos.js +115 -0
  73. package/dist/worker/console/chat/tool-preview.js +42 -0
  74. package/dist/worker/console/chat/turn-process.js +20 -55
  75. package/dist/worker/console/chat/user-questions.js +266 -0
  76. package/dist/worker/console/chat/workspace-landing.js +14 -0
  77. package/dist/worker/console/console-handoff.js +82 -0
  78. package/dist/worker/console/console-update-and-init.js +115 -0
  79. package/dist/worker/console/console-update-runtime.js +84 -0
  80. package/dist/worker/console/dag-execution-receipt.js +33 -0
  81. package/dist/worker/console/draft-store.js +49 -0
  82. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  83. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  84. package/dist/worker/console/index.js +3 -0
  85. package/dist/worker/console/inspect-split.js +18 -0
  86. package/dist/worker/console/native-directory-picker.js +13 -0
  87. package/dist/worker/console/operation-run-facts.js +19 -3
  88. package/dist/worker/console/operation-store.js +227 -7
  89. package/dist/worker/console/operator-actions.js +388 -0
  90. package/dist/worker/console/operator-surface-health.js +1 -0
  91. package/dist/worker/console/operator-user-error.js +2 -2
  92. package/dist/worker/console/prd-intake-bridge.js +13 -0
  93. package/dist/worker/console/routes.js +355 -3
  94. package/dist/worker/console/security.js +67 -13
  95. package/dist/worker/console/server.js +609 -164
  96. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-BAsjg7ew.js} +1 -1
  97. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-CyIlrwZw.js} +1 -1
  98. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-Bo4tobhH.js} +1 -1
  99. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-Dfpow9gu.js} +1 -1
  100. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-DPmzG_B4.js} +1 -1
  101. package/dist/worker/console/static/assets/channel-DSrJAn0-.js +1 -0
  102. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-pKQpB1UW.js} +1 -1
  103. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-5n9YYquZ.js} +1 -1
  104. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-DP3CtA2a.js} +1 -1
  105. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-DYxMJPUy.js} +1 -1
  106. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-CS3D50Io.js} +1 -1
  107. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-gSdDfo5_.js} +1 -1
  108. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-A48q0ZNt.js} +1 -1
  109. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-ow8Mm3DY.js} +1 -1
  110. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-CMKfLWYE.js} +1 -1
  111. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BiTBdQPX.js} +1 -1
  112. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DeMmPl0_.js +1 -0
  113. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DeMmPl0_.js +1 -0
  114. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-Cm7exRz_.js} +1 -1
  115. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-D9O5qGOu.js} +1 -1
  116. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-CkvQnako.js} +1 -1
  117. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-B1rzp7CB.js} +1 -1
  118. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-BP07Ipcc.js} +1 -1
  119. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-BlPzweQb.js} +1 -1
  120. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-CxPa7n0S.js} +1 -1
  121. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-CMTZs53A.js} +1 -1
  122. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-CxG_X52r.js} +1 -1
  123. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-D8axtJM0.js} +1 -1
  124. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-DIsGzn7b.js} +1 -1
  125. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CU9zkDG0.js} +1 -1
  126. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-DCjTmz5P.js} +1 -1
  127. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-BksdmFzg.js} +1 -1
  128. package/dist/worker/console/static/assets/index-B-O5ChYq.css +1 -0
  129. package/dist/worker/console/static/assets/index-CfnY8AQx.js +407 -0
  130. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-CGzzzJtL.js} +1 -1
  131. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-BdseB80N.js} +1 -1
  132. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-DM9gsgha.js} +1 -1
  133. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-3q-_9R2b.js} +1 -1
  134. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-B8N-Mxsd.js} +1 -1
  135. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-EPIHUUsu.js} +5 -5
  136. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-CxwO12aa.js} +1 -1
  137. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-bPqbVX6Y.js} +1 -1
  138. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-KzqIvzeX.js} +1 -1
  139. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-CyofpWiV.js} +1 -1
  140. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-cTWJvLTP.js} +1 -1
  141. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-BK--q27B.js} +1 -1
  142. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-C0SDOHXK.js} +1 -1
  143. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-Cong6YIo.js} +1 -1
  144. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DUVkgZx8.js} +1 -1
  145. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C7XgxuZg.js} +1 -1
  146. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dzbnd5oX.js +1 -0
  147. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-D9Ayvwcy.js} +2 -2
  148. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BY0hHk6G.js +8 -0
  149. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-Byb000cH.js} +1 -1
  150. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DMLegPt4.js} +1 -1
  151. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-ChMMeOsP.js} +1 -1
  152. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-bEMEPisW.js} +1 -1
  153. package/dist/worker/console/static/index.html +2 -2
  154. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  155. package/dist/worker/console/static-src/app/console-types.js +68 -4
  156. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  157. package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
  158. package/dist/worker/console/static-src/app/useRecoveryConsole.js +45 -10
  159. package/dist/worker/console/static-src/app/useRunProgress.js +47 -0
  160. package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
  161. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  162. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  163. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +140 -8
  164. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  165. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  166. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  167. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  168. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  169. package/dist/worker/console/static-src/operator-chat/turn-submission.js +16 -3
  170. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
  171. package/dist/worker/console/static-src/operator-chat/useChatStream.js +102 -21
  172. package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
  173. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  174. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  175. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  176. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  177. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  178. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  179. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  180. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  181. package/dist/worker/console/static-src/shell/workspace-route.js +337 -0
  182. package/dist/worker/console/workspace-context.js +344 -0
  183. package/dist/worker/console/workspace-registry.js +214 -0
  184. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  185. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  186. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  187. package/dist/worker/observability/init-runtime-activity.js +26 -0
  188. package/dist/worker/observability/read-model.js +22 -0
  189. package/dist/worker/observe/node-input.js +160 -3
  190. package/dist/worker/observe/node-process.js +377 -0
  191. package/dist/worker/observe/routes.js +224 -3
  192. package/dist/worker/observe/server.js +4 -0
  193. package/dist/worker/observe/shell-handler-keys.js +34 -0
  194. package/dist/worker/observe/static/api.js +90 -4
  195. package/dist/worker/observe/static/app.js +18 -70
  196. package/dist/worker/observe/static/constants.js +8 -1
  197. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  198. package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
  199. package/dist/worker/observe/static/dag-node-purpose.js +208 -0
  200. package/dist/worker/observe/static/format.js +60 -0
  201. package/dist/worker/observe/static/index.html +6 -1
  202. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  203. package/dist/worker/observe/static/markdown-render.js +20 -1
  204. package/dist/worker/observe/static/operator-chrome.css +145 -20
  205. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  206. package/dist/worker/observe/static/operator-chrome.js +330 -96
  207. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  208. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  209. package/dist/worker/observe/static/router.d.ts +46 -0
  210. package/dist/worker/observe/static/router.js +25 -1
  211. package/dist/worker/observe/static/state.d.ts +50 -0
  212. package/dist/worker/observe/static/state.js +184 -1
  213. package/dist/worker/observe/static/styles.css +395 -0
  214. package/dist/worker/observe/static/views/dag-graph.js +4 -2
  215. package/dist/worker/observe/static/views/dag-inspector.js +1067 -9
  216. package/dist/worker/observe/static/views/dag.d.ts +13 -0
  217. package/dist/worker/observe/static/views/dag.js +14 -5
  218. package/dist/worker/observe/static/views/dashboard.js +6 -4
  219. package/dist/worker/observe/static/views/failures.js +5 -2
  220. package/dist/worker/observe/static/views/night.js +2 -2
  221. package/dist/worker/observe/static/views/pool.js +1 -0
  222. package/dist/worker/observe/static/views/run.js +3 -1
  223. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  224. package/dist/worker/observe/static/views/task.js +56 -1
  225. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +49 -3
  226. package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
  227. package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
  228. package/dist/workflows/dag/backend-test-writer-completeness.js +10 -5
  229. package/dist/workflows/dag/contract-output-registry.js +15 -0
  230. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  231. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  232. package/dist/workflows/dag/dynamic-runtime/map.js +1 -1
  233. package/dist/workflows/dag/failure-category.js +7 -116
  234. package/dist/workflows/dag/frontend-closeout.js +221 -0
  235. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  236. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  237. package/dist/workflows/dag/frontend-implementation-contract.js +534 -178
  238. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  239. package/dist/workflows/dag/frontend-prewrite-gate.js +371 -393
  240. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  241. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  242. package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
  243. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  244. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  245. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  246. package/dist/workflows/dag/frontend-repair.js +7 -424
  247. package/dist/workflows/dag/frontend-review-context.js +288 -14
  248. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  249. package/dist/workflows/dag/frontend-risk.js +15 -2
  250. package/dist/workflows/dag/frontend-shadow-dual-write.js +814 -0
  251. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  252. package/dist/workflows/dag/frontend-shape-facts.js +409 -0
  253. package/dist/workflows/dag/frontend-shape.js +427 -0
  254. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  255. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  256. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  257. package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
  258. package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
  259. package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
  260. package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
  261. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  262. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  263. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  264. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  265. package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
  266. package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
  267. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  268. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  269. package/dist/workflows/dag/frontend-verification-trace.js +52 -17
  270. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  271. package/dist/workflows/dag/frontend-writer-admission.js +272 -0
  272. package/dist/workflows/dag/frontend-writer-status.js +244 -0
  273. package/dist/workflows/dag/init-hybrid.js +984 -618
  274. package/dist/workflows/dag/interrupt-request.js +10 -3
  275. package/dist/workflows/dag/lifecycle.js +3 -3
  276. package/dist/workflows/dag/node-execution.js +468 -34
  277. package/dist/workflows/dag/prompt.js +44 -19
  278. package/dist/workflows/dag/recovery-lease.js +80 -0
  279. package/dist/workflows/dag/report.js +37 -1
  280. package/dist/workflows/dag/rerun-feedback.js +124 -1
  281. package/dist/workflows/dag/rerun-plan.js +144 -5
  282. package/dist/workflows/dag/rerun-run.js +81 -6
  283. package/dist/workflows/dag/rerun-task.js +29 -0
  284. package/dist/workflows/dag/retry-policy.js +318 -8
  285. package/dist/workflows/dag/runner.js +429 -122
  286. package/dist/workflows/dag/scheduler.js +114 -16
  287. package/dist/workflows/dag/structured-output-repair.js +712 -0
  288. package/dist/workflows/dag/types.js +321 -31
  289. package/dist/workflows/dag/upstream-artifacts.js +0 -4
  290. package/dist/workflows/dag/validate.js +92 -32
  291. package/docs/README.md +2 -0
  292. package/docs/architecture/evolution.md +45 -0
  293. package/docs/init-surface.manifest.json +9 -0
  294. package/docs/operations/README.md +1 -1
  295. package/docs/operations/local-development-environment.md +21 -0
  296. package/docs/templates/README.md +2 -1
  297. package/docs/templates/agent-dag-report.schema.json +8 -2
  298. package/docs/templates/agent-dag.schema.json +45 -1
  299. package/docs/templates/frontend-implementation-contract.schema.json +684 -24
  300. package/docs/templates/frontend-test-case-checklist.md +2 -2
  301. package/docs/templates/frontend-test-dag.json +8 -10
  302. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  303. package/docs/templates/init-managed-agents.md +13 -3
  304. package/docs/templates/spec-registry.schema.json +45 -0
  305. package/harness.json +3 -3
  306. package/package.json +7 -1
  307. package/scripts/next-info.mjs +356 -0
  308. package/scripts/next-publish-gate.mjs +238 -0
  309. package/scripts/release-source-binding.mjs +251 -0
  310. package/skills/codebase-scout/SKILL.md +1 -1
  311. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  312. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  313. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  314. package/skills/frontend-bounded-implement/SKILL.md +8 -14
  315. package/skills/frontend-design-review/SKILL.md +20 -41
  316. package/skills/frontend-implementation/SKILL.md +3 -4
  317. package/skills/frontend-implementation/references/design-spec.md +15 -10
  318. package/skills/frontend-implementation/references/node-contracts.md +23 -13
  319. package/skills/frontend-review/SKILL.md +22 -14
  320. package/skills/frontend-review/references/review-findings.md +6 -7
  321. package/skills/loop-agent/references/command-reference.md +12 -4
  322. package/skills/loop-agent/references/hybrid-dag.md +8 -1
  323. package/skills/playwright-cli/SKILL.md +23 -1
  324. package/skills/playwright-cli-case-generator/SKILL.md +29 -8
  325. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  326. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  327. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  328. package/dist/worker/console/static/assets/index-DTZOKgAn.js +0 -404
  329. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
  330. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  331. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
@@ -1,17 +1,194 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { readFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
4
5
  import { formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "./frontend-worktree-diff.js";
5
6
  import { frontendLintAssessmentArtifactSchema } from "./frontend-lint-baseline.js";
6
- import { frontendImplementationContractSchema } from "./frontend-implementation-contract.js";
7
- import { frontendRepairAssessmentSchema } from "./frontend-repair.js";
7
+ import { deterministicSha256, frontendImplementationContractSchema, } from "./frontend-implementation-contract.js";
8
+ import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
9
+ import { ledgerStatsLine, parseLedgerJson } from "../../task/source-prepare/ledger.js";
8
10
  export const FRONTEND_REVIEW_CONTEXT_SCHEMA_ID = "frontend-review-context-v1";
11
+ /**
12
+ * Deterministically assemble the cumulative diff from a task baseline and the
13
+ * current candidate snapshot. Same input always yields the same output:
14
+ * paths are sorted, statuses are derived from a fixed decision table, and both
15
+ * digests use canonical JSON hashing.
16
+ */
17
+ export function assembleCumulativeDiffContext(input) {
18
+ const baselineByPath = new Map(input.baselineEntries.map((entry) => [entry.path, entry.sha256]));
19
+ const currentByPath = new Map(input.currentEntries.map((entry) => [entry.path, entry.sha256]));
20
+ const allPaths = new Set([
21
+ ...baselineByPath.keys(),
22
+ ...currentByPath.keys(),
23
+ ]);
24
+ const sortedPaths = [...allPaths].sort();
25
+ const perPath = sortedPaths
26
+ .map((p) => {
27
+ const baselineSha256 = baselineByPath.get(p) ?? null;
28
+ const hasCurrent = currentByPath.has(p);
29
+ const currentSha256 = hasCurrent
30
+ ? (currentByPath.get(p) ?? null)
31
+ : null;
32
+ let status;
33
+ if (baselineSha256 === null) {
34
+ status = currentSha256 === null ? "deleted" : "added";
35
+ }
36
+ else if (currentSha256 === null) {
37
+ status = hasCurrent ? "deleted" : "unchanged";
38
+ }
39
+ else {
40
+ status =
41
+ currentSha256 === baselineSha256 ? "unchanged" : "modified";
42
+ }
43
+ return { path: p, status, baselineSha256, currentSha256 };
44
+ })
45
+ // Cumulative diff carries only actual changes. The worktree baseline is
46
+ // the full repository inventory (git ls-files -co), so an unfiltered
47
+ // perPath enumerates thousands of unchanged hashes (e.g. 3000+ entries
48
+ // ≈ 700KB), ballooning the review context and blowing the review node
49
+ // read budget on a single read. Unchanged entries add no diff evidence.
50
+ .filter((entry) => entry.status !== "unchanged");
51
+ const baselineDigest = deterministicSha256([...input.baselineEntries].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
52
+ const cumulativeDigest = deterministicSha256(perPath);
53
+ return {
54
+ schemaId: "frontend-cumulative-diff-context-v1",
55
+ baselineDigest,
56
+ perPath,
57
+ cumulativeDigest,
58
+ empty: perPath.length === 0,
59
+ };
60
+ }
61
+ async function sha256File(filePath) {
62
+ try {
63
+ const bytes = await readFile(filePath);
64
+ return createHash("sha256").update(bytes).digest("hex");
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ /**
71
+ * Capture the cumulative diff from the frozen worktree baseline and the
72
+ * run-owned diff gate output. Pure status/digest logic lives in
73
+ * `assembleCumulativeDiffContext`; this wrapper only does IO.
74
+ */
75
+ export async function captureCumulativeDiffContext(input) {
76
+ const root = path.resolve(input.workspaceRoot);
77
+ let baselineEntries = [];
78
+ try {
79
+ const raw = JSON.parse(await readFile(path.join(input.runDir, "contracts/frontend-worktree-baseline.json"), "utf8"));
80
+ baselineEntries = (raw.files ?? [])
81
+ .map((file) => ({ path: file, sha256: raw.hashes?.[file] ?? null }))
82
+ .filter((entry) => entry.sha256 !== null);
83
+ }
84
+ catch {
85
+ // Missing baseline → empty baseline entries (still deterministic).
86
+ }
87
+ const candidatePaths = [
88
+ ...new Set([...input.diff.changedFiles, ...input.diff.untrackedFiles]),
89
+ ];
90
+ const currentEntries = [];
91
+ for (const file of candidatePaths) {
92
+ currentEntries.push({
93
+ path: file,
94
+ sha256: await sha256File(path.join(root, file)),
95
+ });
96
+ }
97
+ return assembleCumulativeDiffContext({ baselineEntries, currentEntries });
98
+ }
99
+ const TYPED_TO_LEGACY = {
100
+ approve_review: "pass",
101
+ request_review_changes: "request-revision",
102
+ };
103
+ function isTypedReviewTerminalVerdict(value) {
104
+ return value === "approve_review" || value === "request_review_changes";
105
+ }
106
+ function isLegacyReviewVerdict(value) {
107
+ return value === "pass" || value === "request-revision";
108
+ }
109
+ /**
110
+ * Deterministic, fail-closed equivalence between the typed review terminal
111
+ * verdict(s) observed in the attempt and the legacy JSON verdict.
112
+ *
113
+ * Fail-closed cases (all yield `match: false` with an explicit reason):
114
+ * - no typed terminal fact;
115
+ * - more than one typed terminal fact (conflicting);
116
+ * - an unknown typed terminal kind;
117
+ * - a missing/illegal legacy JSON verdict.
118
+ */
119
+ export function compareTypedReviewToLegacyJsonVerdict(input) {
120
+ const kinds = [...input.typedKinds];
121
+ const legacyVerdict = isLegacyReviewVerdict(input.legacyVerdict ?? "")
122
+ ? input.legacyVerdict
123
+ : undefined;
124
+ if (kinds.length === 0) {
125
+ return {
126
+ typedVerdict: undefined,
127
+ legacyVerdict,
128
+ match: false,
129
+ reason: "typed review terminal fact missing",
130
+ };
131
+ }
132
+ if (kinds.length > 1) {
133
+ return {
134
+ typedVerdict: undefined,
135
+ legacyVerdict,
136
+ match: false,
137
+ reason: `conflicting typed review terminal facts: ${[...new Set(kinds)].join(",")}`,
138
+ };
139
+ }
140
+ const typedVerdict = kinds[0];
141
+ if (!isTypedReviewTerminalVerdict(typedVerdict)) {
142
+ return {
143
+ typedVerdict: undefined,
144
+ legacyVerdict,
145
+ match: false,
146
+ reason: `unknown typed review terminal fact kind: ${typedVerdict}`,
147
+ };
148
+ }
149
+ if (!legacyVerdict) {
150
+ return {
151
+ typedVerdict,
152
+ legacyVerdict: undefined,
153
+ match: false,
154
+ reason: "legacy JSON verdict missing or not pass|request-revision",
155
+ };
156
+ }
157
+ const expected = TYPED_TO_LEGACY[typedVerdict];
158
+ return {
159
+ typedVerdict,
160
+ legacyVerdict,
161
+ match: expected === legacyVerdict,
162
+ reason: expected === legacyVerdict
163
+ ? "typed review verdict matches legacy JSON verdict"
164
+ : `typed review verdict ${typedVerdict} maps to ${expected} but legacy JSON verdict is ${legacyVerdict}`,
165
+ };
166
+ }
167
+ /**
168
+ * review-context 稳定失败枚举(AC-011 / AC-HARD-005):v2 drift / manifest 失败返回
169
+ * 结构化、可消费的 failure code(稳定枚举 + reason 的 envelope),既有调用方无需字符串
170
+ * 匹配裸 Error 文本。message 保持人类可读(shell stderr 兼容),code 供机械化路由。
171
+ */
172
+ export const FRONTEND_REVIEW_CONTEXT_FAILURE_CODES = [
173
+ "review-context-invalid-contract",
174
+ "review-context-verification-unbound",
175
+ "review-context-ledger-drift",
176
+ "review-context-manifest-missing",
177
+ ];
178
+ export class FrontendReviewContextFailure extends Error {
179
+ code;
180
+ constructor(code, message, options) {
181
+ super(message, options);
182
+ this.name = "FrontendReviewContextFailure";
183
+ this.code = code;
184
+ }
185
+ }
9
186
  async function readRequiredJson(runDir, relativePath) {
10
187
  try {
11
188
  return JSON.parse(await readFile(path.join(runDir, relativePath), "utf8"));
12
189
  }
13
- catch {
14
- throw new Error(`frontend review context missing or invalid ${relativePath}`);
190
+ catch (error) {
191
+ throw new FrontendReviewContextFailure("review-context-manifest-missing", `frontend review context missing or invalid ${relativePath}: ${error instanceof Error ? error.message : String(error)}`);
15
192
  }
16
193
  }
17
194
  async function readOptionalLintAssessment(runDir) {
@@ -30,10 +207,42 @@ async function readOptionalLintAssessment(runDir) {
30
207
  throw new Error(`frontend review context invalid ${relativePath}`);
31
208
  }
32
209
  }
210
+ async function readOptionalRepairAssessment(runDir) {
211
+ const relativePath = "contracts/frontend-repair-assessment.json";
212
+ try {
213
+ return JSON.parse(await readFile(path.join(runDir, relativePath), "utf8"));
214
+ }
215
+ catch (error) {
216
+ if (error &&
217
+ typeof error === "object" &&
218
+ "code" in error &&
219
+ error.code === "ENOENT") {
220
+ return undefined;
221
+ }
222
+ throw new Error(`frontend review context invalid ${relativePath}`);
223
+ }
224
+ }
225
+ /** M8: verify failure ownership facts written by frontend-verify-shell. */
226
+ async function readOptionalVerifyFailure(runDir) {
227
+ const relativePath = "contracts/frontend-verify-failure.json";
228
+ try {
229
+ return JSON.parse(await readFile(path.join(runDir, relativePath), "utf8"));
230
+ }
231
+ catch (error) {
232
+ if (error &&
233
+ typeof error === "object" &&
234
+ "code" in error &&
235
+ error.code === "ENOENT") {
236
+ return undefined;
237
+ }
238
+ throw new Error(`frontend review context invalid ${relativePath}`);
239
+ }
240
+ }
33
241
  function assertReviewEvidence(input) {
34
242
  const contract = frontendImplementationContractSchema.safeParse(input.contract);
35
- if (!contract.success)
36
- throw new Error("frontend review context invalid implementation contract");
243
+ if (!contract.success) {
244
+ throw new FrontendReviewContextFailure("review-context-invalid-contract", "frontend review context invalid implementation contract");
245
+ }
37
246
  const trace = input.verificationTrace;
38
247
  const expectedTargets = new Map(contract.data.verificationTargets.map((target) => [target.id, target]));
39
248
  const actualTargets = Array.isArray(trace?.targets) ? trace.targets : [];
@@ -69,44 +278,109 @@ function assertReviewEvidence(input) {
69
278
  actualIds.size !== expectedTargets.size ||
70
279
  !mockEvidenceBound ||
71
280
  trace.mockStrategy !== contract.data.mockApi.strategy) {
72
- throw new Error("frontend review context verification trace is not bound to a passing contract");
281
+ throw new FrontendReviewContextFailure("review-context-verification-unbound", "frontend review context verification trace is not bound to a passing contract");
73
282
  }
74
- const assessment = frontendRepairAssessmentSchema.safeParse(input.repairAssessment);
75
- if (!assessment.success)
76
- throw new Error("frontend review context invalid repair assessment");
77
283
  }
78
284
  export async function runFrontendReviewContextGate(input) {
79
285
  const contract = await readRequiredJson(input.runDir, "contracts/frontend-implementation-contract.json");
80
286
  const parsedContract = frontendImplementationContractSchema.safeParse(contract);
81
- if (!parsedContract.success)
82
- throw new Error("frontend review context invalid implementation contract");
287
+ if (!parsedContract.success) {
288
+ throw new FrontendReviewContextFailure("review-context-invalid-contract", "frontend review context invalid implementation contract");
289
+ }
83
290
  const diff = await runFrontendWorktreeDiffGate({
84
291
  ...input,
85
292
  authorizedChangedPaths: parsedContract.data.targets.files,
86
293
  });
87
294
  const verificationTrace = await readRequiredJson(input.runDir, "contracts/frontend-verification-trace.json");
88
- const repairAssessment = await readRequiredJson(input.runDir, "contracts/frontend-repair-assessment.json");
295
+ const repairAssessment = await readOptionalRepairAssessment(input.runDir);
296
+ const verifyFailure = await readOptionalVerifyFailure(input.runDir);
89
297
  const lintAssessment = await readOptionalLintAssessment(input.runDir);
298
+ const cumulativeDiff = await captureCumulativeDiffContext({
299
+ runDir: input.runDir,
300
+ workspaceRoot: input.workspaceRoot,
301
+ diff,
302
+ });
90
303
  assertReviewEvidence({ contract, verificationTrace, repairAssessment });
304
+ // Source fidelity ledger 核对(AC-008 / AC-FIDELITY-008):ledger 缺失/漂移即阻断;
305
+ // 同时把核对清单注入 review-context 供最终 reviewer 机械化核对。
306
+ const sourceFidelity = await buildReviewSourceFidelityChecklist(input, parsedContract.data);
91
307
  const payload = {
92
308
  schemaVersion: 1,
93
309
  schemaId: FRONTEND_REVIEW_CONTEXT_SCHEMA_ID,
94
310
  contract,
95
311
  verificationTrace,
96
- repairAssessment,
312
+ ...(repairAssessment ? { repairAssessment } : {}),
313
+ ...(verifyFailure ? { verifyFailure } : {}),
97
314
  ...(lintAssessment ? { lintAssessment } : {}),
315
+ sourceFidelity,
98
316
  diff: {
99
317
  schemaId: diff.schemaId,
100
318
  patchPath: diff.patchPath,
101
319
  patchSha256: diff.patchSha256,
320
+ reviewSummaryPath: diff.reviewSummaryPath,
321
+ reviewIndexPath: diff.reviewIndexPath,
102
322
  changedFiles: diff.changedFiles,
103
323
  untrackedFiles: diff.untrackedFiles,
104
324
  empty: diff.empty,
105
325
  },
326
+ cumulativeDiff,
106
327
  };
107
328
  const artifactPath = await writeDagRunJsonArtifact(input.runDir, "contracts/frontend-review-context.json", payload);
108
329
  return { ok: true, artifactPath, diff };
109
330
  }
331
+ /**
332
+ * 最终 review 核对清单(机械化):ledger 缺失/漂移、canonical requirement 漏项、
333
+ * diff 越界、验证证据缺失均阻断 pass。清单以有界安全摘要注入提示词,不泄漏原文。
334
+ */
335
+ async function buildReviewSourceFidelityChecklist(input, contract) {
336
+ const binding = contract.sourceBinding;
337
+ const checklist = [];
338
+ // 契约级 sourceBinding 的 v2 语义:四个 ledger 字段必须齐全(缺失即 v1 遗留绑定)。
339
+ const boundV2 = Boolean(binding.ledgerPath) &&
340
+ Boolean(binding.ledgerSha256) &&
341
+ Boolean(binding.inputDigest) &&
342
+ binding.requirementToFragments !== undefined;
343
+ if (!boundV2) {
344
+ return {
345
+ bound: false,
346
+ ledger: null,
347
+ checklist: [
348
+ "source fidelity ledger: not bound (v1 binding); 按现有语义评审",
349
+ "canonical contract 与实际 diff 逐项核对,验证证据齐全后 verdict 可为 pass",
350
+ ],
351
+ };
352
+ }
353
+ const absolute = resolveDagTaskSourcePath({
354
+ workspaceRoot: input.workspaceRoot,
355
+ taskId: contract.sourceBinding.taskId,
356
+ sourcePath: binding.ledgerPath,
357
+ });
358
+ let ledger = null;
359
+ try {
360
+ const raw = await readFile(absolute, "utf8");
361
+ const actualSha = createHash("sha256").update(raw).digest("hex");
362
+ if (actualSha !== binding.ledgerSha256) {
363
+ throw new Error(`ledger sha256 mismatch (bound ${binding.ledgerSha256}, observed ${actualSha})`);
364
+ }
365
+ ledger = parseLedgerJson(raw);
366
+ }
367
+ catch (error) {
368
+ // AC-011 / AC-HARD-005:v2 drift 返回结构化 failure code(稳定枚举 + reason),
369
+ // 不再裸 throw new Error;message 保持人类可读供 shell stderr。
370
+ throw new FrontendReviewContextFailure("review-context-ledger-drift", `frontend review context source fidelity ledger unavailable or drifted: ${error instanceof Error ? error.message : String(error)}`);
371
+ }
372
+ checklist.push("ledger: 存在且绑定 sha256 一致(无缺失/漂移)", `ledger 覆盖统计: ${ledgerStatsLine(ledger)}`, "canonical contract requirements 必须覆盖 ledger 全部 canonical requirement IDs", "实际 diff 必须限制在契约 targets 内(越界即阻断 pass)", "验证证据必须绑定契约 verificationTargets(缺失即阻断 pass)", "仅满足派生摘要而遗漏 ledger requirement 时 verdict 不得为 pass");
373
+ return {
374
+ bound: true,
375
+ ledger: {
376
+ path: binding.ledgerPath,
377
+ sha256: binding.ledgerSha256,
378
+ inputDigest: binding.inputDigest,
379
+ stats: ledgerStatsLine(ledger),
380
+ },
381
+ checklist,
382
+ };
383
+ }
110
384
  export function formatFrontendReviewContextStdout(result) {
111
385
  return [
112
386
  "Frontend review context: pass",
@@ -0,0 +1,270 @@
1
+ import path from "node:path";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { readTypedEventStoreFromJsonl, } from "./frontend-typed-event-store.js";
4
+ /**
5
+ * Frontend review findings → downstream work (direction A).
6
+ *
7
+ * When `frontend-review-pi` commits `request_review_changes`, the implementation
8
+ * must not silently finish: the run becomes "not passed" (partial_failed +
9
+ * terminalReason=review-request-changes), and the committed findings are
10
+ * rendered into a downstream-task PRD draft that a later DAG round can consume
11
+ * instead of repeating the previous flow. Rendering is deterministic (pure);
12
+ * only the JSONL read and PRD write are IO.
13
+ *
14
+ * The design-review analog (`request_design_changes`) reuses the same PRD
15
+ * rendering and typed-fact channel: a design rejection is a definite finding
16
+ * (not a protocol/environment error), so it auto-re-enters the next plan round
17
+ * with the committed design findings instead of terminating the run.
18
+ */
19
+ export const FRONTEND_REVIEW_FINDINGS_PRD_REL = "contracts/frontend-review-findings-prd.md";
20
+ /** Review node id whose committed typed facts drive the findings PRD. */
21
+ export const FRONTEND_REVIEW_NODE_ID = "frontend-review-pi";
22
+ /** Design review node id whose committed typed facts drive the findings PRD. */
23
+ export const FRONTEND_DESIGN_REVIEW_NODE_ID = "frontend-design-review-pi";
24
+ /** Deterministic issueCategory → failure owner mapping (A+B AC-009 §4.10). */
25
+ export function classifyReviewIssueCategoryFailureOwner(issueCategory) {
26
+ switch (issueCategory) {
27
+ case "implementation-mismatch":
28
+ return "implementation";
29
+ case "approved-design-defect":
30
+ return "plan";
31
+ case "target-surface-defect":
32
+ return "scout";
33
+ case "contract-requirement-gap":
34
+ return "contract";
35
+ case "unknown":
36
+ default:
37
+ return "unknown";
38
+ }
39
+ }
40
+ const API_SIGNAL_RE = /\b(api|fetch|axios|graphql|endpoint|远程|接口)\b/i;
41
+ /** Negations for the API signal. CJK "无 API/无接口" has no \b boundary, so
42
+ * it is matched separately from the ASCII no api/not-needed forms. */
43
+ const API_NEGATION_RE = /(?:^|[^\w])(?:no\s+api|not-needed)(?:$|[^\w])|无\s*api|无\s*接口/i;
44
+ const SMALL_SIGNAL_WORDS = [
45
+ "单文件局部修复",
46
+ "局部样式",
47
+ "文案调整",
48
+ "无远程接口",
49
+ ];
50
+ function topLevelDir(p) {
51
+ const normalized = p.replace(/\\/g, "/");
52
+ const first = normalized.split("/").filter(Boolean)[0] ?? "";
53
+ return first;
54
+ }
55
+ /** Collect unique non-empty finding `file` values (sorted, de-duplicated). */
56
+ function findingTargetFiles(fact) {
57
+ const files = new Set();
58
+ for (const finding of fact.findings) {
59
+ if (typeof finding.file === "string" && finding.file.trim()) {
60
+ files.add(finding.file.trim());
61
+ }
62
+ }
63
+ return [...files].sort();
64
+ }
65
+ /**
66
+ * Deterministically derive the fix scope for the downstream task. Same fact
67
+ * always yields the same scope. The shape mirrors `resolveFrontendTaskShape`
68
+ * signals (concentrated paths, no remote API → micro/small) so that when the
69
+ * PRD draft is used as the next task's requirement, the frontend DAG generator
70
+ * routes a small fix to micro/small instead of the full 11-node standard
71
+ * topology.
72
+ */
73
+ export function deriveFrontendReviewFixScope(fact) {
74
+ const targetFiles = findingTargetFiles(fact);
75
+ const blob = fact.findings
76
+ .map((f) => `${f.issue} ${f.requiredChange ?? ""} ${f.file ?? ""}`)
77
+ .join("\n");
78
+ const hasApiSignal = API_SIGNAL_RE.test(blob) && !API_NEGATION_RE.test(blob);
79
+ const topDirs = new Set(targetFiles.map(topLevelDir));
80
+ const concentrated = targetFiles.length > 0 && topDirs.size <= 1 && targetFiles.length <= 4;
81
+ let shape;
82
+ let complexity;
83
+ let reason;
84
+ if (targetFiles.length === 0) {
85
+ shape = "standard";
86
+ complexity = "medium";
87
+ reason =
88
+ "findings carry no file references; scope unknown, keep standard topology";
89
+ }
90
+ else if (hasApiSignal) {
91
+ shape = "standard";
92
+ complexity = "medium";
93
+ reason = `findings reference a remote API signal; standard topology required (files=${targetFiles.length})`;
94
+ }
95
+ else if (concentrated && targetFiles.length <= 1) {
96
+ shape = "micro";
97
+ complexity = "trivial";
98
+ reason = "single-file concentrated fix without remote API";
99
+ }
100
+ else if (concentrated) {
101
+ shape = "small";
102
+ complexity = "low";
103
+ reason = `concentrated fix across ${targetFiles.length} file(s) in one top-level directory`;
104
+ }
105
+ else {
106
+ shape = "standard";
107
+ complexity = "medium";
108
+ reason = `fix spans ${targetFiles.length} file(s) across ${topDirs.size} top-level directories; standard topology`;
109
+ }
110
+ const allowedPaths = [
111
+ ...new Set([
112
+ ...targetFiles,
113
+ ...targetFiles.map((f) => `${topLevelDir(f)}/**`),
114
+ ]),
115
+ ].sort();
116
+ return {
117
+ shape,
118
+ complexity,
119
+ targetFiles,
120
+ allowedPaths,
121
+ smallSignals: [...SMALL_SIGNAL_WORDS],
122
+ hasApiSignal,
123
+ reason,
124
+ };
125
+ }
126
+ /**
127
+ * Read the committed `request_review_changes` fact from the review node's typed
128
+ * event store. Returns undefined when no committed request fact exists (e.g.
129
+ * approve_review, missing store, or an unreadable store).
130
+ */
131
+ export async function readCommittedReviewRequestFact(input) {
132
+ const { runDir: reviewRunDir } = input;
133
+ const reviewNodeId = input.reviewNodeId ?? FRONTEND_REVIEW_NODE_ID;
134
+ return readCommittedRequestFactByKind({
135
+ runDir: reviewRunDir,
136
+ nodeId: reviewNodeId,
137
+ factsFile: "review-typed-facts.jsonl",
138
+ kind: "request_review_changes",
139
+ });
140
+ }
141
+ /**
142
+ * Read the committed `request_design_changes` fact from the design review
143
+ * node's typed event store. Returns undefined when no committed request fact
144
+ * exists (e.g. approve_design, missing store, or an unreadable store).
145
+ */
146
+ export async function readCommittedDesignRequestFact(input) {
147
+ const { runDir: designRunDir } = input;
148
+ const designNodeId = input.designNodeId ?? FRONTEND_DESIGN_REVIEW_NODE_ID;
149
+ return readCommittedRequestFactByKind({
150
+ runDir: designRunDir,
151
+ nodeId: designNodeId,
152
+ factsFile: "design-typed-facts.jsonl",
153
+ kind: "request_design_changes",
154
+ });
155
+ }
156
+ async function readCommittedRequestFactByKind(input) {
157
+ let records;
158
+ try {
159
+ records = await readTypedEventStoreFromJsonl(path.join(input.runDir, input.nodeId, input.factsFile));
160
+ }
161
+ catch {
162
+ return undefined;
163
+ }
164
+ const committed = records.filter((record) => record.phase === "committed");
165
+ if (committed.length === 0)
166
+ return undefined;
167
+ const request = committed.find((record) => record.fact.kind === input.kind);
168
+ if (!request)
169
+ return undefined;
170
+ const fact = request.fact;
171
+ if (typeof fact !== "object" || fact === null)
172
+ return undefined;
173
+ return fact;
174
+ }
175
+ function bulletList(items) {
176
+ if (!items || items.length === 0)
177
+ return "_(none)_";
178
+ return items.map((item) => `- ${item}`).join("\n");
179
+ }
180
+ function findingLines(findings) {
181
+ if (findings.length === 0)
182
+ return "_(none)_";
183
+ return findings
184
+ .map((finding) => {
185
+ const location = [finding.file, finding.line]
186
+ .filter((value) => value !== undefined)
187
+ .join(":");
188
+ const prefix = location ? `[${location}] ` : "";
189
+ const requiredChange = finding.requiredChange
190
+ ? `\n - requiredChange: ${finding.requiredChange}`
191
+ : "";
192
+ return `- ${prefix}${finding.severity}: ${finding.issue}${requiredChange}`;
193
+ })
194
+ .join("\n");
195
+ }
196
+ /**
197
+ * Deterministically render a downstream-task PRD draft from a committed
198
+ * `request_review_changes` / `request_design_changes` fact. Same fact always
199
+ * renders byte-identical output; the parent run id and review node id are the
200
+ * only context.
201
+ */
202
+ export function renderFrontendReviewFindingsPrd(input) {
203
+ const { parentRunId, fact } = input;
204
+ const isDesign = fact.kind === "request_design_changes";
205
+ const reviewNodeId = input.reviewNodeId ??
206
+ (isDesign ? FRONTEND_DESIGN_REVIEW_NODE_ID : FRONTEND_REVIEW_NODE_ID);
207
+ const restartPhase = classifyReviewIssueCategoryFailureOwner(fact.issueCategory);
208
+ const scope = deriveFrontendReviewFixScope(fact);
209
+ const targetSummary = scope.targetFiles.length > 0
210
+ ? scope.targetFiles.join(", ")
211
+ : fact.issueCategory;
212
+ return [
213
+ `# 修复 ${isDesign ? "设计" : "review"} 发现(${scope.shape}):${targetSummary}`,
214
+ "",
215
+ "## 背景",
216
+ "",
217
+ `- parentRunId: \`${parentRunId}\``,
218
+ `- reviewNodeId: \`${reviewNodeId}\``,
219
+ `- typedVerdict: \`${fact.kind}\``,
220
+ `- issueCategory: \`${fact.issueCategory}\``,
221
+ `- restartPhase/failureOwner: \`${restartPhase}\``,
222
+ "",
223
+ "## 修复范围(供下一轮任务生成路由)",
224
+ "",
225
+ `- 建议 shape:\`${scope.shape}\`(micro=7 / small=10 / standard=11 节点)`,
226
+ `- 建议 complexity:\`${scope.complexity}\``,
227
+ `- 推导依据:${scope.reason}`,
228
+ ...(scope.hasApiSignal ? ["- ⚠️ findings 含远程 API/接口信号,走 standard 拓扑"] : []),
229
+ scope.targetFiles.length > 0
230
+ ? [`- 目标文件:${scope.targetFiles.map((f) => `\`${f}\``).join(", ")}`]
231
+ : ["- 目标文件:_(findings 未提供 file 引用,需人工补全)_"],
232
+ scope.allowedPaths.length > 0
233
+ ? [`- 建议 allowedPaths:${scope.allowedPaths.map((p) => `\`${p}\``).join(", ")}`]
234
+ : [],
235
+ scope.shape === "micro" || scope.shape === "small"
236
+ ? [`- 范围信号:${scope.smallSignals.join("、")}(供 shape 分类器识别,避免退化为全节点流程)`]
237
+ : [],
238
+ "",
239
+ "## 问题清单",
240
+ "",
241
+ findingLines(fact.findings),
242
+ "",
243
+ "## 证据引用",
244
+ "",
245
+ bulletList(fact.evidenceRefs),
246
+ "",
247
+ "## 修复目标",
248
+ "",
249
+ `- 按 issueCategory 映射的重启阶段(${restartPhase})定点修复,不复用上一轮未通过的实现。`,
250
+ "- 逐条关闭上面「问题清单」中的 findings,并保留上一轮已通过的验证目标继续有效。",
251
+ `- 完成修复后重新走 frontend DAG 的 ${isDesign ? "plan → design" : "implement → verify → review"} 链,直到${isDesign ? " design-review 提交 approve_design" : " review 提交 approve_review"}。`,
252
+ "",
253
+ ].join("\n");
254
+ }
255
+ /** Write the findings PRD draft under the run dir (run-owned artifact). */
256
+ export async function writeFrontendReviewFindingsPrd(input) {
257
+ const markdown = renderFrontendReviewFindingsPrd(input);
258
+ const abs = path.join(input.runDir, FRONTEND_REVIEW_FINDINGS_PRD_REL);
259
+ await mkdir(path.dirname(abs), { recursive: true });
260
+ await writeFile(abs, markdown, "utf8");
261
+ return FRONTEND_REVIEW_FINDINGS_PRD_REL;
262
+ }
263
+ export async function readFrontendReviewFindingsPrd(runDir) {
264
+ try {
265
+ return await readFile(path.join(runDir, FRONTEND_REVIEW_FINDINGS_PRD_REL), "utf8");
266
+ }
267
+ catch {
268
+ return undefined;
269
+ }
270
+ }
@@ -2,6 +2,12 @@
2
2
  * Deterministic frontend-implementation risk classifier (M4).
3
3
  * Pure function: no I/O, no model, no side effects.
4
4
  */
5
+ /**
6
+ * Dependency-installation language in task text. Mentioning package.json or
7
+ * allowing its path is NOT dependency introduction by itself; the manifest
8
+ * path check in classifyFrontendRisk requires this language to escalate.
9
+ */
10
+ const DEPENDENCY_INSTALL_LANGUAGE = /new\s+dependency|add\s+dependencies?|install\s+dependencies?|npm\s+(?:install|i)\b|yarn\s+(?:add|install)\b|pnpm\s+(?:add|install)\b|bun\s+(?:add|install)\b|新增依赖|添加依赖|安装依赖|引入依赖/i;
5
11
  const HIGH_RISK_PATTERNS = [
6
12
  {
7
13
  id: "new-route",
@@ -25,7 +31,7 @@ const HIGH_RISK_PATTERNS = [
25
31
  },
26
32
  {
27
33
  id: "new-dependency",
28
- re: /new\s+dependency|add\s+dependency|npm\s+install|新增依赖|package\.json/i,
34
+ re: DEPENDENCY_INSTALL_LANGUAGE,
29
35
  },
30
36
  {
31
37
  id: "schema-api-change",
@@ -88,7 +94,14 @@ export function classifyFrontendRisk(input) {
88
94
  const paths = input.allowedPaths ?? [];
89
95
  const spread = pathSpreadScore(paths);
90
96
  if (paths.some((p) => /package\.json|pnpm-lock|yarn\.lock|package-lock/.test(p))) {
91
- signals.push("new-dependency");
97
+ if (DEPENDENCY_INSTALL_LANGUAGE.test(blob)) {
98
+ signals.push("new-dependency");
99
+ }
100
+ else {
101
+ // Editing manifest scripts/config is not dependency introduction;
102
+ // record the rejected escalation instead of forcing full gates.
103
+ rejectedSignals.push("new-dependency:manifest-path-without-install-language");
104
+ }
92
105
  }
93
106
  // Explicit multi-domain language only — path count alone is not high-risk
94
107
  if (spread >= 3 &&