@tea-agent/loop-agent 0.43.0-next.9 → 0.44.0-next.1

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 (287) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +108 -2308
  3. package/README.md +7 -0
  4. package/dist/application/dag/args.js +8 -0
  5. package/dist/application/task-lifecycle/advance.js +38 -7
  6. package/dist/build-stamp.json +3 -3
  7. package/dist/cli/command-definitions.js +2 -0
  8. package/dist/cli/program.js +3 -1
  9. package/dist/cli/update/init-surface-notifier.js +2 -0
  10. package/dist/cli/update/policy.js +2 -0
  11. package/dist/commands/client-recovery.js +20 -55
  12. package/dist/commands/cursor-prompt.js +2 -0
  13. package/dist/commands/dag-approve.js +2 -0
  14. package/dist/commands/dag-final-verification.js +2 -0
  15. package/dist/commands/dag-init-hybrid.js +2 -0
  16. package/dist/commands/dag-reconcile-tasks.js +2 -0
  17. package/dist/commands/dag-reject.js +2 -0
  18. package/dist/commands/dag-report.js +2 -0
  19. package/dist/commands/dag-rerun.js +2 -0
  20. package/dist/commands/dag-resume.js +2 -0
  21. package/dist/commands/dag-workflow-compile.js +2 -0
  22. package/dist/commands/dag-workflow-plan.js +2 -0
  23. package/dist/commands/dag-workflow-validate.js +2 -0
  24. package/dist/commands/delegate.js +2 -0
  25. package/dist/commands/examples.js +2 -0
  26. package/dist/commands/import-prd.js +2 -0
  27. package/dist/commands/init-model-catalog.js +2 -0
  28. package/dist/commands/init.js +19 -13
  29. package/dist/commands/instructions.js +2 -0
  30. package/dist/commands/knowledge.js +2 -0
  31. package/dist/commands/loop-benchmark.js +2 -0
  32. package/dist/commands/pi-prompt.js +2 -0
  33. package/dist/commands/pi-reuse-benchmark.js +2 -0
  34. package/dist/commands/promote-run.js +2 -0
  35. package/dist/commands/stats.js +1 -1
  36. package/dist/commands/study-init.js +2 -0
  37. package/dist/commands/task-advance.js +34 -0
  38. package/dist/commands/task-source-prepare.js +2 -0
  39. package/dist/commands/worktree-create.js +2 -0
  40. package/dist/commands/worktree-remove.js +2 -0
  41. package/dist/executors/dag-pi-executor.js +1650 -198
  42. package/dist/executors/pi-executor.js +85 -3
  43. package/dist/executors/pi-sdk-executor.js +18 -0
  44. package/dist/executors/shell-executor.js +93 -7
  45. package/dist/executors/shell-verification.js +3 -0
  46. package/dist/executors/shell-write-guard.js +27 -32
  47. package/dist/governance/exec-plans.js +6 -3
  48. package/dist/infrastructure/console/app-data.js +6 -0
  49. package/dist/shared/artifacts-core.js +2 -0
  50. package/dist/shared/backend-dogfood-preflight.js +47 -0
  51. package/dist/shared/dag-failure-category.js +3 -0
  52. package/dist/shared/git-progress.js +2 -0
  53. package/dist/shared/one-shot-prompt-args.js +2 -0
  54. package/dist/shared/operator/capabilities.js +180 -0
  55. package/dist/shared/package-metadata.js +6 -4
  56. package/dist/shared/pi-context-pressure/extension.js +26 -16
  57. package/dist/shared/pi-context-pressure/sift-bridge.js +1 -1
  58. package/dist/shared/pi-provider-recovery.js +182 -0
  59. package/dist/shared/pi-retry-settings.js +40 -0
  60. package/dist/shared/reference-context.js +2 -0
  61. package/dist/task/config-types.js +29 -0
  62. package/dist/task/contract/project.js +9 -0
  63. package/dist/task/contract/recover.js +6 -0
  64. package/dist/task/contract/schema.js +17 -0
  65. package/dist/task/source-prepare/artifact-meta.js +9 -2
  66. package/dist/task/source-prepare/build-draft.js +60 -0
  67. package/dist/task/source-prepare/fragment-inventory.js +4 -1
  68. package/dist/task/source-prepare/parse-intent.js +36 -11
  69. package/dist/task/task-demand-routing.js +5 -2
  70. package/dist/worker/console/chat/browser-automation.js +523 -0
  71. package/dist/worker/console/chat/browser-policy.js +6 -36
  72. package/dist/worker/console/chat/browser-routes.js +188 -56
  73. package/dist/worker/console/chat/explore-tools.js +5 -2
  74. package/dist/worker/console/chat/pi-mode-loop-isolation.js +155 -0
  75. package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +44 -0
  76. package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +184 -0
  77. package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +202 -0
  78. package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +79 -0
  79. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +94 -0
  80. package/dist/worker/console/chat/pi-runtime/custom-tools/scheduled-goal-tools.js +22 -0
  81. package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +38 -0
  82. package/dist/worker/console/chat/pi-runtime/inspection-types.js +28 -0
  83. package/dist/worker/console/chat/pi-runtime/inspection.js +439 -0
  84. package/dist/worker/console/chat/pi-runtime/progressive-tools.js +195 -0
  85. package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +153 -0
  86. package/dist/worker/console/chat/pi-runtime/sdk-session.js +5 -0
  87. package/dist/worker/console/chat/pi-runtime.js +208 -1138
  88. package/dist/worker/console/chat/routes.js +27 -7
  89. package/dist/worker/console/chat/scheduled-goal-booking.js +59 -0
  90. package/dist/worker/console/chat/scheduled-goal-delivery.js +27 -0
  91. package/dist/worker/console/chat/scheduled-goal-request.js +190 -0
  92. package/dist/worker/console/chat/sdd-data-alignment.js +1 -1
  93. package/dist/worker/console/chat/session-mode-view.js +5 -4
  94. package/dist/worker/console/chat/session-mode.js +18 -12
  95. package/dist/worker/console/chat/session-store.js +42 -9
  96. package/dist/worker/console/chat/tool-preview.js +115 -0
  97. package/dist/worker/console/chat/tools.js +10 -0
  98. package/dist/worker/console/chat/turn-order.js +13 -0
  99. package/dist/worker/console/chat/turn-process.js +32 -30
  100. package/dist/worker/console/console-update-and-init.js +1 -19
  101. package/dist/worker/console/console-update-runtime.js +5 -20
  102. package/dist/worker/console/operator-actions.js +63 -1
  103. package/dist/worker/console/prd-intake-bridge.js +54 -1
  104. package/dist/worker/console/routes.js +8 -0
  105. package/dist/worker/console/scheduled-goal-host.js +98 -0
  106. package/dist/worker/console/scheduled-goal-operation-adapter.js +127 -0
  107. package/dist/worker/console/server.js +24 -1
  108. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-D_fNbWc_.js → abnfDiagram-N423BO3Z-8-j6y-sd.js} +1 -1
  109. package/dist/worker/console/static/assets/{arc-_HuqLW6m.js → arc-eoQiMvuk.js} +1 -1
  110. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Djy2qY1m.js → architectureDiagram-T3A2C74G-C4A3uMcI.js} +1 -1
  111. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-bB-1TSyY.js → blockDiagram-VBNYF7ZC-D4zD2F-Q.js} +1 -1
  112. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-C5Hmghrz.js → c4Diagram-5PPSVZJV-j1RkJziL.js} +1 -1
  113. package/dist/worker/console/static/assets/channel-ChE7y-cx.js +1 -0
  114. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Duyl39oy.js → chunk-2GRJ4B5K-YBHmhik1.js} +1 -1
  115. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-x6mdnSyf.js → chunk-2Q5K7J3B-COfWyo9P.js} +1 -1
  116. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-WlBuFj7W.js → chunk-5RXB4S5H-I99OUkHY.js} +1 -1
  117. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CTF429HX.js → chunk-5VM5RSS4-XKxoJNJ4.js} +1 -1
  118. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Dwd6r-SF.js → chunk-6Q2QTUOP-DLT_cYx1.js} +1 -1
  119. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C-Br82D1.js → chunk-GF5L2VYU-CxcKZ9mV.js} +1 -1
  120. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CuGp0DeE.js → chunk-JWPE2WC7-V1EqXdjY.js} +1 -1
  121. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CJhFX6Lh.js → chunk-KBJHAD2P-DebTtdFp.js} +1 -1
  122. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DpGOa_jT.js → chunk-RYQCIY6F-B-ivNRDf.js} +1 -1
  123. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DXJaTynd.js → chunk-XXDRQBXY-DC_Ds11b.js} +1 -1
  124. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +1 -0
  125. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +1 -0
  126. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CRBZWjE4.js → cose-bilkent-JH36ORCC-sWEqKwIb.js} +1 -1
  127. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BdXcxtlb.js → cynefin-VYW2F7L2-BXa_dcu4.js} +1 -1
  128. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-07UKsOts.js → cynefinDiagram-MW4NZA55-C-Mle84F.js} +1 -1
  129. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-D38p9__S.js → dagre-VZM6K2ZE-CXPDBITe.js} +1 -1
  130. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-nSiBNNjH.js → diagram-7IWD3JNH-CC-WJQfa.js} +1 -1
  131. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BFOTot5D.js → diagram-B4RE2ZJO-CDAV5Vs4.js} +1 -1
  132. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DypKaZ6k.js → diagram-LBJQPF4R-CmFiAcNz.js} +1 -1
  133. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-B6yoOQeW.js → diagram-Q27KOJAE-DPBZHuyn.js} +1 -1
  134. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BHYS6viX.js → diagram-UB23O5K3-jUlm_Ds3.js} +1 -1
  135. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-D2VD5Snj.js → ebnfDiagram-BXEA7PRR-DWhQ3mfY.js} +1 -1
  136. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-E98mWxN1.js → erDiagram-JOGREHBK-Tr2gMqet.js} +1 -1
  137. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CT1zxhqP.js → flowDiagram-UKHOOZJN-DJjVQHPA.js} +1 -1
  138. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-6GcngNcy.js → ganttDiagram-PKOTCBZU-D74dQ4u0.js} +1 -1
  139. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DCL6NvKe.js → gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js} +1 -1
  140. package/dist/worker/console/static/assets/index-BWkIfcrK.css +1 -0
  141. package/dist/worker/console/static/assets/index-Cdkvw_H6.js +469 -0
  142. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C0627e-9.js → infoDiagram-6WML65LV-ILCbxJyb.js} +1 -1
  143. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-C_u3P6Ik.js → ishikawaDiagram-WSZJBQD7-DeuWBQ89.js} +1 -1
  144. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DPDPv9HL.js → journeyDiagram-NVQOT4AX-CF5ih8Fk.js} +1 -1
  145. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-D3OHdWiO.js → kanban-definition-27J2QSJJ-C7yOSuRO.js} +1 -1
  146. package/dist/worker/console/static/assets/{linear-DI88zD7n.js → linear-BDZ9riWi.js} +1 -1
  147. package/dist/worker/console/static/assets/{mermaid.core-p2l8IrWY.js → mermaid.core-7pKqYtpZ.js} +5 -5
  148. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-B1bAX-oS.js → mindmap-definition-FAOFIHXS-LeJDybSU.js} +1 -1
  149. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DwOnWxdD.js → pegDiagram-VL7TDLO6-BJvT3pMD.js} +1 -1
  150. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-xRkTXnJC.js → pieDiagram-7S7Q4E2Y-_rGqpMan.js} +1 -1
  151. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-_9UC5Zio.js → quadrantDiagram-CIZ2JOQS-DPcSv5aA.js} +1 -1
  152. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CgEUirWJ.js → railroadDiagram-AXF67PYL-Drxx4hkJ.js} +1 -1
  153. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-Czbpwiyz.js → requirementDiagram-LRYGKXZP-BCTUdU4z.js} +1 -1
  154. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-_j7_KiqY.js → sankeyDiagram-W5VNT64P-B6wzbZmB.js} +1 -1
  155. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BNJa6Hyi.js → sequenceDiagram-SI44F4Z6-BGt8d3QQ.js} +1 -1
  156. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DedpKtOc.js → sizeCapture-X5ZJPWSS-7nlhJKo7.js} +1 -1
  157. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BBvthr7c.js → stateDiagram-OKZ733FA-Cv2stqMA.js} +1 -1
  158. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +1 -0
  159. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Ds7umgEM.js → swimlanes-SLNWSIFB-qDAo4Yc1.js} +2 -2
  160. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +8 -0
  161. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKa-p1nF.js → timeline-definition-Z64GVDOM-CKbDNKTr.js} +1 -1
  162. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-LoUNR56o.js → vennDiagram-T6HMQDX7-DI-9EHic.js} +1 -1
  163. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-Bqbl6ym7.js → wardleyDiagram-T6FBY63Y-BrzzRDpX.js} +1 -1
  164. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-B8dUg4lf.js → xychartDiagram-ELKLHX3M-BclH5hGh.js} +1 -1
  165. package/dist/worker/console/static/index.html +2 -2
  166. package/dist/worker/console/static-src/operator-chat/chat-scroll-position.js +24 -0
  167. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +41 -11
  168. package/dist/worker/console/static-src/operator-chat/compaction-message.js +3 -17
  169. package/dist/worker/console/static-src/operator-chat/composer-session-ledger.js +192 -0
  170. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +9 -1
  171. package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +15 -5
  172. package/dist/worker/console/static-src/operator-chat/timeline-merge.js +29 -0
  173. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +92 -38
  174. package/dist/worker/console/static-src/operator-chat/useChatStream.js +145 -70
  175. package/dist/worker/console/static-src/operator-chat/useChatThread.js +38 -55
  176. package/dist/worker/console/static-src/operator-chat/useComposer.js +182 -69
  177. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +2 -0
  178. package/dist/worker/console/static-src/prd-file-import.js +3 -1
  179. package/dist/worker/console/workspace-context.js +22 -0
  180. package/dist/worker/console/workspace-initialization.js +209 -0
  181. package/dist/worker/delivery/git-transaction.js +2 -0
  182. package/dist/worker/feature/fullstack-validate.js +2 -0
  183. package/dist/worker/loop-agent/loop-agent-client.js +8 -2
  184. package/dist/worker/materialize/harness-task-materializer.js +4 -1
  185. package/dist/worker/metrics/projector.js +1 -1
  186. package/dist/worker/observability/event-store.js +4 -1
  187. package/dist/worker/observability/read-model.js +4 -1
  188. package/dist/worker/observe/node-transparency.js +23 -1
  189. package/dist/worker/observe/routes.js +4 -0
  190. package/dist/worker/observe/static/operator-chrome.js +3 -1
  191. package/dist/worker/observe/static/styles.css +7 -4
  192. package/dist/worker/observe/static/views/dag-inspector.js +5 -19
  193. package/dist/worker/outcomes/declared-artifacts.js +2 -0
  194. package/dist/worker/preflight.js +3 -0
  195. package/dist/worker/scheduler/scheduled-goal-dispatch.js +117 -0
  196. package/dist/worker/scheduler/scheduled-goal-evidence.js +167 -0
  197. package/dist/worker/scheduler/scheduled-goal-recovery.js +62 -0
  198. package/dist/worker/scheduler/scheduled-goal-store.js +699 -0
  199. package/dist/worker/scheduler/scheduled-goal-supervisor.js +132 -0
  200. package/dist/worker/scheduler/scheduled-goal-time.js +102 -0
  201. package/dist/worker/scheduler/scheduled-goal-types.js +95 -0
  202. package/dist/worker/task-spec/validate.js +4 -1
  203. package/dist/workflows/dag/backend-test-markdown-workflow.js +6 -24
  204. package/dist/workflows/dag/backend-test-plan-protocol.js +82 -5
  205. package/dist/workflows/dag/dag-retry-schema.js +11 -0
  206. package/dist/workflows/dag/decision-envelope.js +2 -0
  207. package/dist/workflows/dag/dynamic-runtime/shared.js +2 -2
  208. package/dist/workflows/dag/frontend-closeout.js +3 -1
  209. package/dist/workflows/dag/frontend-committed-facts.js +461 -0
  210. package/dist/workflows/dag/frontend-durable-tools.js +15 -3
  211. package/dist/workflows/dag/frontend-implementation-contract.js +369 -12
  212. package/dist/workflows/dag/frontend-plan-canary.js +53 -0
  213. package/dist/workflows/dag/frontend-plan-decision-contract.js +803 -0
  214. package/dist/workflows/dag/frontend-plan-render.js +0 -2
  215. package/dist/workflows/dag/frontend-provider-capability-matrix.js +8 -61
  216. package/dist/workflows/dag/frontend-recovery-run.js +57 -0
  217. package/dist/workflows/dag/frontend-repair.js +55 -76
  218. package/dist/workflows/dag/frontend-review-context.js +42 -68
  219. package/dist/workflows/dag/frontend-review-scopes.js +2 -2
  220. package/dist/workflows/dag/frontend-risk.js +92 -10
  221. package/dist/workflows/dag/frontend-session-budget.js +117 -3
  222. package/dist/workflows/dag/frontend-shape.js +11 -55
  223. package/dist/workflows/dag/frontend-test-execution-evidence.js +35 -10
  224. package/dist/workflows/dag/frontend-typed-event-store.js +32 -25
  225. package/dist/workflows/dag/frontend-verification-trace.js +33 -54
  226. package/dist/workflows/dag/frontend-writer-admission.js +3 -47
  227. package/dist/workflows/dag/frontend-writer-status.js +0 -23
  228. package/dist/workflows/dag/init-hybrid.js +47 -19
  229. package/dist/workflows/dag/interrupt-request.js +2 -0
  230. package/dist/workflows/dag/lifecycle.js +11 -2
  231. package/dist/workflows/dag/node-execution.js +50 -13
  232. package/dist/workflows/dag/reconcile-run.js +2 -0
  233. package/dist/workflows/dag/repair-artifact.js +3 -1
  234. package/dist/workflows/dag/report.js +2 -0
  235. package/dist/workflows/dag/rerun-plan.js +10 -0
  236. package/dist/workflows/dag/rerun-task.js +79 -1
  237. package/dist/workflows/dag/retry-policy.js +18 -0
  238. package/dist/workflows/dag/scheduler.js +2 -4
  239. package/dist/workflows/dag/types.js +44 -6
  240. package/dist/workflows/dag/validate.js +4 -0
  241. package/docs/README.md +1 -0
  242. package/docs/architecture/runtime-boundaries.md +3 -3
  243. package/docs/governance/README.md +1 -0
  244. package/docs/init-surface.manifest.json +1 -0
  245. package/docs/operations/README.md +2 -0
  246. package/docs/templates/README.md +1 -1
  247. package/docs/templates/agent-dag.schema.json +2 -2
  248. package/docs/templates/backend-test-dag.json +14 -10
  249. package/docs/templates/frontend-implementation-contract.schema.json +0 -7
  250. package/package.json +9 -3
  251. package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
  252. package/skills/frontend-contract/references/contract-protocol.md +3 -1
  253. package/skills/frontend-plan/SKILL.md +8 -6
  254. package/skills/frontend-plan/references/decision-contract.md +3 -3
  255. package/skills/frontend-review/SKILL.md +18 -28
  256. package/skills/frontend-review/references/review-findings.md +8 -3
  257. package/dist/commands/new-task.js +0 -5
  258. package/dist/commands/status.js +0 -57
  259. package/dist/commands/task-contract.js +0 -269
  260. package/dist/shared/prompts.js +0 -26
  261. package/dist/task/config.js +0 -2
  262. package/dist/task/contract/validate-draft.js +0 -106
  263. package/dist/task/goal.js +0 -3
  264. package/dist/task/index.js +0 -12
  265. package/dist/task/lifecycle.js +0 -1
  266. package/dist/task/operator/capabilities.js +0 -6
  267. package/dist/task/operator/envelope.js +0 -2
  268. package/dist/task/operator/index.js +0 -5
  269. package/dist/task/operator/registry.js +0 -2
  270. package/dist/task/operator/types.js +0 -1
  271. package/dist/task/paths.js +0 -1
  272. package/dist/task/source-state.js +0 -1
  273. package/dist/task/subagent-guidance.js +0 -1
  274. package/dist/worker/console/chat/sift-bridge.js +0 -1
  275. package/dist/worker/console/index.js +0 -20
  276. package/dist/worker/console/static/assets/channel-C0A6UEIa.js +0 -1
  277. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DzguNppy.js +0 -1
  278. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DzguNppy.js +0 -1
  279. package/dist/worker/console/static/assets/index-24jp3aBh.js +0 -468
  280. package/dist/worker/console/static/assets/index-CY6y6I8M.css +0 -1
  281. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BABVQnfM.js +0 -1
  282. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D0qsLaxR.js +0 -8
  283. package/dist/worker/scheduler/index.js +0 -25
  284. package/dist/workflows/dag/facts.js +0 -4
  285. package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -975
  286. package/dist/workflows/dag/index.js +0 -6
  287. package/dist/workflows/dynamic/index.js +0 -7
@@ -26,28 +26,41 @@
26
26
  * `createDefaultPiSdkBindings()`.
27
27
  */
28
28
  import { createHash, randomBytes } from "node:crypto";
29
- import { access, mkdir, rm, stat, writeFile } from "node:fs/promises";
29
+ import { stat } from "node:fs/promises";
30
30
  import path from "node:path";
31
31
  import { extractAssistantContentBlocks, extractAssistantText, isAssistantSdkMessage, } from "./assistant-content.js";
32
32
  import { parseProviderError, } from "./provider-error.js";
33
33
  import { projectCompactSnapshot, } from "./chat-event-store.js";
34
34
  import { extractUsageSample } from "./usage.js";
35
35
  import { OPERATOR_CHAT_ALLOWED_TOOLS, authorizeOperatorChatTool, assertNoWriteToolInList, } from "./tools.js";
36
- import { authorizeChatSessionTool, DEFAULT_CONSOLE_CHAT_SESSION_MODE, PI_TODO_WRITE_DESCRIPTION, chatActiveToolBaseline, composeChatSystemPrompt, composeChatSystemPromptParts, computeChatActiveToolNames, shouldInjectGoalContinuation, shouldLoadWorkflowBusinessTools, } from "./session-mode.js";
36
+ import { authorizeChatSessionTool, DEFAULT_CONSOLE_CHAT_SESSION_MODE, chatActiveToolBaseline, composeChatSystemPrompt, composeChatSystemPromptParts, computeChatActiveToolNames, shouldLoadWorkflowBusinessTools, } from "./session-mode.js";
37
37
  import { buildAgentCustomTool, ExploreOrchestrator, } from "./subagents/index.js";
38
38
  import { createOperatorChatResourceLoader, } from "./resource-loader.js";
39
- import { applySkillAutoInvocationPreferences, isAutoInvocationEnabled, loadResourcePreferences, skillResourceId, } from "./resource-preferences-store.js";
40
- import { buildModelCallableToolSchemas } from "./tool-adapter.js";
39
+ import { applySkillAutoInvocationPreferences, loadResourcePreferences, } from "./resource-preferences-store.js";
40
+ import { buildPiModeLoopIsolationOptions } from "./pi-mode-loop-isolation.js";
41
+ import { applyProcessRetryOverride, } from "../../../shared/pi-retry-settings.js";
41
42
  import { applyProcessCompactionOverride, createCompactionController, createContextPressureExtension, createSiftBridgeState, housekeepSiftStash, isConsoleSiftEnabled, parseCompactionPolicy, parseSiftBridgeConfig, projectPromptPressure, deriveEffectiveWindow, projectSafeTelemetry, emptyContextPressureCounters, } from "../../../shared/pi-context-pressure/index.js";
42
43
  import { incrementCounter as incrementPressureCounter } from "../../../shared/pi-context-pressure/telemetry.js";
43
44
  import { resolveDefaultChatModel, resolveLowChatModel, } from "./model-resolver.js";
44
45
  import { filterActiveInterviewTools } from "../interview/tools.js";
45
- import { RUNTIME_CONTEXT_TEXT_MAX, redactRuntimeText, } from "./runtime-context.js";
46
+ import { redactRuntimeText } from "./runtime-context.js";
47
+ import { BrowserAutomationService } from "./browser-automation.js";
46
48
  import { buildTodoContinuationContext, } from "./todos.js";
47
49
  import { buildPersistentTerminalTools } from "./terminal-tools.js";
48
50
  import { buildScmTools } from "./scm-tools.js";
49
51
  import { buildGoalContinuationContext, } from "./goals.js";
50
52
  import { injectWizardLocalHeaders, } from "./sdd-data-alignment.js";
53
+ import { buildScheduledGoalTools } from "./pi-runtime/custom-tools/scheduled-goal-tools.js";
54
+ import { WorkflowToolExposure, configuredToolExposure, isScheduledBookingTurn, pinScheduledBookingTurnTools, } from "./pi-runtime/progressive-tools.js";
55
+ import { buildCustomOperatorTools } from "./pi-runtime/custom-tools/operator-tools.js";
56
+ import { buildBrowserTools } from "./pi-runtime/custom-tools/browser-tools.js";
57
+ import { buildAskUserQuestionTool } from "./pi-runtime/custom-tools/ask-user-question.js";
58
+ import { buildTodoWriteTool } from "./pi-runtime/custom-tools/todo-write.js";
59
+ import { buildGoalTools } from "./pi-runtime/custom-tools/goal-tools.js";
60
+ import { buildExploreCustomTools } from "./pi-runtime/custom-tools/explore-tools.js";
61
+ import { THINKING_LEVELS, RuntimeSnapshotUnavailableError, } from "./pi-runtime/inspection-types.js";
62
+ import { collectRuntimeInventories, collectSlashCommands, projectRuntimeSnapshot, } from "./pi-runtime/inspection.js";
63
+ import { createDefaultPiSdkBindings, safeGetAgentDir, } from "./pi-runtime/sdk-bindings.js";
51
64
  /**
52
65
  * Base system prompt fragment every General Operator Chat session receives.
53
66
  *
@@ -70,13 +83,14 @@ import { injectWizardLocalHeaders, } from "./sdd-data-alignment.js";
70
83
  */
71
84
  export const OPERATOR_CHAT_SYSTEM_PROMPT_BASE = [
72
85
  "You are General Operator Chat.",
73
- "Use operator tools first (dagReport, dagRerunPlan, dagRerun). Pi read/write/edit/bash/grep/find/ls are available; obey repository AGENTS.md. apply_patch, full-tools, shell and coding-chat are denied. Prefer safe-read/safe-grep for probes.",
86
+ "For workflow requests use operator tools; discover missing capabilities via operatorCapabilities(target). Obey AGENTS.md. Deny apply_patch/full-tools/shell/coding-chat; probe with safe-read/safe-grep. Browser_*: visible Browser panel only (static http(s), no workspace edits/scripts).",
74
87
  "No harness and asked to init: `loop-agent init instructions --repo-root .`, then `loop-agent init --repo-root . --profile full --merge`; verify with doctor, inspect and docs audit. Updates: `loop-agent init upgrade --repo-root . --json` until stable; `loop-agent init check-update` is read-only.",
88
+ "Scheduled intent takes precedence over task content: prepareScheduledTask first; no curl/HTTP probes or tests; never perform the scheduled work now. Only a planned goalId means booked; never self-confirm.",
75
89
  "Long DAGs: prepareDagExecution → runDag → operationId. Never foreground-Bash task advance, tail/head pipes or nohup/Start-Process/start; do not end on accepted/queued/running or an operationId — NOT completion. Supervise operationGet/status/dagReport/dagDoctor/operationWait at 60s → 90s → 120s → 180s; reset to 60s on state change; 30-60s re-checks when stall suspected. operationWait MUST use afterSeq=previous nextSeq (recommendedNextCall). Heartbeats: not meaningful progress. Run id ≠ task id.",
76
- "Failure: terminal runSummary first. With progress wait; else dagRerunPlan(fromNode=primaryFailure.nodeId); eligible → dagRerun: reruns the failed subgraph, keeps prior successes. Ineligible: Worker → workerTaskRetry; standalone → standaloneTaskRerun after contract adopt (regenerates the whole task, not node continue). Do not call dagResume; do not re-advance a run-failed task. Pre-run identical state twice: stop and report; never loop identical bash/read/grep. Approve only bounded writeSet inside allowedPaths, outside forbiddenPaths, with structured verification.",
77
- "Iterate toward the goal in bounded rounds: first summarize the previous round's error in one line, fix its root cause — not the symptom — then rerun the failed node with a fresh plan. Never resubmit an unchanged fix; after two rounds failing the same way, stop and report evidence and remaining routes.",
78
- "Continue until success, user stop, or no safe recovery remains; then stop and report failure/evidence. Never create a new task for a transient failure or replace repair-pi with direct edits.",
79
- "Narrate long-DAG progress briefly at start, meaningful phase changes/findings, verification failure/recovery, prolonged quiet and terminal state: say what finished and what is next. Never narrate unchanged operationWait polls or heartbeats, or claim beyond structured facts.",
90
+ "Failure: dagReport(runId) → terminal runSummary. With progress wait; else dagRerunPlan(fromNode=primaryFailure.nodeId); eligible → dagRerun: reruns the failed subgraph, keeps prior successes. Ineligible: Worker → workerTaskRetry; standalone → standaloneTaskRerun after contract adopt (regenerates the whole task, not node continue). No dagResume or re-advance of run-failed tasks. Pre-run identical state twice: stop and report; never loop identical bash/read/grep. Approve only bounded writeSet inside allowedPaths, outside forbiddenPaths, with structured verification.",
91
+ "Each bounded round: summarize the previous round's error in one line, fix its root cause — not the symptom — then rerun the failed node with a fresh plan. Never resubmit an unchanged fix; after two rounds failing the same way, stop and report evidence and remaining routes.",
92
+ "Continue until success, user stop, or no safe recovery remains. Never create a new task for a transient failure or replace repair-pi with direct edits.",
93
+ "Report start, meaningful phase changes, failures, prolonged quiet and completion: what finished and what is next. Never narrate unchanged operationWait polls or heartbeats.",
80
94
  ].join("\n");
81
95
  /**
82
96
  * Built-in SDK tool names registered for every Chat session (ADR 0011).
@@ -278,595 +292,8 @@ export function createWizardLocalHeadersExtension(options) {
278
292
  },
279
293
  };
280
294
  }
281
- /**
282
- * Build Pi custom ToolDefinition objects for every whitelisted operator action.
283
- * Each tool's execute() routes into the operator action dispatcher (Gate 3).
284
- * Requires the `typebox` + `defineTool` packages (both bundled with the SDK).
285
- */
286
- async function buildCustomOperatorTools(options) {
287
- // typebox is a transitive dep of @earendil-works/pi-coding-agent; Node's
288
- // resolver hoists to the SDK's nested copy. defineTool is re-exported by SDK.
289
- const [{ Type }, { defineTool }] = await Promise.all([
290
- import("typebox"),
291
- import("@earendil-works/pi-coding-agent"),
292
- ]);
293
- const schemas = buildModelCallableToolSchemas();
294
- const ctx = options.actionContext;
295
- return schemas.map((schema) => {
296
- // TypeBox schema is built dynamically; cast through unknown to the builder
297
- // since Static<TParams> is structural and cannot be statically inferred here.
298
- const parameters = Type.Object(schema.inputParams.reduce((acc, param) => {
299
- // Carry the registry's param description into the tool schema so
300
- // the model sees parameter semantics, not just names/types.
301
- const desc = param.description?.trim()
302
- ? { description: param.description }
303
- : {};
304
- let t;
305
- switch (param.type) {
306
- case "string":
307
- t = Type.String(desc);
308
- break;
309
- case "boolean":
310
- t = Type.Boolean(desc);
311
- break;
312
- case "number":
313
- t = Type.Number(desc);
314
- break;
315
- case "array":
316
- t = Type.Array(Type.String({}), Object.keys(desc).length ? desc : {});
317
- break;
318
- default:
319
- t = Type.Object({}, { additionalProperties: true });
320
- }
321
- acc[param.name] = param.required ? t : Type.Optional(t);
322
- return acc;
323
- }, {}), { additionalProperties: false });
324
- return defineTool({
325
- name: schema.toolId,
326
- label: schema.toolId,
327
- description: `${schema.description} (kind=${schema.kind}; cli: ${schema.cli})`,
328
- promptSnippet: schema.description,
329
- parameters: parameters,
330
- async execute(toolCallId, params) {
331
- const decision = authorizeChatSessionTool(schema.toolId, options.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE);
332
- if (!decision.ok) {
333
- return {
334
- content: [
335
- {
336
- type: "text",
337
- text: JSON.stringify({
338
- error: decision.message,
339
- code: decision.code,
340
- }),
341
- },
342
- ],
343
- details: { error: decision.message, code: decision.code },
344
- };
345
- }
346
- // Defer require to avoid circular import at module load.
347
- const { dispatchChatToolCall } = await import("./session-store.js");
348
- const result = await dispatchChatToolCall(ctx, decision.toolId, params, toolCallId, options.sessionMode);
349
- const payload = result.ok
350
- ? result.result
351
- : { error: result.message, code: result.errorCode };
352
- return {
353
- content: [{ type: "text", text: JSON.stringify(payload) }],
354
- details: payload,
355
- };
356
- },
357
- });
358
- });
359
- }
360
- const ASK_USER_QUESTION_DESCRIPTION = "Ask the user concise questions when a confirmation, choice, or missing detail is required before proceeding. " +
361
- "The tool pauses until the user answers. Use it instead of writing questions as ordinary assistant text. " +
362
- "Send one or more questions with stable ids; recommended options should be first and end with (Recommended).";
363
- async function buildAskUserQuestionTool(options) {
364
- const [{ Type }, { defineTool }] = await Promise.all([
365
- import("typebox"),
366
- import("@earendil-works/pi-coding-agent"),
367
- ]);
368
- return defineTool({
369
- name: "ask_user_question",
370
- label: "Ask user",
371
- description: ASK_USER_QUESTION_DESCRIPTION,
372
- promptSnippet: ASK_USER_QUESTION_DESCRIPTION,
373
- parameters: Type.Object({
374
- questions: Type.Array(Type.Object({
375
- id: Type.String({ description: "Stable question id echoed in the answer." }),
376
- question: Type.String({ description: "Specific user-facing question." }),
377
- header: Type.Optional(Type.String({ description: "Optional short heading." })),
378
- detail: Type.Optional(Type.String({ description: "Optional Markdown context shown below the question." })),
379
- options: Type.Optional(Type.Array(Type.Object({
380
- label: Type.String({ description: "Short user-facing option label." }),
381
- description: Type.Optional(Type.String({ description: "One sentence explaining impact or tradeoff." })),
382
- }, { additionalProperties: false }))),
383
- multi_select: Type.Optional(Type.Boolean({ description: "Allow more than one option." })),
384
- }, { additionalProperties: false }), { minItems: 1, maxItems: 20 }),
385
- }, { additionalProperties: false }),
386
- async execute(toolCallId, params, signal) {
387
- const answer = await options.registry.ask({
388
- sessionId: options.sessionId,
389
- toolCallId,
390
- questions: params.questions,
391
- signal,
392
- });
393
- return {
394
- content: [{ type: "text", text: JSON.stringify(answer) }],
395
- details: answer,
396
- };
397
- },
398
- });
399
- }
400
- const TODO_WRITE_DESCRIPTION = "Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (no partial updates, no per-item edits). Before starting multi-step work, add one todo per concrete step. Keep at least one todo `in_progress` while work remains. Mark a todo `completed` immediately when it is done (do not batch completions) and resend the full list as progress or DAG state advances. Skip the list for simple Q&A, read-only lookups, and single-step tasks. Use it for multi-step implementation and whenever you prepare, run, or supervise a DAG — call `todo_write` before entering the DAG tool chain. Phrase DAG todos as user-facing phases users can understand, not copies of internal DAG nodes. Statuses: `pending` / `in_progress` / `completed`.";
401
- async function buildTodoWriteTool(options) {
402
- const [{ Type }, { defineTool }] = await Promise.all([
403
- import("typebox"),
404
- import("@earendil-works/pi-coding-agent"),
405
- ]);
406
- const description = options.sessionMode === "pi"
407
- ? PI_TODO_WRITE_DESCRIPTION
408
- : TODO_WRITE_DESCRIPTION;
409
- return defineTool({
410
- name: "todo_write",
411
- label: "Update todo list",
412
- description,
413
- promptSnippet: description,
414
- parameters: Type.Object({
415
- todos: Type.Array(Type.Object({
416
- content: Type.String({ description: "Short imperative task description." }),
417
- status: Type.Union([
418
- Type.Literal("pending"),
419
- Type.Literal("in_progress"),
420
- Type.Literal("completed"),
421
- ]),
422
- }, { additionalProperties: false })),
423
- }, { additionalProperties: false }),
424
- async execute(toolCallId, params) {
425
- const result = options.registry.write(options.sessionId, toolCallId, params.todos);
426
- return {
427
- content: [{ type: "text", text: JSON.stringify(result) }],
428
- details: result,
429
- };
430
- },
431
- });
432
- }
433
- const GET_GOAL_DESCRIPTION = "Read the current session's durable cross-turn Goal, including revision, status, round limit, and whether this Console process is authorized to continue it.";
434
- const CREATE_GOAL_DESCRIPTION = "Create a durable cross-turn Goal only when the user explicitly asks to start or create a Goal. Never silently upgrade an ordinary request. A session may have only one unfinished Goal.";
435
- const UPDATE_GOAL_DESCRIPTION = "Finish or block the current durable Goal using its exact goalId and revision. Set complete only when the objective is genuinely achieved and no required work remains. Set blocked only when safe progress cannot continue; include the blocker.";
436
- async function buildGoalTools(options) {
437
- const [{ Type }, { defineTool }] = await Promise.all([
438
- import("typebox"),
439
- import("@earendil-works/pi-coding-agent"),
440
- ]);
441
- const textResult = (value) => ({
442
- content: [{ type: "text", text: JSON.stringify(value) }],
443
- details: value,
444
- });
445
- return [
446
- defineTool({
447
- name: "get_goal",
448
- label: "Get goal",
449
- description: GET_GOAL_DESCRIPTION,
450
- promptSnippet: GET_GOAL_DESCRIPTION,
451
- parameters: Type.Object({}, { additionalProperties: false }),
452
- async execute() {
453
- return textResult({ goal: options.registry.get(options.sessionId) });
454
- },
455
- }),
456
- defineTool({
457
- name: "create_goal",
458
- label: "Create goal",
459
- description: CREATE_GOAL_DESCRIPTION,
460
- promptSnippet: CREATE_GOAL_DESCRIPTION,
461
- parameters: Type.Object({
462
- objective: Type.String({
463
- description: "Concrete final objective and completion standard explicitly requested by the user.",
464
- }),
465
- }, { additionalProperties: false }),
466
- async execute(_toolCallId, params) {
467
- if (!options.registry.consumeModelCreateGrant(options.sessionId)) {
468
- return textResult({
469
- ok: false,
470
- code: "GOAL_EXPLICIT_REQUEST_REQUIRED",
471
- message: "create_goal requires an explicit user request to create or start a Goal",
472
- });
473
- }
474
- return textResult(options.registry.create({
475
- sessionId: options.sessionId,
476
- objective: params.objective,
477
- }));
478
- },
479
- }),
480
- defineTool({
481
- name: "update_goal",
482
- label: "Update goal",
483
- description: UPDATE_GOAL_DESCRIPTION,
484
- promptSnippet: UPDATE_GOAL_DESCRIPTION,
485
- parameters: Type.Object({
486
- goalId: Type.String(),
487
- revision: Type.Number({ minimum: 1 }),
488
- status: Type.Union([
489
- Type.Literal("complete"),
490
- Type.Literal("blocked"),
491
- ]),
492
- blocker: Type.Optional(Type.String()),
493
- }, { additionalProperties: false }),
494
- async execute(_toolCallId, params) {
495
- return textResult(options.registry.update({
496
- sessionId: options.sessionId,
497
- goalId: params.goalId,
498
- expectedRevision: params.revision,
499
- phase: params.status,
500
- blocker: params.blocker,
501
- arm: false,
502
- }));
503
- },
504
- }),
505
- ];
506
- }
507
- /**
508
- * Build the safe explore custom tools (roadmap M0-A / G16): safe-read,
509
- * safe-grep, gitStatus, gitDiff. These replace the bare SDK bash/read/grep so
510
- * the model can probe the repo WITHOUT write escapes (echo redirect, tee,
511
- * sed -i, node -e) and WITHOUT reading sensitive files (env files, key/pem,
512
- * auth.json, sessions dir) — and every returned byte is secret-scrubbed
513
- * before reaching the model context.
514
- *
515
- * gitStatus/gitDiff use a FIXED argv (typed, no shell) so they cannot be
516
- * turned into an arbitrary-shell vector.
517
- */
518
- async function buildExploreCustomTools(options) {
519
- const [{ Type }, { defineTool }] = await Promise.all([
520
- import("typebox"),
521
- import("@earendil-works/pi-coding-agent"),
522
- ]);
523
- const { execFileSync } = await import("node:child_process");
524
- const repoRoot = options.repoRoot;
525
- const { safeReadFile, safeGrep, scrubSecrets } = await import("./explore-tools.js");
526
- function safeGit(args) {
527
- try {
528
- const stdout = execFileSync("git", args, {
529
- cwd: repoRoot,
530
- encoding: "utf8",
531
- // Fixed argv, no shell, bounded output.
532
- maxBuffer: 512 * 1024,
533
- timeout: 10_000,
534
- windowsHide: true,
535
- });
536
- return { ok: true, stdout };
537
- }
538
- catch (e) {
539
- return {
540
- ok: false,
541
- code: "GIT_FAILED",
542
- message: e instanceof Error ? e.message : String(e),
543
- };
544
- }
545
- }
546
- const safeReadTool = defineTool({
547
- name: "safe-read",
548
- label: "safe-read",
549
- description: "Read a repo-relative file with sensitive-file denylist (.env*/*.key/*.pem/.git/**/auth.json/sessions/**) and secret scrubbing. Absolute paths and .. are rejected.",
550
- promptSnippet: "safe-read: read a repo file (safe; secrets scrubbed)",
551
- parameters: Type.Object({
552
- path: Type.String({ description: "repo-relative file path" }),
553
- }, { additionalProperties: false }),
554
- async execute(_toolCallId, params) {
555
- const raw = String(params.path ?? "");
556
- try {
557
- const result = await safeReadFile(repoRoot, raw);
558
- return {
559
- content: [{ type: "text", text: JSON.stringify(result) }],
560
- details: result,
561
- };
562
- }
563
- catch (e) {
564
- const err = e;
565
- return {
566
- content: [
567
- {
568
- type: "text",
569
- text: JSON.stringify({
570
- error: err.message ?? String(e),
571
- code: err.code ?? "READ_FAILED",
572
- }),
573
- },
574
- ],
575
- details: {
576
- error: err.message ?? String(e),
577
- code: err.code ?? "READ_FAILED",
578
- },
579
- };
580
- }
581
- },
582
- });
583
- const safeGrepTool = defineTool({
584
- name: "safe-grep",
585
- label: "safe-grep",
586
- description: "Line search across the repo working tree with sensitive-file denylist and secret scrubbing. pattern is a JS regex source; optional glob filters paths.",
587
- promptSnippet: "safe-grep: search repo files (safe; secrets scrubbed)",
588
- parameters: Type.Object({
589
- pattern: Type.String({ description: "regex source (no shell)" }),
590
- caseInsensitive: Type.Optional(Type.Boolean()),
591
- glob: Type.Optional(Type.String({ description: "optional glob filter e.g. *.ts" })),
592
- }, { additionalProperties: false }),
593
- async execute(_toolCallId, params) {
594
- try {
595
- const result = await safeGrep(repoRoot, String(params.pattern ?? ""), {
596
- caseInsensitive: Boolean(params.caseInsensitive),
597
- glob: params.glob ? String(params.glob) : undefined,
598
- });
599
- return {
600
- content: [{ type: "text", text: JSON.stringify(result) }],
601
- details: result,
602
- };
603
- }
604
- catch (e) {
605
- const err = e;
606
- return {
607
- content: [
608
- {
609
- type: "text",
610
- text: JSON.stringify({
611
- error: err.message ?? String(e),
612
- code: err.code ?? "GREP_FAILED",
613
- }),
614
- },
615
- ],
616
- details: {
617
- error: err.message ?? String(e),
618
- code: err.code ?? "GREP_FAILED",
619
- },
620
- };
621
- }
622
- },
623
- });
624
- const gitStatusTool = defineTool({
625
- name: "git-status",
626
- label: "git-status",
627
- description: "Typed `git status --porcelain` (fixed argv, no shell). Returns scrubbed working-tree status.",
628
- promptSnippet: "git-status: working tree status (read-only)",
629
- parameters: Type.Object({}, { additionalProperties: false }),
630
- async execute() {
631
- const r = safeGit(["status", "--porcelain"]);
632
- if (!r.ok) {
633
- return {
634
- content: [
635
- {
636
- type: "text",
637
- text: JSON.stringify({ error: r.message, code: r.code }),
638
- },
639
- ],
640
- details: { error: r.message, code: r.code },
641
- };
642
- }
643
- const { scrubbed, redactions } = scrubSecrets(r.stdout);
644
- const payload = { status: scrubbed, redactions };
645
- return {
646
- content: [{ type: "text", text: JSON.stringify(payload) }],
647
- details: payload,
648
- };
649
- },
650
- });
651
- const gitDiffTool = defineTool({
652
- name: "git-diff",
653
- label: "git-diff",
654
- description: "Typed `git diff` (fixed argv: --stat or a single -- path; no shell). Returns scrubbed diff. path must be repo-relative and not sensitive.",
655
- promptSnippet: "git-diff: diff (read-only, scrubbed)",
656
- parameters: Type.Object({
657
- path: Type.Optional(Type.String({
658
- description: "optional repo-relative path to diff (default: whole tree --stat)",
659
- })),
660
- }, { additionalProperties: false }),
661
- async execute(_toolCallId, params) {
662
- let rawPath = params.path ? String(params.path) : undefined;
663
- // If a path is supplied, validate it via the read-path guard so git diff
664
- // cannot be aimed at sensitive files (.env* / auth.json / sessions).
665
- if (rawPath) {
666
- const { guardReadPath } = await import("./explore-tools.js");
667
- const g = await guardReadPath(repoRoot, rawPath);
668
- if (!g.ok) {
669
- return {
670
- content: [
671
- {
672
- type: "text",
673
- text: JSON.stringify({ error: g.message, code: g.code }),
674
- },
675
- ],
676
- details: { error: g.message, code: g.code },
677
- };
678
- }
679
- rawPath = g.repoRelative;
680
- }
681
- const args = rawPath ? ["diff", "--", rawPath] : ["diff", "--stat"];
682
- const r = safeGit(args);
683
- if (!r.ok) {
684
- return {
685
- content: [
686
- {
687
- type: "text",
688
- text: JSON.stringify({ error: r.message, code: r.code }),
689
- },
690
- ],
691
- details: { error: r.message, code: r.code },
692
- };
693
- }
694
- const { scrubbed, redactions } = scrubSecrets(r.stdout);
695
- const payload = { diff: scrubbed, redactions };
696
- return {
697
- content: [{ type: "text", text: JSON.stringify(payload) }],
698
- details: payload,
699
- };
700
- },
701
- });
702
- return [safeReadTool, safeGrepTool, gitStatusTool, gitDiffTool];
703
- }
704
- export const THINKING_LEVELS = [
705
- "off",
706
- "minimal",
707
- "low",
708
- "medium",
709
- "high",
710
- "xhigh",
711
- "max",
712
- ];
713
- /**
714
- * Structured critical-failure error for snapshot projection (RF-02). Model
715
- * enumeration or system prompt projection failures throw this so consuming
716
- * routes can return 503 PI_RUNTIME_UNAVAILABLE instead of a fake-empty 200.
717
- * Partial ResourceLoader / package-inventory failures are NOT critical — they
718
- * keep the 200 snapshot and surface as structured diagnostics instead.
719
- */
720
- export class RuntimeSnapshotUnavailableError extends Error {
721
- code = "PI_RUNTIME_UNAVAILABLE";
722
- constructor(message) {
723
- super(message);
724
- this.name = "RuntimeSnapshotUnavailableError";
725
- }
726
- }
727
- /**
728
- * Browser-safe projection of a live runtime snapshot (design §2.2 / RF-03).
729
- *
730
- * Every browser-visible string runs the shared secret-scrub chain
731
- * (scrubSecrets + ghp_/Bearer redaction via redactRuntimeText) and a fixed
732
- * per-field length limit: 200 (names/spec/source/origin), 400 (paths/errors/
733
- * diagnostic messages), 600 (skill descriptions), 80 (package versions);
734
- * truncation uses the fixed `…[truncated N chars …]` marker. The server-only
735
- * `raw` prompt and the RAW-based `sha256`/`characters` pass through untouched,
736
- * and real empty lists stay empty.
737
- *
738
- * Pure + deterministic and exported so the route harness reuses the exact
739
- * production implementation (same code path as getRuntimeSnapshot).
740
- */
741
- export function projectRuntimeSnapshot(input) {
742
- const field = (value, max) => value === undefined ? undefined : redactRuntimeText(value, max);
743
- const projectSkill = (skill) => ({
744
- name: field(skill.name, 200) ?? "",
745
- description: field(skill.description, 600) ?? "",
746
- path: field(skill.path, 400) ?? "",
747
- ...(skill.baseDir ? { baseDir: field(skill.baseDir, 400) } : {}),
748
- scope: skill.scope,
749
- source: field(skill.source, 200) ?? "",
750
- resourceId: field(skill.resourceId, 200) ?? "",
751
- autoInvocationEnabled: skill.autoInvocationEnabled,
752
- diagnostics: skill.diagnostics.map((diag) => field(diag, 400) ?? ""),
753
- });
754
- const projectExtension = (ext) => ({
755
- name: field(ext.name, 200) ?? "",
756
- path: field(ext.path, 400) ?? "",
757
- ...(ext.resolvedPath
758
- ? { resolvedPath: field(ext.resolvedPath, 400) }
759
- : {}),
760
- source: field(ext.source, 200) ?? "",
761
- scope: ext.scope,
762
- ...(ext.origin ? { origin: field(ext.origin, 200) } : {}),
763
- enabled: ext.enabled,
764
- ...(ext.error ? { error: field(ext.error, 400) } : {}),
765
- });
766
- const projectPackage = (pkg) => ({
767
- spec: field(pkg.spec, 200) ?? "",
768
- scope: pkg.scope,
769
- enabled: pkg.enabled,
770
- ...(pkg.version ? { version: field(pkg.version, 80) } : {}),
771
- ...(pkg.configuredVersion
772
- ? { configuredVersion: field(pkg.configuredVersion, 80) }
773
- : {}),
774
- ...(pkg.packageName ? { packageName: field(pkg.packageName, 200) } : {}),
775
- ...(pkg.path ? { path: field(pkg.path, 400) } : {}),
776
- ...(pkg.installedPath ? { installedPath: field(pkg.installedPath, 400) } : {}),
777
- ...(pkg.cwd ? { cwd: field(pkg.cwd, 400) } : {}),
778
- resolved: pkg.resolved.map((resource) => ({
779
- type: resource.type,
780
- name: field(resource.name, 200) ?? "",
781
- path: field(resource.path, 400) ?? "",
782
- source: field(resource.source, 200) ?? "",
783
- scope: resource.scope,
784
- })),
785
- diagnostics: pkg.diagnostics.map((diag) => field(diag, 400) ?? ""),
786
- });
787
- const projectMcpTool = (tool) => ({
788
- name: field(tool.name, 200) ?? "",
789
- description: field(tool.description, 600) ?? "",
790
- ...(tool.server ? { server: field(tool.server, 200) } : {}),
791
- source: field(tool.source, 200) ?? "",
792
- scope: tool.scope,
793
- ...(tool.path ? { path: field(tool.path, 400) } : {}),
794
- active: tool.active,
795
- });
796
- const projectMcp = (mcp) => ({
797
- servers: mcp.servers.map((server) => ({
798
- name: field(server.name, 200) ?? "",
799
- scope: server.scope,
800
- status: server.status,
801
- enabled: server.enabled,
802
- ...(server.transport ? { transport: field(server.transport, 80) } : {}),
803
- ...(server.command ? { command: field(server.command, 400) } : {}),
804
- ...(server.url ? { url: field(server.url, 400) } : {}),
805
- ...(server.configPath ? { configPath: field(server.configPath, 400) } : {}),
806
- tools: server.tools.map(projectMcpTool),
807
- })),
808
- unassignedTools: mcp.unassignedTools.map(projectMcpTool),
809
- });
810
- const projectDiagnostic = (diag) => ({
811
- type: diag.type,
812
- ...(diag.code ? { code: diag.code } : {}),
813
- message: field(diag.message, 400) ?? "",
814
- ...(diag.path ? { path: field(diag.path, 400) } : {}),
815
- ...(diag.retryable !== undefined
816
- ? { retryable: diag.retryable }
817
- : {}),
818
- });
819
- return {
820
- sessionId: input.sessionId,
821
- revision: input.revision,
822
- loadedAt: input.loadedAt,
823
- selection: {
824
- ...(input.selection.model
825
- ? {
826
- model: {
827
- provider: input.selection.model.provider,
828
- id: input.selection.model.id,
829
- ...(input.selection.model.name
830
- ? { name: field(input.selection.model.name, 200) }
831
- : {}),
832
- },
833
- }
834
- : {}),
835
- thinkingSelection: input.selection.thinkingSelection,
836
- ...(input.selection.effectiveThinkingLevel
837
- ? { effectiveThinkingLevel: input.selection.effectiveThinkingLevel }
838
- : {}),
839
- },
840
- models: input.models.map((model) => ({
841
- provider: model.provider,
842
- id: model.id,
843
- ...(model.name ? { name: field(model.name, 200) } : {}),
844
- ...(model.hasCredentials !== undefined
845
- ? { hasCredentials: model.hasCredentials }
846
- : {}),
847
- })),
848
- thinkingLevels: input.thinkingLevels.map((option) => ({ ...option })),
849
- systemPrompt: {
850
- ...(input.systemPrompt.raw !== undefined
851
- ? { raw: input.systemPrompt.raw }
852
- : {}),
853
- text: redactRuntimeText(input.systemPrompt.text, RUNTIME_CONTEXT_TEXT_MAX),
854
- sha256: input.systemPrompt.sha256,
855
- characters: input.systemPrompt.characters,
856
- },
857
- activeTools: [...input.activeTools],
858
- contextFiles: input.contextFiles.map((file) => ({
859
- path: field(file.path, 400) ?? "",
860
- characters: file.characters,
861
- })),
862
- skills: input.skills.map(projectSkill),
863
- extensions: input.extensions.map(projectExtension),
864
- packages: input.packages.map(projectPackage),
865
- mcp: projectMcp(input.mcp ?? { servers: [], unassignedTools: [] }),
866
- diagnostics: input.diagnostics.map(projectDiagnostic),
867
- hasBash: input.hasBash,
868
- };
869
- }
295
+ export { THINKING_LEVELS, RuntimeSnapshotUnavailableError, projectRuntimeSnapshot, };
296
+ export { createDefaultPiSdkBindings } from "./pi-runtime/sdk-bindings.js";
870
297
  export { extractAssistantContentBlocks, extractAssistantText, isAssistantSdkMessage };
871
298
  function readSessionManagerId(manager) {
872
299
  if (!manager || typeof manager !== "object")
@@ -964,6 +391,7 @@ export class ConsolePiRuntime {
964
391
  /** Non-sensitive models.json metadata used to avoid reparsing unchanged config. */
965
392
  modelConfigFiles = new Map();
966
393
  sessionManagers = new Map();
394
+ toolExposures = new Map();
967
395
  /** Per-session SDK service scope (resourceLoader / settingsManager) used by the live runtime snapshot. */
968
396
  serviceScopes = new Map();
969
397
  /** Snapshot revision: bumped on create / reopen / model / thinking / reload. */
@@ -994,10 +422,18 @@ export class ConsolePiRuntime {
994
422
  exploreCustomToolsPromise;
995
423
  exploreOrchestrator;
996
424
  bindings;
425
+ browserAutomation;
426
+ ownsBrowserAutomation;
997
427
  constructor(options) {
998
428
  this.options = options;
999
429
  this.loader = createOperatorChatResourceLoader();
1000
430
  this.bindings = options.bindings ?? createDefaultPiSdkBindings();
431
+ this.ownsBrowserAutomation = options.browserAutomation === undefined;
432
+ this.browserAutomation = options.browserAutomation ?? new BrowserAutomationService({
433
+ profileDir: path.join(this.options.sessionDir, "..", "browser-profile"),
434
+ downloadsDir: path.join(this.options.sessionDir, "..", "browser-downloads"),
435
+ screenshotsDir: path.join(this.options.sessionDir, "..", "browser-screenshots"),
436
+ });
1001
437
  this.exploreOrchestrator = new ExploreOrchestrator({
1002
438
  cwd: this.options.cwd,
1003
439
  bindings: this.bindings,
@@ -1058,11 +494,10 @@ export class ConsolePiRuntime {
1058
494
  this.sessionModes.set(sessionId, sessionMode);
1059
495
  return sessionMode;
1060
496
  }
1061
- async buildResourceLoaderOptions(base = {}, sessionId) {
497
+ async buildResourceLoaderOptions(base = {}, sessionId, sessionMode = DEFAULT_CONSOLE_CHAT_SESSION_MODE) {
1062
498
  const inheritedFactories = Array.isArray(base.extensionFactories)
1063
499
  ? base.extensionFactories
1064
500
  : [];
1065
- const sessionMode = this.sessionModeOf(sessionId);
1066
501
  const todoContextExtension = sessionId && this.options.todoRegistry
1067
502
  ? createTodoCompactionContextExtension({
1068
503
  sessionId,
@@ -1072,9 +507,7 @@ export class ConsolePiRuntime {
1072
507
  const pressureExtension = sessionId
1073
508
  ? this.createContextPressureInlineExtension(sessionId)
1074
509
  : undefined;
1075
- const goalContextExtension = sessionId &&
1076
- this.options.goalRegistry &&
1077
- shouldInjectGoalContinuation(sessionMode)
510
+ const goalContextExtension = sessionId && this.options.goalRegistry
1078
511
  ? createGoalCompactionContextExtension({
1079
512
  sessionId,
1080
513
  registry: this.options.goalRegistry,
@@ -1092,6 +525,7 @@ export class ConsolePiRuntime {
1092
525
  pressureExtension,
1093
526
  wizardLocalHeadersExtension,
1094
527
  ].filter((item) => Boolean(item));
528
+ const piMode = sessionMode === "pi";
1095
529
  return {
1096
530
  ...base,
1097
531
  ...(extraFactories.length > 0
@@ -1099,18 +533,23 @@ export class ConsolePiRuntime {
1099
533
  extensionFactories: [...inheritedFactories, ...extraFactories],
1100
534
  }
1101
535
  : {}),
1102
- skillsOverride: (current) => {
536
+ ...buildPiModeLoopIsolationOptions({
537
+ enabled: piMode,
538
+ cwd: this.options.cwd,
1103
539
  // Synchronous override surface: use last-known prefs via a sync
1104
540
  // fail-closed path. Prefer async cache filled by ensurePrefs.
1105
- const prefs = this.cachedSkillPrefs ?? {
1106
- schemaVersion: 1,
1107
- skills: {},
1108
- };
1109
- return {
1110
- skills: applySkillAutoInvocationPreferences(current.skills, prefs),
1111
- diagnostics: current.diagnostics,
1112
- };
1113
- },
541
+ readPreferences: () => this.cachedSkillPrefs,
542
+ skillsOverride: (current) => {
543
+ const prefs = this.cachedSkillPrefs ?? {
544
+ schemaVersion: 1,
545
+ skills: {},
546
+ };
547
+ return {
548
+ skills: applySkillAutoInvocationPreferences(current.skills, prefs),
549
+ diagnostics: current.diagnostics,
550
+ };
551
+ },
552
+ }),
1114
553
  };
1115
554
  }
1116
555
  /** Last successfully loaded skill prefs (sync override + snapshot projection). */
@@ -1160,9 +599,11 @@ export class ConsolePiRuntime {
1160
599
  policy,
1161
600
  controller: this.getCompactionController(sessionId),
1162
601
  sift,
602
+ normalizeTransientProviderErrors: true,
1163
603
  });
1164
604
  }
1165
- applySessionCompactionOverride(sessionId, services, session) {
605
+ applySessionRecoveryOverrides(sessionId, services, session) {
606
+ applyProcessRetryOverride(services?.settingsManager);
1166
607
  const manager = services?.settingsManager;
1167
608
  const window = session?.getSessionStats?.()?.contextUsage?.contextWindow;
1168
609
  applyProcessCompactionOverride(manager, {
@@ -1180,7 +621,7 @@ export class ConsolePiRuntime {
1180
621
  const window = stats?.contextUsage?.contextWindow;
1181
622
  const tokens = stats?.contextUsage?.tokens;
1182
623
  if (typeof window === "number" && window > 0) {
1183
- this.applySessionCompactionOverride(sessionId, this.serviceScopes.get(sessionId) ?? {}, session);
624
+ this.applySessionRecoveryOverrides(sessionId, this.serviceScopes.get(sessionId) ?? {}, session);
1184
625
  }
1185
626
  const projection = projectPromptPressure({
1186
627
  currentContextTokens: tokens,
@@ -1418,31 +859,36 @@ export class ConsolePiRuntime {
1418
859
  this.sessionInit.set(sessionId, { status: "initializing", promise });
1419
860
  return promise;
1420
861
  }
1421
- async materializeSession(sessionId, init) {
1422
- if (this.disposedSessions.has(sessionId))
1423
- return;
1424
- await this.syncAimaxAuthToken();
1425
- const sessionMode = this.rememberSessionMode(sessionId, init.sessionMode ?? this.sessionModeOf(sessionId));
862
+ /**
863
+ * Shared session preparation for create/reopen/fork: config capture, prompt
864
+ * composition, resource loading, model resolution, tool assembly, SDK session
865
+ * create, extension bind, tool pin, compaction override. Lifecycle maps stay
866
+ * owned by the caller.
867
+ */
868
+ async prepareLiveSession(init) {
869
+ const aborted = () => Boolean(init.abortIfDisposed && this.disposedSessions.has(init.sessionId));
870
+ if (aborted())
871
+ return undefined;
872
+ if (init.sessionMode === "workflow" && this.options.actionContext) {
873
+ this.toolExposures.set(init.sessionId, new WorkflowToolExposure(init.sessionManager, this.options.toolExposure ?? configuredToolExposure(process.env.CONSOLE_CHAT_TOOL_EXPOSURE)));
874
+ }
1426
875
  const agentDir = await safeGetAgentDir(this.bindings);
1427
876
  // Capture before services parse models.json. A concurrent edit can therefore
1428
877
  // cause at most one harmless extra refresh, never a missed next-turn refresh.
1429
878
  const modelConfigFile = await captureModelConfigFile(agentDir);
1430
879
  const appendSystemPrompt = composeChatSystemPromptParts({
1431
- sessionMode,
880
+ sessionMode: init.sessionMode,
1432
881
  workflowPrompt: OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
1433
882
  systemPromptSuffix: init.systemPromptSuffix,
1434
883
  });
1435
- // Single services construction for formal materialization (+ optional
1436
- // default-model resolve from the same modelRuntime). Repo-scoped skill
1437
- // auto-invocation prefs inject skillsOverride without touching SKILL.md.
1438
884
  await this.refreshSkillPrefsCache().catch(() => undefined);
1439
885
  const { services } = await this.bindings.createServices({
1440
886
  cwd: this.options.cwd,
1441
887
  agentDir,
1442
- resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt }, sessionId),
888
+ resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt }, init.sessionId, init.sessionMode),
1443
889
  });
1444
- if (this.disposedSessions.has(sessionId))
1445
- return;
890
+ if (aborted())
891
+ return undefined;
1446
892
  const modelRuntime = services.modelRuntime;
1447
893
  let model = init.model;
1448
894
  if (!model && init.resolveDefaultModel) {
@@ -1456,9 +902,9 @@ export class ConsolePiRuntime {
1456
902
  }) ?? undefined)
1457
903
  : undefined;
1458
904
  assertNoWriteToolInList(OPERATOR_CHAT_ALLOWED_TOOLS);
1459
- const customTools = await this.loadCustomTools(sessionId);
1460
- if (this.disposedSessions.has(sessionId))
1461
- return;
905
+ const customTools = await this.loadCustomTools(init.sessionId);
906
+ if (aborted())
907
+ return undefined;
1462
908
  // pi-web parity: do NOT pass `tools`. A closed allowlist becomes
1463
909
  // SDK allowedToolNames and filters extension MCP tools out of the
1464
910
  // registry before Gate 2 can re-add them. Operator/safe customTools
@@ -1472,41 +918,77 @@ export class ConsolePiRuntime {
1472
918
  ...(customTools.length > 0 ? { customTools } : {}),
1473
919
  ...(resolvedModel ? { model: resolvedModel } : {}),
1474
920
  });
1475
- if (this.disposedSessions.has(sessionId)) {
921
+ if (aborted()) {
1476
922
  try {
1477
923
  session.dispose();
1478
924
  }
1479
925
  catch {
1480
926
  // ignore late dispose cleanup
1481
927
  }
1482
- return;
928
+ return undefined;
1483
929
  }
1484
- await bindSessionExtensions(session);
1485
- pinOperatorChatTools(session, sessionMode);
930
+ try {
931
+ await bindSessionExtensions(session);
932
+ this.pinSessionTools(init.sessionId, session);
933
+ }
934
+ catch (error) {
935
+ try {
936
+ session.dispose();
937
+ }
938
+ finally {
939
+ this.toolExposures.delete(init.sessionId);
940
+ }
941
+ throw error;
942
+ }
943
+ this.applySessionRecoveryOverrides(init.sessionId, services, session);
944
+ return {
945
+ session,
946
+ modelRuntime,
947
+ modelConfigFile,
948
+ model,
949
+ services: {
950
+ resourceLoader: services.resourceLoader,
951
+ settingsManager: services.settingsManager,
952
+ },
953
+ };
954
+ }
955
+ async materializeSession(sessionId, init) {
956
+ if (this.disposedSessions.has(sessionId))
957
+ return;
958
+ await this.syncAimaxAuthToken();
959
+ const sessionMode = this.rememberSessionMode(sessionId, init.sessionMode ?? this.sessionModeOf(sessionId));
960
+ const prepared = await this.prepareLiveSession({
961
+ sessionId,
962
+ sessionManager: init.sessionManager,
963
+ model: init.model,
964
+ systemPromptSuffix: init.systemPromptSuffix,
965
+ sessionMode,
966
+ resolveDefaultModel: init.resolveDefaultModel,
967
+ abortIfDisposed: true,
968
+ });
969
+ if (!prepared)
970
+ return;
1486
971
  // Register then re-check dispose race: a concurrent dispose must win.
1487
- this.sessions.set(sessionId, session);
972
+ this.sessions.set(sessionId, prepared.session);
1488
973
  this.sessionManagers.set(sessionId, init.sessionManager);
1489
- this.modelRuntimes.set(sessionId, modelRuntime);
1490
- this.modelConfigFiles.set(sessionId, modelConfigFile);
1491
- this.sessionModels.set(sessionId, model);
1492
- this.serviceScopes.set(sessionId, {
1493
- resourceLoader: services.resourceLoader,
1494
- settingsManager: services.settingsManager,
1495
- });
1496
- this.applySessionCompactionOverride(sessionId, services, session);
974
+ this.modelRuntimes.set(sessionId, prepared.modelRuntime);
975
+ this.modelConfigFiles.set(sessionId, prepared.modelConfigFile);
976
+ this.sessionModels.set(sessionId, prepared.model);
977
+ this.serviceScopes.set(sessionId, prepared.services);
1497
978
  this.revisions.set(sessionId, 1);
1498
979
  if (!this.thinkingSelections.has(sessionId)) {
1499
980
  this.thinkingSelections.set(sessionId, "auto");
1500
981
  }
1501
982
  if (this.disposedSessions.has(sessionId)) {
1502
983
  try {
1503
- session.dispose();
984
+ prepared.session.dispose();
1504
985
  }
1505
986
  catch {
1506
987
  // ignore
1507
988
  }
1508
989
  this.sessions.delete(sessionId);
1509
990
  this.sessionManagers.delete(sessionId);
991
+ this.toolExposures.delete(sessionId);
1510
992
  this.modelRuntimes.delete(sessionId);
1511
993
  this.modelConfigFiles.delete(sessionId);
1512
994
  this.sessionModels.delete(sessionId);
@@ -1516,6 +998,20 @@ export class ConsolePiRuntime {
1516
998
  this.thinkingSelections.delete(sessionId);
1517
999
  }
1518
1000
  }
1001
+ pinSessionTools(sessionId, session, interview = false) {
1002
+ const exposure = this.toolExposures.get(sessionId);
1003
+ if (exposure) {
1004
+ exposure.pin(session, interview);
1005
+ return;
1006
+ }
1007
+ if (interview) {
1008
+ session.setActiveToolsByName(filterActiveInterviewTools(chatActiveToolBaseline(this.sessionModeOf(sessionId))).allowed);
1009
+ }
1010
+ else
1011
+ pinOperatorChatTools(session, this.sessionModeOf(sessionId));
1012
+ }
1013
+ /** Read-only rollout diagnostics; no model parameters or host identities. */
1014
+ getToolExposure(sessionId) { return this.toolExposures.get(sessionId)?.snapshot; }
1519
1015
  /** Cached session-less custom tool definitions (operator + explore). */
1520
1016
  async loadCustomTools(sessionId) {
1521
1017
  const sessionMode = this.sessionModeOf(sessionId);
@@ -1526,6 +1022,11 @@ export class ConsolePiRuntime {
1526
1022
  : Promise.resolve([]),
1527
1023
  this.loadExploreCustomTools(),
1528
1024
  ]);
1025
+ const scheduledGoalTools = loadWorkflowTools && this.options.actionContext
1026
+ ? await buildScheduledGoalTools({ sessionId, actionContext: this.options.actionContext }) : [];
1027
+ const browserTools = await buildBrowserTools({
1028
+ automation: this.browserAutomation,
1029
+ });
1529
1030
  const questionTool = this.options.questionRegistry
1530
1031
  ? await buildAskUserQuestionTool({
1531
1032
  sessionId,
@@ -1539,7 +1040,7 @@ export class ConsolePiRuntime {
1539
1040
  sessionMode,
1540
1041
  })
1541
1042
  : undefined;
1542
- const goalTools = loadWorkflowTools && this.options.goalRegistry
1043
+ const goalTools = this.options.goalRegistry
1543
1044
  ? await buildGoalTools({ sessionId, registry: this.options.goalRegistry })
1544
1045
  : [];
1545
1046
  const terminalTools = this.options.terminalRegistry
@@ -1576,14 +1077,21 @@ export class ConsolePiRuntime {
1576
1077
  process.stderr.write(`[console] chat Agent tool build failed: ${error instanceof Error ? error.message : String(error)}\n`);
1577
1078
  return undefined;
1578
1079
  });
1080
+ const exposure = this.toolExposures.get(sessionId);
1081
+ const fullCapabilities = operatorCustomTools.find(tool => tool.name === "operatorCapabilities");
1082
+ // Always session-bound: a branch can restore progressive after full fallback.
1083
+ const discovery = exposure?.buildTool(fullCapabilities);
1579
1084
  return [
1580
- ...operatorCustomTools,
1085
+ ...operatorCustomTools.filter(tool => !discovery || tool.name !== "operatorCapabilities"),
1086
+ ...(discovery ? [discovery] : []),
1087
+ ...scheduledGoalTools,
1581
1088
  ...exploreCustomTools,
1582
1089
  ...(questionTool ? [questionTool] : []),
1583
1090
  ...(todoTool ? [todoTool] : []),
1584
1091
  ...goalTools,
1585
1092
  ...terminalTools,
1586
1093
  ...scmTools,
1094
+ ...browserTools,
1587
1095
  ...(agentTool ? [agentTool] : []),
1588
1096
  ];
1589
1097
  }
@@ -1666,6 +1174,7 @@ export class ConsolePiRuntime {
1666
1174
  const entry = input.summary
1667
1175
  ? manager.branchWithSummary?.(input.branchFromEntryId, input.summary)
1668
1176
  : (manager.branch(input.branchFromEntryId), undefined);
1177
+ this.pinSessionTools(input.sessionId, session);
1669
1178
  return {
1670
1179
  branchedFromEntryId: input.branchFromEntryId,
1671
1180
  ...(entry?.id ? { branchSummaryEntryId: entry.id } : {}),
@@ -1677,6 +1186,9 @@ export class ConsolePiRuntime {
1677
1186
  throw new Error(`chat session not active: ${sessionId}`);
1678
1187
  manager.branch(this.mainlineLeaves.get(sessionId));
1679
1188
  this.mainlineLeaves.delete(sessionId);
1189
+ const session = this.sessions.get(sessionId);
1190
+ if (session)
1191
+ this.pinSessionTools(sessionId, session);
1680
1192
  }
1681
1193
  getBranchContext(sessionId) {
1682
1194
  const manager = this.sessionManagers.get(sessionId);
@@ -1934,9 +1446,9 @@ export class ConsolePiRuntime {
1934
1446
  await session.reload();
1935
1447
  const services = this.serviceScopes.get(sessionId);
1936
1448
  if (services)
1937
- this.applySessionCompactionOverride(sessionId, services, session);
1449
+ this.applySessionRecoveryOverrides(sessionId, services, session);
1938
1450
  await this.refreshModelConfiguration(sessionId, "force");
1939
- pinOperatorChatTools(session, this.sessionModeOf(sessionId));
1451
+ this.pinSessionTools(sessionId, session);
1940
1452
  }
1941
1453
  catch (error) {
1942
1454
  return {
@@ -1967,91 +1479,14 @@ export class ConsolePiRuntime {
1967
1479
  * Never returns prompt/skill file bodies.
1968
1480
  */
1969
1481
  async listSlashCommands(sessionId) {
1970
- const scrub = (value) => {
1971
- const raw = String(value ?? "");
1972
- return raw
1973
- .replace(/\/Users\/[^\s]+/gi, "~")
1974
- .replace(/[A-Za-z]:\\Users\\[^\s]+/gi, "~")
1975
- .slice(0, 240);
1976
- };
1977
1482
  // Fail soft at the HTTP layer: bubble readiness errors so routes can attach
1978
1483
  // a compact warning while the UI keeps local Operator/Pi Web commands.
1979
1484
  await this.ensureSessionReady(sessionId);
1980
1485
  const session = this.sessions.get(sessionId);
1981
1486
  if (!session)
1982
1487
  return { commands: [] };
1983
- // Same-name conflict priority (AC-3): extension > skill > prompt.
1984
- // Enum order must not decide the winner — prompt-before-skill still loses.
1985
- const SOURCE_RANK = {
1986
- extension: 0,
1987
- skill: 1,
1988
- prompt: 2,
1989
- };
1990
- const byKey = new Map();
1991
- const sourceWarnings = [];
1992
- const push = (command, label, description, source) => {
1993
- const name = command.startsWith("/") ? command : `/${command}`;
1994
- const key = name.toLowerCase();
1995
- if (!key || key === "/")
1996
- return;
1997
- const next = {
1998
- command: name,
1999
- label: (label || name).slice(0, 80),
2000
- description: scrub(description),
2001
- source,
2002
- };
2003
- const existing = byKey.get(key);
2004
- if (!existing || SOURCE_RANK[source] < SOURCE_RANK[existing.source]) {
2005
- byKey.set(key, next);
2006
- }
2007
- };
2008
- try {
2009
- const cmds = session.extensionRunner?.getRegisteredCommands?.() ?? [];
2010
- for (const cmd of cmds) {
2011
- const name = cmd.invocationName || cmd.name;
2012
- if (!name)
2013
- continue;
2014
- push(String(name), String(cmd.name || name), String(cmd.description || ""), "extension");
2015
- }
2016
- }
2017
- catch (error) {
2018
- // Source isolation: keep other sources; surface compact warning (AC-3).
2019
- sourceWarnings.push(`extension: ${scrub(error instanceof Error ? error.message : String(error))}`);
2020
- }
2021
- try {
2022
- // Enumerate prompts before skills on purpose: priority must still let skill win.
2023
- for (const tpl of session.promptTemplates ?? []) {
2024
- if (!tpl?.name)
2025
- continue;
2026
- push(String(tpl.name), String(tpl.name), String(tpl.description || ""), "prompt");
2027
- }
2028
- }
2029
- catch (error) {
2030
- sourceWarnings.push(`prompt: ${scrub(error instanceof Error ? error.message : String(error))}`);
2031
- }
2032
- try {
2033
- const services = this.serviceScopes.get(sessionId);
2034
- const resourceLoader = services?.resourceLoader;
2035
- const skills = resourceLoader?.getSkills?.()?.skills ?? [];
2036
- for (const skill of skills) {
2037
- if (!skill?.name)
2038
- continue;
2039
- const command = skill.name.startsWith("skill:")
2040
- ? `/${skill.name}`
2041
- : `/skill:${skill.name}`;
2042
- push(command, skill.name, String(skill.description || ""), "skill");
2043
- }
2044
- }
2045
- catch (error) {
2046
- sourceWarnings.push(`skill: ${scrub(error instanceof Error ? error.message : String(error))}`);
2047
- }
2048
- const warning = sourceWarnings.length > 0
2049
- ? sourceWarnings.join("; ").slice(0, 160)
2050
- : undefined;
2051
- return {
2052
- commands: [...byKey.values()],
2053
- ...(warning ? { warning } : {}),
2054
- };
1488
+ const resourceLoader = this.serviceScopes.get(sessionId)?.resourceLoader;
1489
+ return collectSlashCommands({ session, resourceLoader });
2055
1490
  }
2056
1491
  async getRuntimeSnapshot(sessionId, options) {
2057
1492
  try {
@@ -2095,9 +1530,6 @@ export class ConsolePiRuntime {
2095
1530
  throw new RuntimeSnapshotUnavailableError(`model enumeration failed: ${error instanceof Error ? error.message : String(error)}`);
2096
1531
  }
2097
1532
  }
2098
- // Thinking options: auto is always offered; explicit levels only when the
2099
- // runtime session supports setThinkingLevel (xhigh/max ship with the SDK
2100
- // and therefore surface only when this runtime provides them).
2101
1533
  const supportedLevels = typeof session.setThinkingLevel === "function"
2102
1534
  ? [...THINKING_LEVELS]
2103
1535
  : [];
@@ -2105,220 +1537,25 @@ export class ConsolePiRuntime {
2105
1537
  { value: "auto" },
2106
1538
  ...supportedLevels.map((value) => ({ value })),
2107
1539
  ];
2108
- // Resource inventories from the session ResourceLoader (ADR 0012 open
2109
- // surface). Missing loader → real empty lists, never fabricated entries.
2110
- // Each subface is isolated (RF-02): a throwing subface keeps the other
2111
- // inventories AND surfaces a structured retryable diagnostic instead of
2112
- // silently dropping the whole resource projection.
2113
- const contextFiles = [];
2114
- const skills = [];
2115
- const extensions = [];
2116
- const diagnostics = [];
2117
- const resolvedResources = [];
2118
- const resourceFailure = (subface, error) => {
2119
- diagnostics.push({
2120
- type: "error",
2121
- code: "RESOURCE_LOAD_FAILED",
2122
- message: `${subface} inventory failed: ${error instanceof Error ? error.message : String(error)}`,
2123
- retryable: true,
2124
- });
2125
- };
1540
+ let skillPrefs = { schemaVersion: 1, skills: {} };
1541
+ let skillPrefsError;
2126
1542
  if (resourceLoader) {
2127
- try {
2128
- const agentsFiles = resourceLoader.getAgentsFiles?.();
2129
- for (const file of agentsFiles?.agentsFiles ?? []) {
2130
- if (!file.path)
2131
- continue;
2132
- contextFiles.push({
2133
- path: file.path,
2134
- characters: file.content?.length ?? 0,
2135
- });
2136
- }
2137
- }
2138
- catch (error) {
2139
- resourceFailure("context files", error);
2140
- }
2141
- // Preferences are the source of truth for UI auto-invocation state;
2142
- // loader disableModelInvocation may also reflect frontmatter. Prefer
2143
- // explicit preference (default on) when projecting autoInvocationEnabled.
2144
- let skillPrefs = { schemaVersion: 1, skills: {} };
2145
1543
  try {
2146
1544
  skillPrefs = await this.loadSkillAutoInvocationPreferences();
2147
1545
  }
2148
1546
  catch (error) {
2149
- diagnostics.push({
2150
- type: "error",
2151
- code: "PI_RESOURCE_PREFERENCES_MALFORMED",
2152
- message: error instanceof Error ? error.message : String(error),
2153
- retryable: true,
2154
- });
1547
+ skillPrefsError = error;
2155
1548
  }
2156
- try {
2157
- const skillsResult = resourceLoader.getSkills?.();
2158
- for (const skill of skillsResult?.skills ?? []) {
2159
- const resourceId = skillResourceId({
2160
- name: skill.name,
2161
- filePath: skill.filePath,
2162
- });
2163
- // Preference off always means autoInvocationEnabled=false even if
2164
- // loader already applied disableModelInvocation. Frontmatter-only
2165
- // disable (no preference) also surfaces as autoInvocationEnabled=false.
2166
- const prefEnabled = isAutoInvocationEnabled(skillPrefs, resourceId);
2167
- const autoInvocationEnabled = prefEnabled && skill.disableModelInvocation !== true;
2168
- skills.push({
2169
- name: skill.name,
2170
- description: skill.description ?? "",
2171
- path: skill.filePath,
2172
- ...(skill.baseDir ? { baseDir: skill.baseDir } : {}),
2173
- scope: skill.sourceInfo?.scope ?? "unknown",
2174
- source: skill.sourceInfo?.source ?? "",
2175
- resourceId,
2176
- autoInvocationEnabled,
2177
- diagnostics: [],
2178
- });
2179
- resolvedResources.push({
2180
- type: "skill",
2181
- name: skill.name,
2182
- path: skill.filePath,
2183
- source: skill.sourceInfo?.source ?? "",
2184
- scope: skill.sourceInfo?.scope ?? "unknown",
2185
- });
2186
- }
2187
- for (const diag of skillsResult?.diagnostics ?? []) {
2188
- if (!diag?.message)
2189
- continue;
2190
- diagnostics.push({
2191
- type: diag.type ?? "warning",
2192
- message: diag.message,
2193
- ...(diag.path ? { path: diag.path } : {}),
2194
- });
2195
- }
2196
- }
2197
- catch (error) {
2198
- resourceFailure("skills", error);
2199
- }
2200
- try {
2201
- const extResult = resourceLoader.getExtensions?.();
2202
- for (const ext of extResult?.extensions ?? []) {
2203
- const path = ext.resolvedPath ?? ext.path ?? "";
2204
- extensions.push({
2205
- name: ext.sourceInfo?.source ?? ext.path ?? "extension",
2206
- path,
2207
- ...(ext.resolvedPath ? { resolvedPath: ext.resolvedPath } : {}),
2208
- source: ext.sourceInfo?.source ?? "",
2209
- scope: ext.sourceInfo?.scope ?? "unknown",
2210
- ...(ext.sourceInfo?.origin ? { origin: ext.sourceInfo.origin } : {}),
2211
- enabled: true,
2212
- });
2213
- resolvedResources.push({
2214
- type: "extension",
2215
- name: ext.sourceInfo?.source ?? ext.path ?? "extension",
2216
- path,
2217
- source: ext.sourceInfo?.source ?? "",
2218
- scope: ext.sourceInfo?.scope ?? "unknown",
2219
- });
2220
- }
2221
- for (const err of extResult?.errors ?? []) {
2222
- if (!err?.error)
2223
- continue;
2224
- diagnostics.push({
2225
- type: "error",
2226
- message: err.error,
2227
- ...(err.path ? { path: err.path } : {}),
2228
- });
2229
- }
2230
- }
2231
- catch (error) {
2232
- resourceFailure("extensions", error);
2233
- }
2234
- try {
2235
- const promptsResult = resourceLoader.getPrompts?.();
2236
- for (const prompt of promptsResult?.prompts ?? []) {
2237
- resolvedResources.push({
2238
- type: "prompt",
2239
- name: prompt.name,
2240
- path: prompt.filePath,
2241
- source: prompt.sourceInfo?.source ?? "",
2242
- scope: prompt.sourceInfo?.scope ?? "unknown",
2243
- });
2244
- }
2245
- for (const diag of promptsResult?.diagnostics ?? []) {
2246
- if (!diag?.message)
2247
- continue;
2248
- diagnostics.push({
2249
- type: diag.type ?? "warning",
2250
- message: diag.message,
2251
- ...(diag.path ? { path: diag.path } : {}),
2252
- });
2253
- }
2254
- }
2255
- catch (error) {
2256
- resourceFailure("prompts", error);
2257
- }
2258
- try {
2259
- const themesResult = resourceLoader.getThemes?.();
2260
- for (const theme of themesResult?.themes ?? []) {
2261
- if (!theme.path)
2262
- continue;
2263
- resolvedResources.push({
2264
- type: "theme",
2265
- name: theme.name ?? theme.path,
2266
- path: theme.path,
2267
- source: "",
2268
- scope: "unknown",
2269
- });
2270
- }
2271
- for (const diag of themesResult?.diagnostics ?? []) {
2272
- if (!diag?.message)
2273
- continue;
2274
- diagnostics.push({
2275
- type: diag.type ?? "warning",
2276
- message: diag.message,
2277
- ...(diag.path ? { path: diag.path } : {}),
2278
- });
2279
- }
2280
- }
2281
- catch (error) {
2282
- resourceFailure("themes", error);
2283
- }
2284
- }
2285
- // Packages: Pi settings specs + resolved resource metadata. Fields the SDK
2286
- // does not expose stay absent (real missing, never fabricated). A
2287
- // readPackageInventory failure is a PARTIAL failure (RF-02): the snapshot
2288
- // stays 200 with a structured retryable diagnostic — never a silent
2289
- // fake-empty package list.
2290
- const { readPackageInventory } = await import("./pi-console-config.js");
2291
- let packages = [];
2292
- try {
2293
- packages = (await readPackageInventory(await safeGetAgentDir(this.bindings), this.options.cwd, resolvedResources));
2294
- }
2295
- catch (error) {
2296
- packages = [];
2297
- diagnostics.push({
2298
- type: "error",
2299
- code: "PACKAGE_INVENTORY_FAILED",
2300
- message: error instanceof Error ? error.message : String(error),
2301
- retryable: true,
2302
- });
2303
- }
2304
- const { readMcpInventory } = await import("./mcp-inventory.js");
2305
- let mcp = { servers: [], unassignedTools: [] };
2306
- try {
2307
- mcp = await readMcpInventory({
2308
- agentDir: await safeGetAgentDir(this.bindings),
2309
- cwd: this.options.cwd,
2310
- tools: session.getAllTools?.() ?? [],
2311
- activeToolNames: session.getActiveToolNames(),
2312
- });
2313
- }
2314
- catch (error) {
2315
- diagnostics.push({
2316
- type: "error",
2317
- code: "MCP_INVENTORY_FAILED",
2318
- message: error instanceof Error ? error.message : String(error),
2319
- retryable: true,
2320
- });
2321
1549
  }
1550
+ const inventories = await collectRuntimeInventories({
1551
+ resourceLoader,
1552
+ skillPrefs,
1553
+ skillPrefsError,
1554
+ agentDir: await safeGetAgentDir(this.bindings),
1555
+ cwd: this.options.cwd,
1556
+ tools: session.getAllTools?.() ?? [],
1557
+ activeToolNames: session.getActiveToolNames(),
1558
+ });
2322
1559
  return projectRuntimeSnapshot({
2323
1560
  sessionId,
2324
1561
  revision: this.revisions.get(sessionId) ?? 0,
@@ -2343,19 +1580,24 @@ export class ConsolePiRuntime {
2343
1580
  thinkingLevels,
2344
1581
  systemPrompt: {
2345
1582
  ...(options?.includeRaw ? { raw: rawSystemPrompt } : {}),
2346
- // The RAW prompt flows into the projection: text is scrubbed +
2347
- // length-limited while sha256/characters stay RAW-based (design §2.2).
2348
1583
  text: rawSystemPrompt,
2349
1584
  sha256: createHash("sha256").update(rawSystemPrompt).digest("hex"),
2350
1585
  characters: rawSystemPrompt.length,
2351
1586
  },
2352
1587
  activeTools: session.getActiveToolNames(),
2353
- contextFiles,
2354
- skills,
2355
- extensions,
2356
- packages,
2357
- mcp,
2358
- diagnostics,
1588
+ contextFiles: inventories.contextFiles,
1589
+ skills: inventories.skills,
1590
+ extensions: inventories.extensions,
1591
+ packages: inventories.packages,
1592
+ mcp: inventories.mcp,
1593
+ diagnostics: [
1594
+ ...inventories.diagnostics,
1595
+ ...(this.toolExposures.has(sessionId) ? [{
1596
+ type: "info",
1597
+ code: "WORKFLOW_TOOL_EXPOSURE",
1598
+ message: JSON.stringify(this.toolExposures.get(sessionId).snapshot),
1599
+ }] : []),
1600
+ ],
2359
1601
  hasBash: true,
2360
1602
  });
2361
1603
  }
@@ -2534,47 +1776,21 @@ export class ConsolePiRuntime {
2534
1776
  */
2535
1777
  async buildSessionWithServices(init) {
2536
1778
  const sessionMode = this.rememberSessionMode(init.sessionId, init.sessionMode ?? this.sessionModeOf(init.sessionId));
2537
- const agentDir = await safeGetAgentDir(this.bindings);
2538
- const modelConfigFile = await captureModelConfigFile(agentDir);
2539
- const appendSystemPrompt = composeChatSystemPromptParts({
2540
- sessionMode,
2541
- workflowPrompt: OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
2542
- systemPromptSuffix: init.systemPromptSuffix,
2543
- });
2544
- await this.refreshSkillPrefsCache().catch(() => undefined);
2545
- const { services } = await this.bindings.createServices({
2546
- cwd: this.options.cwd,
2547
- agentDir,
2548
- resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt }, init.sessionId),
2549
- });
2550
- const modelRuntime = services.modelRuntime;
2551
- const resolvedModel = init.model
2552
- ? (this.bindings.resolveModel({
2553
- modelRuntime,
2554
- provider: init.model.provider,
2555
- modelId: init.model.modelId,
2556
- }) ?? undefined)
2557
- : undefined;
2558
- assertNoWriteToolInList(OPERATOR_CHAT_ALLOWED_TOOLS);
2559
- const customTools = await this.loadCustomTools(init.sessionId);
2560
- const { session } = await this.bindings.createSessionFromServices({
2561
- services,
1779
+ const prepared = await this.prepareLiveSession({
1780
+ sessionId: init.sessionId,
2562
1781
  sessionManager: init.sessionManager,
2563
- excludeTools: [...OPERATOR_CHAT_SESSION_EXCLUDE_TOOLS],
2564
- ...(customTools.length > 0 ? { customTools } : {}),
2565
- ...(resolvedModel ? { model: resolvedModel } : {}),
1782
+ model: init.model,
1783
+ systemPromptSuffix: init.systemPromptSuffix,
1784
+ sessionMode,
2566
1785
  });
2567
- await bindSessionExtensions(session);
2568
- pinOperatorChatTools(session, sessionMode);
2569
- this.applySessionCompactionOverride(init.sessionId, services, session);
1786
+ if (!prepared) {
1787
+ throw new Error(`chat session preparation aborted: ${init.sessionId}`);
1788
+ }
2570
1789
  return {
2571
- session,
2572
- modelRuntime,
2573
- modelConfigFile,
2574
- services: {
2575
- resourceLoader: services.resourceLoader,
2576
- settingsManager: services.settingsManager,
2577
- },
1790
+ session: prepared.session,
1791
+ modelRuntime: prepared.modelRuntime,
1792
+ modelConfigFile: prepared.modelConfigFile,
1793
+ services: prepared.services,
2578
1794
  };
2579
1795
  }
2580
1796
  /** Re-open a previously created session (cross-request). */
@@ -2676,8 +1892,7 @@ export class ConsolePiRuntime {
2676
1892
  if (options?.turnId && this.options.todoRegistry) {
2677
1893
  this.options.todoRegistry.startTurn(sessionId, options.turnId);
2678
1894
  }
2679
- if (this.options.goalRegistry &&
2680
- shouldInjectGoalContinuation(this.sessionModeOf(sessionId))) {
1895
+ if (this.options.goalRegistry) {
2681
1896
  this.options.goalRegistry.startTurn(sessionId, text);
2682
1897
  }
2683
1898
  // AC-FIX-002: if Stop already fired before readiness, never start model work.
@@ -2699,11 +1914,10 @@ export class ConsolePiRuntime {
2699
1914
  // explicitly restores the normal Operator Chat set (baseline + extension
2700
1915
  // tools, ADR 0012).
2701
1916
  const sessionMode = this.sessionModeOf(sessionId);
2702
- const requestedTools = chatActiveToolBaseline(sessionMode);
2703
- const activeTools = options?.mode === "requirement-interview" && sessionMode === "workflow"
2704
- ? filterActiveInterviewTools(requestedTools).allowed
2705
- : computeChatActiveToolNames(session, sessionMode);
2706
- session.setActiveToolsByName(activeTools);
1917
+ this.pinSessionTools(sessionId, session, options?.mode === "requirement-interview" && sessionMode === "workflow");
1918
+ if (sessionMode === "workflow" && isScheduledBookingTurn(text)) {
1919
+ pinScheduledBookingTurnTools(session);
1920
+ }
2707
1921
  // Final pre-prompt fence: abort after tool pin must still skip session.prompt.
2708
1922
  if (options?.signal?.aborted) {
2709
1923
  return;
@@ -3036,6 +2250,7 @@ export class ConsolePiRuntime {
3036
2250
  }
3037
2251
  this.sessions.delete(sessionId);
3038
2252
  this.sessionManagers.delete(sessionId);
2253
+ this.toolExposures.delete(sessionId);
3039
2254
  this.mainlineLeaves.delete(sessionId);
3040
2255
  this.modelRuntimes.delete(sessionId);
3041
2256
  this.modelConfigFiles.delete(sessionId);
@@ -3052,6 +2267,8 @@ export class ConsolePiRuntime {
3052
2267
  for (const id of [...this.sessions.keys()])
3053
2268
  this.dispose(id);
3054
2269
  void this.exploreOrchestrator.abortAll();
2270
+ if (this.ownsBrowserAutomation)
2271
+ void this.browserAutomation.dispose();
3055
2272
  }
3056
2273
  }
3057
2274
  function mapAssistantMessageEvent(type, sessionId, message) {
@@ -3208,153 +2425,6 @@ async function waitForIdle(session, options) {
3208
2425
  await new Promise((r) => setTimeout(r, 50));
3209
2426
  }
3210
2427
  }
3211
- async function safeGetAgentDir(bindings) {
3212
- try {
3213
- return await bindings.getAgentDir();
3214
- }
3215
- catch {
3216
- // Fall back to a Console-app-data scoped agent dir so we never accidentally
3217
- // touch the user's real ~/.pi/agent config if getAgentDir throws.
3218
- return path.join(process.cwd(), ".harness", "console", "pi-agent-dir");
3219
- }
3220
- }
3221
- /**
3222
- * Cached SDK module loader. The pi-coding-agent package is ESM-only, so we
3223
- * must use dynamic import() (not require). Cached to avoid repeated imports.
3224
- */
3225
- let cachedSdk;
3226
- async function loadSdk() {
3227
- if (!cachedSdk) {
3228
- cachedSdk = import("@earendil-works/pi-coding-agent");
3229
- }
3230
- return cachedSdk;
3231
- }
3232
- /**
3233
- * Default production bindings backed by the real Pi SDK (services mode).
3234
- * Dynamically imported so the worker bundle does not hard-fail if the SDK is
3235
- * absent at module load (discovered lazily by pi-readiness instead).
3236
- */
3237
- export function createDefaultPiSdkBindings() {
3238
- return {
3239
- getAgentDir: async () => {
3240
- const sdk = (await loadSdk());
3241
- return sdk.getAgentDir();
3242
- },
3243
- createServices: async (opts) => {
3244
- const sdk = (await loadSdk());
3245
- // createAgentSessionServices returns the full AgentSessionServices object
3246
- // directly (cwd/agentDir/modelRuntime/settingsManager/resourceLoader).
3247
- // Wrap it so our type contract is satisfied.
3248
- const result = await sdk.createAgentSessionServices({
3249
- cwd: opts.cwd,
3250
- agentDir: opts.agentDir,
3251
- resourceLoaderOptions: opts.resourceLoaderOptions,
3252
- });
3253
- return { services: result };
3254
- },
3255
- createSessionFromServices: async (opts) => {
3256
- const sdk = (await loadSdk());
3257
- return sdk.createAgentSessionFromServices({
3258
- services: opts.services,
3259
- sessionManager: opts.sessionManager,
3260
- ...(opts.tools ? { tools: opts.tools } : {}),
3261
- ...(opts.noTools ? { noTools: opts.noTools } : {}),
3262
- ...(opts.excludeTools ? { excludeTools: opts.excludeTools } : {}),
3263
- ...(opts.customTools ? { customTools: opts.customTools } : {}),
3264
- ...(opts.model ? { model: opts.model } : {}),
3265
- });
3266
- },
3267
- createSessionManager: async (opts) => {
3268
- const sdk = (await loadSdk());
3269
- return sdk.SessionManager.create(opts.cwd, opts.sessionDir, {
3270
- id: opts.sessionId,
3271
- });
3272
- },
3273
- materializeSessionShell: async (opts) => {
3274
- const sdk = (await loadSdk());
3275
- const manager = opts.sessionManager;
3276
- if (typeof manager.setSessionFile !== "function") {
3277
- throw new Error("Pi SessionManager cannot materialize a session file");
3278
- }
3279
- let created = false;
3280
- try {
3281
- await mkdir(path.dirname(opts.sessionFile), { recursive: true });
3282
- // Match Pi's documented session header. Rebinding through the public
3283
- // SessionManager API marks it flushed, so Pi appends the first user
3284
- // entry instead of trying to create the already-present file later.
3285
- await writeFile(opts.sessionFile, `${JSON.stringify({
3286
- type: "session",
3287
- version: typeof sdk.CURRENT_SESSION_VERSION === "number"
3288
- ? sdk.CURRENT_SESSION_VERSION
3289
- : 3,
3290
- id: opts.sessionId,
3291
- timestamp: new Date().toISOString(),
3292
- cwd: opts.cwd,
3293
- })}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
3294
- created = true;
3295
- manager.setSessionFile(opts.sessionFile);
3296
- }
3297
- catch (error) {
3298
- if (created)
3299
- await rm(opts.sessionFile, { force: true }).catch(() => undefined);
3300
- throw error;
3301
- }
3302
- },
3303
- createInMemorySessionManager: async (opts) => {
3304
- const sdk = (await loadSdk());
3305
- return sdk.SessionManager.inMemory(opts.cwd);
3306
- },
3307
- forkSessionManager: async (opts) => {
3308
- const sdk = (await loadSdk());
3309
- return sdk.SessionManager.forkFrom(opts.sourcePath, opts.targetCwd, opts.targetSessionDir, { id: opts.targetSessionId });
3310
- },
3311
- openSessionManager: async (opts) => {
3312
- const sdk = (await loadSdk());
3313
- try {
3314
- // Pi SessionManager.open(missingPath) does NOT throw — it creates a
3315
- // fresh in-memory session with a random UUID. That would reopen the
3316
- // wrong conversation under the durable Console sessionId. Fail closed
3317
- // before open when the persisted file is absent.
3318
- await access(opts.sessionFile);
3319
- }
3320
- catch (error) {
3321
- const code = error && typeof error === "object" && "code" in error
3322
- ? String(error.code ?? "")
3323
- : "";
3324
- if (code === "ENOENT") {
3325
- throw Object.assign(new Error(`SESSION_FILE_MISSING: ${opts.sessionFile}`), { code: "SESSION_FILE_MISSING", cause: error });
3326
- }
3327
- // File exists but is unreadable (permissions / I/O) — caller maps
3328
- // undefined to 503 rather than silently creating a fresh session.
3329
- return undefined;
3330
- }
3331
- return sdk.SessionManager.open(opts.sessionFile);
3332
- },
3333
- resolveModel: (opts) => {
3334
- const rt = opts.modelRuntime;
3335
- if (typeof rt?.getModel !== "function")
3336
- return undefined;
3337
- try {
3338
- return rt.getModel(opts.provider, opts.modelId);
3339
- }
3340
- catch {
3341
- return undefined;
3342
- }
3343
- },
3344
- listAvailableModels: (opts) => {
3345
- const rt = opts.modelRuntime;
3346
- const available = rt?.snapshot?.available ?? [];
3347
- const configured = rt?.snapshot?.configuredProviders;
3348
- const isConfigured = (provider) => configured instanceof Set ? configured.has(provider) : false;
3349
- return available.map((m) => ({
3350
- id: m.id,
3351
- name: m.name,
3352
- provider: m.provider,
3353
- hasCredentials: isConfigured(m.provider),
3354
- }));
3355
- },
3356
- };
3357
- }
3358
2428
  /**
3359
2429
  * Re-export the tool gates for the dispatcher layer so the per-tool-call gate
3360
2430
  * (Gate 3) lives in exactly one place. authorizeChatSessionTool is the