@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
@@ -0,0 +1,8 @@
1
+ import { collectNextInfo, formatNextInfo, } from "../../scripts/next-info.mjs";
2
+ export async function runInspectNext(repoRoot, args = []) {
3
+ const json = args.includes("--json");
4
+ const result = collectNextInfo({ cwd: repoRoot });
5
+ process.stdout.write(formatNextInfo(result, { json }));
6
+ if (!result.ok)
7
+ process.exitCode = result.exitCode || 1;
8
+ }
@@ -1,4 +1,5 @@
1
1
  import { advanceTaskLifecycle, } from "../application/task-lifecycle/index.js";
2
+ import { backendTestConfigSchema, } from "../task/config-types.js";
2
3
  import { buildOperatorResult, operatorFailed, processExitCodeForOutcome, writeOperatorJson, } from "../shared/operator/index.js";
3
4
  import { createRunDagProgressObserver, validateRunDagProgressIntervalMs, } from "./run-dag-progress.js";
4
5
  const COMMAND = "task advance";
@@ -14,6 +15,7 @@ const USAGE = `usage:
14
15
  [--forbidden-path <glob>]...
15
16
  [--verify <label:command>]...
16
17
  [--verify-timeout <label:timeoutMs>]...
18
+ [--backend-test <json>]
17
19
  [--accept-recommendations <digest>]
18
20
  [--approve-gate <gate-id:digest>]
19
21
  [--reject-gate <gate-id:digest> --reason <text>]
@@ -52,6 +54,30 @@ function parseVerifyTimeout(flag) {
52
54
  }
53
55
  return { label: flag.slice(0, idx), timeoutMs };
54
56
  }
57
+ /**
58
+ * Parse `--backend-test <json>` into a validated managed backendTest config.
59
+ * `{}` is legal (explicit clear back to the default layout).
60
+ */
61
+ function parseBackendTestFlag(raw) {
62
+ let parsed;
63
+ try {
64
+ parsed = JSON.parse(raw);
65
+ }
66
+ catch {
67
+ throw new Error(`invalid --backend-test ${raw}; expected an inline JSON object`);
68
+ }
69
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
70
+ throw new Error(`invalid --backend-test ${raw}; expected an inline JSON object`);
71
+ }
72
+ const result = backendTestConfigSchema.safeParse(parsed);
73
+ if (!result.success) {
74
+ const summary = result.error.issues
75
+ .map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`)
76
+ .join("; ");
77
+ throw new Error(`invalid --backend-test: ${summary}`);
78
+ }
79
+ return result.data ?? {};
80
+ }
55
81
  export function parseTaskAdvanceArgs(args) {
56
82
  const options = {};
57
83
  const positional = [];
@@ -113,6 +139,10 @@ export function parseTaskAdvanceArgs(args) {
113
139
  options.verifyTimeout = pushList(options.verifyTimeout, next());
114
140
  continue;
115
141
  }
142
+ if (token === "--backend-test") {
143
+ options.backendTest = parseBackendTestFlag(next());
144
+ continue;
145
+ }
116
146
  if (token === "--accept-recommendations") {
117
147
  options.acceptRecommendations = next();
118
148
  continue;
@@ -273,6 +303,7 @@ export function toUseCaseInput(repoRoot, taskId, options, observer) {
273
303
  allowedPaths: options.allowedPaths,
274
304
  forbiddenPaths: options.forbiddenPaths,
275
305
  verifyCommands: verifyCommands.length > 0 ? verifyCommands : undefined,
306
+ backendTest: options.backendTest,
276
307
  approveGateToken: options.approveGate,
277
308
  rejectGateToken: options.rejectGate,
278
309
  rejectReason: options.reason,
@@ -2,14 +2,17 @@ import path from "node:path";
2
2
  import { createHash } from "node:crypto";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { writeDagNodeJsonArtifact, writeTextArtifactFile, } from "../infrastructure/harness/artifact-store.js";
5
- import { executePiStep, } from "./pi-executor.js";
5
+ import { executePiStep, resolvePiBackend, } from "./pi-executor.js";
6
+ import { resolveDagPiExtensions, } from "./pi-extension-resolver.js";
6
7
  import { buildPiWriterToolPolicyContext, createPiWriterCustomTools, } from "./pi-writer-tool-policy.js";
7
8
  import { cleanupPlaywrightCliDefaultSession, createPlaywrightCliTool, PI_COMMAND_CAPABILITY_REGISTRY, resolveCaseIdFromWriteSet, resolveEvidenceDirFromWriteSet, } from "./pi-playwright-cli-tool.js";
8
9
  import { dagCommandPolicyAllows, resolveDagCommandPolicy, } from "../workflows/dag/types.js";
9
10
  import { redactPromptForLog, truncateOutput, } from "../shared/output-truncation.js";
10
11
  import { GitStatusUnavailableError, pathsChangedDuringRun, readGitStatusPorcelain, recoverRootNulArtifact, snapshotGitStatusPathFingerprints, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
11
- import { isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
12
+ import { isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
12
13
  import { assessBackendTestMdPlanCompleteness, assessBackendTestMdWriterCompleteness, assessBackendTestPytestPlanCompleteness, assessBackendTestPytestWriterCompleteness, assessBackendTestShardChildCompleteness, backendTestWriterProgressRoleForTask, classifyBackendTestWriterCompletenessFailure, isBackendTestCompletenessRetryCandidate, isBackendTestMdPlanTask, isBackendTestPytestPlanTask, isBackendTestShardChildTask, writeBackendTestWriterProgressArtifacts, } from "../workflows/dag/backend-test-writer-completeness.js";
14
+ import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
15
+ import { frontendTestLayoutFromSpec, } from "../workflows/dag/frontend-test-layout.js";
13
16
  import { redactSecrets, truncateUtf8Preview } from "../shared/preview.js";
14
17
  /**
15
18
  * Writer classification for a length-stopped thinking-only attempt: the model
@@ -326,6 +329,36 @@ export async function writePiExecutorArtifacts(artifactsDir, input) {
326
329
  await writeTextArtifactFile(summaryPath, buildPiResultSummaryMarkdown(input));
327
330
  return { promptPath, summaryPath };
328
331
  }
332
+ /**
333
+ * Run-owned per-node Pi extension facts (plan 2026-08-21): requested ids,
334
+ * resolved packages (with absolute entry paths), and degraded/missing ids.
335
+ * Written only for nodes that declare `piExtensions`; never into the repo-level
336
+ * DAG template. Best-effort: artifact failures never fail the node.
337
+ */
338
+ async function writePiExtensionsArtifact(nodeArtifactsDir, requested, resolved, missing) {
339
+ try {
340
+ const runDir = path.dirname(nodeArtifactsDir);
341
+ const nodeId = path.basename(nodeArtifactsDir);
342
+ await writeDagNodeJsonArtifact(runDir, nodeId, "pi-extensions.json", {
343
+ schemaVersion: 1,
344
+ requested: [...requested],
345
+ resolved: resolved.map((entry) => ({
346
+ id: entry.id,
347
+ packageName: entry.packageName,
348
+ version: entry.version,
349
+ path: entry.entryPaths.join(", "),
350
+ })),
351
+ missing: missing.map((entry) => ({
352
+ id: entry.id,
353
+ reason: entry.reason,
354
+ ...(entry.detail ? { detail: entry.detail } : {}),
355
+ })),
356
+ });
357
+ }
358
+ catch {
359
+ // best-effort run fact; node execution continues
360
+ }
361
+ }
329
362
  async function resolveValidatedFrontendBaseUrlFromContext(workspaceRoot, runDir) {
330
363
  try {
331
364
  const capability = JSON.parse(await readFile(path.join(runDir, "preflight-frontend-browser-tool-shell", "frontend-browser-capability.json"), "utf8"));
@@ -480,10 +513,11 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
480
513
  throw new Error(`unknown command capability: ${capability}`);
481
514
  }
482
515
  if (capability === "playwright-cli") {
483
- const caseId = resolveCaseIdFromWriteSet(input.task.writeSet) ??
516
+ const layout = frontendTestLayoutFromSpec(meta.spec);
517
+ const caseId = resolveCaseIdFromWriteSet(input.task.writeSet, layout) ??
484
518
  input.task.id;
485
- const evidenceDir = resolveEvidenceDirFromWriteSet(input.task.writeSet) ??
486
- `testcase/frontend/evidence/${caseId}`;
519
+ const evidenceDir = resolveEvidenceDirFromWriteSet(input.task.writeSet, layout) ??
520
+ `${layout.evidenceDir}/${caseId}`;
487
521
  playwrightToolContext = {
488
522
  repoRoot: input.cwd,
489
523
  runDir: meta.runDir,
@@ -491,7 +525,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
491
525
  caseId,
492
526
  evidenceDir,
493
527
  baseUrl: await resolveValidatedFrontendBaseUrlFromContext(input.cwd, meta.runDir),
494
- inputRoot: "testcase/frontend/fixtures",
528
+ inputRoot: layout.fixturesDir,
529
+ layout,
495
530
  };
496
531
  // Cleanup must be confirmed before a new case; otherwise default-session
497
532
  // isolation is unknown and no Pi invocation is permitted.
@@ -549,6 +584,29 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
549
584
  };
550
585
  }
551
586
  }
587
+ // Plan 2026-08-21 D4/D6: resolve the node's explicit Pi extension
588
+ // allowlist against the settings inventory. Degrade on missing packages —
589
+ // never fail the node — and persist requested/resolved/missing facts.
590
+ const piExtensionsResolution = input.task.piExtensions
591
+ ? resolveDagPiExtensions({
592
+ repoRoot: input.cwd,
593
+ ids: input.task.piExtensions,
594
+ })
595
+ : undefined;
596
+ if (piExtensionsResolution) {
597
+ const piBackend = resolvePiBackend();
598
+ const cliDegraded = piBackend === "cli-only";
599
+ await writePiExtensionsArtifact(path.join(meta.runDir, input.task.id), input.task.piExtensions, cliDegraded ? [] : piExtensionsResolution.resolved, cliDegraded
600
+ ? input.task.piExtensions.map((id) => ({
601
+ id,
602
+ reason: "not-installed",
603
+ detail: "cli backend does not load pi extensions (sdk-only v1)",
604
+ }))
605
+ : piExtensionsResolution.missing);
606
+ }
607
+ const piExtensionPaths = piExtensionsResolution && resolvePiBackend() !== "cli-only"
608
+ ? piExtensionsResolution.resolved.flatMap((entry) => entry.entryPaths)
609
+ : undefined;
552
610
  result = await piStepFn({
553
611
  attachedFiles: [],
554
612
  modelConfig: resolveDagPiModelConfig(input.model, input.thinking ? { thinking: input.thinking } : undefined),
@@ -563,6 +621,9 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
563
621
  stallTimeoutMs: input.stallTimeoutMs,
564
622
  abortGraceMs: input.abortGraceMs,
565
623
  ...(writerToolPolicy ? { writerToolPolicy } : {}),
624
+ ...(piExtensionPaths && piExtensionPaths.length > 0
625
+ ? { piExtensionPaths }
626
+ : {}),
566
627
  onActivity: bridgeActivity
567
628
  ? (activity) => {
568
629
  if (activity.kind === "lease" ||
@@ -808,6 +869,13 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
808
869
  isBackendTestCompletenessRetryCandidate(input.task)) {
809
870
  try {
810
871
  const writerRole = backendTestWriterProgressRoleForTask(input.task.id);
872
+ const backendLayout = resolveBackendTestLayout(meta.spec.backendTestLayout
873
+ ? {
874
+ testRoot: meta.spec.backendTestLayout.testRoot,
875
+ markdownRel: meta.spec.backendTestLayout.markdownRel,
876
+ scriptRel: meta.spec.backendTestLayout.scriptRel,
877
+ }
878
+ : undefined);
811
879
  const progress = isBackendTestShardChildTask(input.task)
812
880
  ? await assessBackendTestShardChildCompleteness({
813
881
  workspaceRoot: input.cwd,
@@ -815,12 +883,12 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
815
883
  writeSet: input.task.writeSet ?? [],
816
884
  })
817
885
  : isBackendTestMdPlanTask(input.task)
818
- ? await assessBackendTestMdPlanCompleteness(input.cwd)
886
+ ? await assessBackendTestMdPlanCompleteness(input.cwd, backendLayout)
819
887
  : isBackendTestPytestPlanTask(input.task)
820
- ? await assessBackendTestPytestPlanCompleteness(input.cwd)
888
+ ? await assessBackendTestPytestPlanCompleteness(input.cwd, backendLayout)
821
889
  : writerRole === "pytest-generate"
822
- ? await assessBackendTestPytestWriterCompleteness(input.cwd)
823
- : await assessBackendTestMdWriterCompleteness(input.cwd);
890
+ ? await assessBackendTestPytestWriterCompleteness(input.cwd, backendLayout)
891
+ : await assessBackendTestMdWriterCompleteness(input.cwd, backendLayout);
824
892
  await writeBackendTestWriterProgressArtifacts({
825
893
  runDir: meta.runDir,
826
894
  progress,
@@ -877,22 +945,31 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
877
945
  if (meta.writeGuardAttribution === "best-effort") {
878
946
  stderrParts.push("write guard note: concurrent rank writers use best-effort per-node attribution; keep same-rank writeSet entries disjoint");
879
947
  }
880
- // Pure provider stall/timeout with zero workspace side effects is eligible
881
- // for a single writer transport retry (implement-pi flake resilience).
882
- // Partial writes, tool calls, or write-guard failures keep raw `timeout`.
883
- const writerCleanTimeout = !mapped.ok &&
884
- mapped.failureCategory === "timeout" &&
885
- writeGuardOk &&
948
+ const rawFailureCategory = mapped.failureCategory;
949
+ const writeToolCallCount = readWriterThinkingExhaustionEvidence(result).writeToolCallCount;
950
+ const zeroSideEffects = writeGuardOk &&
886
951
  !completenessFailure &&
887
952
  !writerThinkingExhausted &&
888
- isWriterTransportRetryCandidate(input.task) &&
953
+ !writerOutcomeViolation &&
889
954
  changeManifestChangedFiles !== undefined &&
890
955
  changeManifestChangedFiles.length === 0 &&
891
- readWriterThinkingExhaustionEvidence(result).writeToolCallCount === 0;
956
+ writeToolCallCount === 0;
957
+ const targetCleanTransport = !mapped.ok &&
958
+ isTargetTemplateTransientRetryNode(input.task) &&
959
+ rawFailureCategory !== undefined &&
960
+ ["timeout", "network", "rate-limit", "unavailable"].includes(rawFailureCategory) &&
961
+ zeroSideEffects;
962
+ const frontendCleanTimeout = !mapped.ok &&
963
+ rawFailureCategory === "timeout" &&
964
+ isWriterTransportRetryCandidate(input.task) &&
965
+ !isTargetTemplateTransientRetryNode(input.task) &&
966
+ zeroSideEffects;
967
+ const writerCleanTimeout = targetCleanTransport || frontendCleanTimeout;
892
968
  return {
893
969
  ...mapped,
894
970
  ok: false,
895
971
  stderr: stderrParts.filter(Boolean).join("\n\n"),
972
+ ...(rawFailureCategory ? { rawFailureCategory } : {}),
896
973
  failureCategory: mapped.ok
897
974
  ? writeGuardOk
898
975
  ? completenessFailure
@@ -977,6 +1054,15 @@ function parseWriterImplementationOutcome(text) {
977
1054
  const lines = text.split(/\r?\n/);
978
1055
  const candidates = [];
979
1056
  for (const [lineIndex, line] of lines.entries()) {
1057
+ const localizedOutcome = localizedWriterImplementationOutcome(line);
1058
+ if (localizedOutcome) {
1059
+ candidates.push({
1060
+ lineIndex,
1061
+ value: localizedOutcome,
1062
+ outcome: localizedOutcome,
1063
+ });
1064
+ continue;
1065
+ }
980
1066
  const nextLine = lines[lineIndex + 1];
981
1067
  const normalized = normalizeProtocolLine(line, WRITER_OUTCOME_PROTOCOL_LINE, nextLine);
982
1068
  if (normalized === undefined)
@@ -1005,6 +1091,26 @@ function parseWriterImplementationOutcome(text) {
1005
1091
  candidates,
1006
1092
  };
1007
1093
  }
1094
+ function localizedWriterImplementationOutcome(line) {
1095
+ const normalized = normalizedProtocolValue(line);
1096
+ const match = /^(?:实现|实施)结果\s*[::]\s*(已变更|已更改|已修改|已满足|无需变更|已阻塞|阻塞)$/u.exec(normalized);
1097
+ if (!match)
1098
+ return undefined;
1099
+ switch (match[1]) {
1100
+ case "已变更":
1101
+ case "已更改":
1102
+ case "已修改":
1103
+ return "changed";
1104
+ case "已满足":
1105
+ case "无需变更":
1106
+ return "already-satisfied";
1107
+ case "已阻塞":
1108
+ case "阻塞":
1109
+ return "blocked";
1110
+ default:
1111
+ return undefined;
1112
+ }
1113
+ }
1008
1114
  function isWriterImplementationOutcome(value) {
1009
1115
  return (value === "changed" || value === "already-satisfied" || value === "blocked");
1010
1116
  }
@@ -51,9 +51,50 @@ export function normalizePiSessionEvent(event) {
51
51
  }
52
52
  return unifyToolPayloadFields(event);
53
53
  }
54
+ const TERMINAL_FINISH_REASON_TYPES = new Set([
55
+ 'message_end',
56
+ 'turn_end',
57
+ 'assistant_message',
58
+ ]);
59
+ function readExplicitFinishReason(source) {
60
+ const message = isRecord(source.message) ? source.message : undefined;
61
+ const candidates = [
62
+ source.finish_reason,
63
+ source.finishReason,
64
+ source.stop_reason,
65
+ source.stopReason,
66
+ message?.finish_reason,
67
+ message?.finishReason,
68
+ message?.stop_reason,
69
+ message?.stopReason,
70
+ ];
71
+ for (const candidate of candidates) {
72
+ if (typeof candidate === 'string' && candidate.trim().length > 0) {
73
+ return candidate;
74
+ }
75
+ }
76
+ return undefined;
77
+ }
78
+ function applyFinishReason(original, normalized) {
79
+ const originalType = typeof original.type === 'string' ? original.type : undefined;
80
+ if (!originalType || !TERMINAL_FINISH_REASON_TYPES.has(originalType)) {
81
+ return normalized;
82
+ }
83
+ const existing = readExplicitFinishReason({
84
+ finish_reason: normalized.finish_reason,
85
+ });
86
+ if (existing !== undefined) {
87
+ return normalized;
88
+ }
89
+ const promoted = readExplicitFinishReason(original);
90
+ if (promoted === undefined) {
91
+ return normalized;
92
+ }
93
+ return { ...normalized, finish_reason: promoted };
94
+ }
54
95
  /** Serialize one SDK session event as a single JSONL line. */
55
96
  export function serializeSessionEvent(event, options) {
56
- const normalized = normalizePiSessionEvent(event);
97
+ const normalized = applyFinishReason(event, normalizePiSessionEvent(event));
57
98
  if (typeof normalized.recordedAt !== 'string') {
58
99
  normalized.recordedAt = options?.recordedAt ?? new Date().toISOString();
59
100
  }
@@ -408,6 +408,50 @@ export class BoundedTextPreview {
408
408
  ].join("\n");
409
409
  }
410
410
  }
411
+ /**
412
+ * Normalize provider-specific completion-limit labels to the DAG's canonical
413
+ * `length` signal. Other terminal reasons remain observable without being
414
+ * treated as truncation.
415
+ */
416
+ export function normalizeProviderStopReason(value) {
417
+ if (typeof value !== "string" || value.trim().length === 0)
418
+ return undefined;
419
+ const trimmed = value.trim();
420
+ const compact = trimmed.toLowerCase().replace(/[\s_-]/g, "");
421
+ if ([
422
+ "length",
423
+ "maxtokens",
424
+ "maxoutputtokens",
425
+ "outputlimit",
426
+ "tokenlimit",
427
+ "tokenlimitreached",
428
+ ].includes(compact)) {
429
+ return "length";
430
+ }
431
+ return trimmed;
432
+ }
433
+ /**
434
+ * Extract a terminal stop reason from the provider event shapes used by Pi
435
+ * SDK/CLI bridges. Providers disagree on `stop*` versus `finish*`; keep this
436
+ * boundary centralized so downstream DAG recovery sees one canonical signal.
437
+ */
438
+ export function extractPiEventStopReason(event) {
439
+ const nested = [event.message, event.response, event.completion].filter(isRecord);
440
+ const containers = [...nested, event];
441
+ for (const container of containers) {
442
+ for (const key of [
443
+ "stopReason",
444
+ "stop_reason",
445
+ "finishReason",
446
+ "finish_reason",
447
+ ]) {
448
+ const normalized = normalizeProviderStopReason(container[key]);
449
+ if (normalized)
450
+ return normalized;
451
+ }
452
+ }
453
+ return undefined;
454
+ }
411
455
  export class PiJsonlStreamCollector {
412
456
  lineBuffer = "";
413
457
  discardUntilNewline = false;
@@ -415,6 +459,7 @@ export class PiJsonlStreamCollector {
415
459
  parsedEvents = 0;
416
460
  tokensUsed = 0;
417
461
  outputTooLarge = false;
462
+ stopReason;
418
463
  subagentTotalCalls = 0;
419
464
  subagentFailedCalls = 0;
420
465
  subagentModes = new Set();
@@ -454,6 +499,7 @@ export class PiJsonlStreamCollector {
454
499
  assistantText: this.assistantText.trim(),
455
500
  outputTooLarge: this.outputTooLarge,
456
501
  parsedEvents: this.parsedEvents,
502
+ stopReason: this.stopReason,
457
503
  subagentStats: this.formatSubagentStats(),
458
504
  tokensUsed: this.tokensUsed,
459
505
  };
@@ -472,6 +518,10 @@ export class PiJsonlStreamCollector {
472
518
  }
473
519
  }
474
520
  consumeEvent(event) {
521
+ const stopReason = extractPiEventStopReason(event);
522
+ if (stopReason === "length" || !this.stopReason) {
523
+ this.stopReason = stopReason ?? this.stopReason;
524
+ }
475
525
  const candidate = extractAssistantTextFromEvent(event);
476
526
  if (candidate !== undefined) {
477
527
  if (candidate.length > MAX_ASSISTANT_TEXT_CHARS) {
@@ -865,6 +915,7 @@ async function executeSingleCliAttempt(options, modelConfig, promptFilePath) {
865
915
  stdoutTruncated: stdoutPreview.truncated,
866
916
  stderrTruncated: stderrPreview.truncated,
867
917
  outputTooLarge: collected.outputTooLarge,
918
+ stopReason: collected.stopReason,
868
919
  timedOut,
869
920
  tokensUsed: collected.tokensUsed,
870
921
  subagentStats: collected.subagentStats,
@@ -0,0 +1,233 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ /**
5
+ * Pi extension allowlist for backend implementation DAGs (plan D2/D3).
6
+ * DAG specs carry these frozen short ids; the executor resolves them against
7
+ * the Pi agent settings package inventory at run time.
8
+ */
9
+ export const DAG_PI_EXTENSION_IDS = ["pi-codegraph", "pi-lens"];
10
+ /**
11
+ * Alias table: one DAG short id may resolve to multiple published package
12
+ * names. The first settings spec whose de-`npm:`-prefixed package name is in
13
+ * the alias set wins (project settings override user settings).
14
+ */
15
+ export const DAG_PI_EXTENSION_PACKAGE_ALIASES = {
16
+ "pi-codegraph": [
17
+ "@estebanforge/pi-codegraph-enhanced",
18
+ "@vndv/pi-codegraph",
19
+ "pi-codegraph",
20
+ ],
21
+ "pi-lens": ["pi-lens"],
22
+ };
23
+ /**
24
+ * Extension tools that must never be activated as extras on any node (D5):
25
+ * they mutate files without the writer path policy. Writer nodes keep only
26
+ * policy-wrapped edit/write; read nodes keep only navigation tools.
27
+ */
28
+ export const DAG_PI_EXTENSION_WRITE_TOOL_DENY = new Set([
29
+ "ast_grep_replace",
30
+ ]);
31
+ /** Builtin tool names extensions must never replace (D5). */
32
+ export const DAG_PI_EXTENSION_PROTECTED_BUILTIN_TOOLS = new Set(["read", "edit", "write", "bash", "grep", "find", "ls"]);
33
+ /** Deny surface extensions must never activate (D5). */
34
+ export const DAG_PI_EXTENSION_DENIED_TOOL_IDS = new Set([
35
+ "apply_patch",
36
+ "apply-patch",
37
+ "full-tools",
38
+ "full_tools",
39
+ "coding-chat",
40
+ "coding_chat",
41
+ "shell",
42
+ ]);
43
+ export function isDagPiExtensionId(value) {
44
+ return DAG_PI_EXTENSION_IDS.includes(value);
45
+ }
46
+ function getPiAgentDir() {
47
+ const envDir = process.env.PI_CODING_AGENT_DIR;
48
+ if (envDir)
49
+ return envDir;
50
+ return path.join(os.homedir(), ".pi", "agent");
51
+ }
52
+ function readSettingsPackages(settingsPath) {
53
+ try {
54
+ if (!existsSync(settingsPath))
55
+ return undefined;
56
+ const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
57
+ if (!Array.isArray(parsed.packages))
58
+ return undefined;
59
+ return parsed.packages.filter((spec) => typeof spec === "string");
60
+ }
61
+ catch {
62
+ return undefined;
63
+ }
64
+ }
65
+ /** Strip an `npm:` prefix from a settings package spec, e.g. `npm:pi-lens`. */
66
+ export function packageNameFromSpec(spec) {
67
+ return spec.startsWith("npm:") ? spec.slice(4) : spec;
68
+ }
69
+ function loadPackageInventories(repoRoot) {
70
+ const agentDir = getPiAgentDir();
71
+ const userSpecs = readSettingsPackages(path.join(agentDir, "settings.json"));
72
+ const projectSpecs = readSettingsPackages(path.join(repoRoot, ".pi", "settings.json"));
73
+ const inventories = [];
74
+ if (userSpecs) {
75
+ inventories.push({
76
+ specs: userSpecs,
77
+ nodeModulesRoot: path.join(agentDir, "npm", "node_modules"),
78
+ });
79
+ }
80
+ if (projectSpecs) {
81
+ inventories.push({
82
+ specs: projectSpecs,
83
+ nodeModulesRoot: path.join(repoRoot, "node_modules"),
84
+ });
85
+ }
86
+ return inventories;
87
+ }
88
+ const EXTENSION_FILE_RE = /\.(?:ts|js|mjs|cjs)$/;
89
+ /**
90
+ * Resolve a package's `pi.extensions` entries to absolute entry paths.
91
+ * - A file entry maps to itself.
92
+ * - A directory entry expands to every extension file directly inside it
93
+ * (e.g. `@estebanforge/pi-codegraph-enhanced` ships `./extensions` holding
94
+ * `codegraph.ts`).
95
+ * No `pi.extensions` declaration, or a declaration whose paths do not exist,
96
+ * yields no entries (caller records `no-entry`) — no heuristic scanning
97
+ * outside declared paths.
98
+ */
99
+ export function resolvePackageEntryPaths(packageDir) {
100
+ let manifest;
101
+ try {
102
+ manifest = JSON.parse(readFileSync(path.join(packageDir, "package.json"), "utf8"));
103
+ }
104
+ catch {
105
+ return { entryPaths: [], reason: "no-entry" };
106
+ }
107
+ const declared = manifest.pi?.extensions;
108
+ if (!Array.isArray(declared)) {
109
+ return { entryPaths: [], reason: "no-entry" };
110
+ }
111
+ const entryPaths = [];
112
+ for (const entry of declared) {
113
+ if (typeof entry !== "string" || entry.length === 0)
114
+ continue;
115
+ const resolved = path.resolve(packageDir, entry);
116
+ if (!existsSync(resolved))
117
+ continue;
118
+ let isDirectory = false;
119
+ try {
120
+ isDirectory = statSync(resolved).isDirectory();
121
+ }
122
+ catch {
123
+ continue;
124
+ }
125
+ if (!isDirectory) {
126
+ entryPaths.push(resolved);
127
+ continue;
128
+ }
129
+ let names;
130
+ try {
131
+ names = readdirSync(resolved);
132
+ }
133
+ catch {
134
+ continue;
135
+ }
136
+ for (const name of names.sort()) {
137
+ if (!EXTENSION_FILE_RE.test(name))
138
+ continue;
139
+ entryPaths.push(path.join(resolved, name));
140
+ }
141
+ }
142
+ if (entryPaths.length === 0) {
143
+ return { entryPaths: [], reason: "no-entry" };
144
+ }
145
+ return { entryPaths };
146
+ }
147
+ /**
148
+ * Resolve frozen DAG short ids against the Pi settings package inventory
149
+ * (user `~/.pi/agent/settings.json`, then project `.pi/settings.json` — a
150
+ * later match on the same alias wins). Pure synchronous read; never throws.
151
+ */
152
+ export function resolveDagPiExtensions(input) {
153
+ const inventories = loadPackageInventories(input.repoRoot);
154
+ const resolved = [];
155
+ const missing = [];
156
+ for (const id of input.ids) {
157
+ if (!isDagPiExtensionId(id)) {
158
+ missing.push({
159
+ id,
160
+ reason: "package-unreadable",
161
+ detail: `unknown pi extension id: ${id}`,
162
+ });
163
+ continue;
164
+ }
165
+ const aliases = DAG_PI_EXTENSION_PACKAGE_ALIASES[id];
166
+ let matched;
167
+ // Later inventories (project) override earlier (user).
168
+ for (const inventory of inventories) {
169
+ for (const spec of inventory.specs) {
170
+ const packageName = packageNameFromSpec(spec);
171
+ if (!aliases.includes(packageName))
172
+ continue;
173
+ const packageDir = path.join(inventory.nodeModulesRoot, ...packageName.split("/"));
174
+ if (!existsSync(packageDir))
175
+ continue;
176
+ try {
177
+ const manifest = JSON.parse(readFileSync(path.join(packageDir, "package.json"), "utf8"));
178
+ if (typeof manifest.version !== "string")
179
+ continue;
180
+ matched = { packageDir, packageName, version: manifest.version };
181
+ }
182
+ catch {
183
+ // unreadable manifest: skip this candidate
184
+ }
185
+ }
186
+ }
187
+ if (!matched) {
188
+ missing.push({ id, reason: "not-installed" });
189
+ continue;
190
+ }
191
+ const entries = resolvePackageEntryPaths(matched.packageDir);
192
+ if (entries.entryPaths.length === 0) {
193
+ missing.push({
194
+ id,
195
+ reason: entries.reason === "no-entry" ? "no-entry" : "package-unreadable",
196
+ });
197
+ continue;
198
+ }
199
+ resolved.push({
200
+ id,
201
+ packageName: matched.packageName,
202
+ version: matched.version,
203
+ entryPaths: entries.entryPaths,
204
+ });
205
+ }
206
+ return { resolved, missing };
207
+ }
208
+ /**
209
+ * Compute extension tool names that may join a node's active tool allowlist
210
+ * after session creation (D5): new tool names registered by the loaded
211
+ * extensions, minus protected builtins, the deny surface, and write-capable
212
+ * extension tools (both buckets — writer writes stay policy-wrapped only).
213
+ */
214
+ export function computeExtensionToolAllowlistExtras(sessionTools, baseline) {
215
+ const baselineLower = new Set(baseline.map((name) => name.toLowerCase()));
216
+ const extras = [];
217
+ for (const tool of sessionTools) {
218
+ const name = tool && typeof tool.name === "string" ? tool.name : undefined;
219
+ if (!name)
220
+ continue;
221
+ const lower = name.toLowerCase();
222
+ if (baselineLower.has(lower))
223
+ continue;
224
+ if (DAG_PI_EXTENSION_PROTECTED_BUILTIN_TOOLS.has(lower))
225
+ continue;
226
+ if (DAG_PI_EXTENSION_DENIED_TOOL_IDS.has(lower))
227
+ continue;
228
+ if (DAG_PI_EXTENSION_WRITE_TOOL_DENY.has(lower))
229
+ continue;
230
+ extras.push(name);
231
+ }
232
+ return extras;
233
+ }