@tea-agent/loop-agent 0.43.0-next.17 → 0.43.0-next.19

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 (236) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +68 -2309
  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/cursor-prompt.js +2 -0
  12. package/dist/commands/dag-approve.js +2 -0
  13. package/dist/commands/dag-final-verification.js +2 -0
  14. package/dist/commands/dag-init-hybrid.js +2 -0
  15. package/dist/commands/dag-reconcile-tasks.js +2 -0
  16. package/dist/commands/dag-reject.js +2 -0
  17. package/dist/commands/dag-report.js +2 -0
  18. package/dist/commands/dag-rerun.js +2 -0
  19. package/dist/commands/dag-resume.js +2 -0
  20. package/dist/commands/dag-workflow-compile.js +2 -0
  21. package/dist/commands/dag-workflow-plan.js +2 -0
  22. package/dist/commands/dag-workflow-validate.js +2 -0
  23. package/dist/commands/delegate.js +2 -0
  24. package/dist/commands/examples.js +2 -0
  25. package/dist/commands/import-prd.js +2 -0
  26. package/dist/commands/init-model-catalog.js +2 -0
  27. package/dist/commands/init.js +19 -13
  28. package/dist/commands/instructions.js +2 -0
  29. package/dist/commands/knowledge.js +2 -0
  30. package/dist/commands/loop-benchmark.js +2 -0
  31. package/dist/commands/pi-prompt.js +2 -0
  32. package/dist/commands/pi-reuse-benchmark.js +2 -0
  33. package/dist/commands/promote-run.js +2 -0
  34. package/dist/commands/stats.js +1 -1
  35. package/dist/commands/study-init.js +2 -0
  36. package/dist/commands/task-advance.js +34 -0
  37. package/dist/commands/task-source-prepare.js +2 -0
  38. package/dist/commands/worktree-create.js +2 -0
  39. package/dist/commands/worktree-remove.js +2 -0
  40. package/dist/executors/dag-pi-executor.js +1640 -194
  41. package/dist/executors/pi-executor.js +85 -3
  42. package/dist/executors/pi-sdk-executor.js +18 -0
  43. package/dist/executors/shell-executor.js +92 -7
  44. package/dist/executors/shell-verification.js +3 -0
  45. package/dist/governance/exec-plans.js +6 -3
  46. package/dist/infrastructure/console/app-data.js +6 -0
  47. package/dist/shared/artifacts-core.js +2 -0
  48. package/dist/shared/backend-dogfood-preflight.js +47 -0
  49. package/dist/shared/dag-failure-category.js +3 -0
  50. package/dist/shared/git-progress.js +2 -0
  51. package/dist/shared/one-shot-prompt-args.js +2 -0
  52. package/dist/shared/operator/capabilities.js +180 -0
  53. package/dist/shared/package-metadata.js +6 -4
  54. package/dist/shared/pi-context-pressure/sift-bridge.js +1 -1
  55. package/dist/shared/reference-context.js +2 -0
  56. package/dist/task/config-types.js +29 -0
  57. package/dist/task/contract/project.js +9 -0
  58. package/dist/task/contract/recover.js +6 -0
  59. package/dist/task/contract/schema.js +17 -0
  60. package/dist/task/source-prepare/artifact-meta.js +9 -2
  61. package/dist/task/source-prepare/build-draft.js +60 -0
  62. package/dist/task/source-prepare/fragment-inventory.js +4 -1
  63. package/dist/task/source-prepare/parse-intent.js +36 -11
  64. package/dist/task/task-demand-routing.js +5 -2
  65. package/dist/worker/console/chat/browser-automation.js +468 -308
  66. package/dist/worker/console/chat/browser-routes.js +175 -57
  67. package/dist/worker/console/chat/explore-tools.js +5 -2
  68. package/dist/worker/console/chat/pi-mode-loop-isolation.js +155 -0
  69. package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +115 -20
  70. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +24 -16
  71. package/dist/worker/console/chat/pi-runtime/custom-tools/scheduled-goal-tools.js +22 -0
  72. package/dist/worker/console/chat/pi-runtime/progressive-tools.js +195 -0
  73. package/dist/worker/console/chat/pi-runtime.js +95 -28
  74. package/dist/worker/console/chat/routes.js +20 -1
  75. package/dist/worker/console/chat/scheduled-goal-booking.js +59 -0
  76. package/dist/worker/console/chat/scheduled-goal-delivery.js +27 -0
  77. package/dist/worker/console/chat/scheduled-goal-request.js +190 -0
  78. package/dist/worker/console/chat/sdd-data-alignment.js +1 -1
  79. package/dist/worker/console/chat/session-mode.js +8 -4
  80. package/dist/worker/console/chat/session-store.js +42 -9
  81. package/dist/worker/console/chat/tool-preview.js +115 -0
  82. package/dist/worker/console/chat/tools.js +7 -1
  83. package/dist/worker/console/chat/turn-order.js +13 -0
  84. package/dist/worker/console/chat/turn-process.js +32 -30
  85. package/dist/worker/console/operator-actions.js +50 -0
  86. package/dist/worker/console/prd-intake-bridge.js +54 -1
  87. package/dist/worker/console/scheduled-goal-host.js +98 -0
  88. package/dist/worker/console/scheduled-goal-operation-adapter.js +127 -0
  89. package/dist/worker/console/server.js +15 -0
  90. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4NXyedy.js → abnfDiagram-N423BO3Z-8-j6y-sd.js} +1 -1
  91. package/dist/worker/console/static/assets/{arc-NZ3qXvBI.js → arc-eoQiMvuk.js} +1 -1
  92. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-COZSRvml.js → architectureDiagram-T3A2C74G-C4A3uMcI.js} +1 -1
  93. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Ck0Gxi9C.js → blockDiagram-VBNYF7ZC-D4zD2F-Q.js} +1 -1
  94. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BX5HQ4Ox.js → c4Diagram-5PPSVZJV-j1RkJziL.js} +1 -1
  95. package/dist/worker/console/static/assets/channel-ChE7y-cx.js +1 -0
  96. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CoviemSL.js → chunk-2GRJ4B5K-YBHmhik1.js} +1 -1
  97. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DbOdOkbZ.js → chunk-2Q5K7J3B-COfWyo9P.js} +1 -1
  98. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BsG2pmdQ.js → chunk-5RXB4S5H-I99OUkHY.js} +1 -1
  99. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DGdvrNm8.js → chunk-5VM5RSS4-XKxoJNJ4.js} +1 -1
  100. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BAy7Yd6y.js → chunk-6Q2QTUOP-DLT_cYx1.js} +1 -1
  101. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D9UA7hUb.js → chunk-GF5L2VYU-CxcKZ9mV.js} +1 -1
  102. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DPz7SZ8V.js → chunk-JWPE2WC7-V1EqXdjY.js} +1 -1
  103. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BmuEOIh7.js → chunk-KBJHAD2P-DebTtdFp.js} +1 -1
  104. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CIhBWnd8.js → chunk-RYQCIY6F-B-ivNRDf.js} +1 -1
  105. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CScR6mDD.js → chunk-XXDRQBXY-DC_Ds11b.js} +1 -1
  106. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +1 -0
  107. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +1 -0
  108. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BjpfWaUr.js → cose-bilkent-JH36ORCC-sWEqKwIb.js} +1 -1
  109. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BFQohJgT.js → cynefin-VYW2F7L2-BXa_dcu4.js} +1 -1
  110. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-D3gzRJnS.js → cynefinDiagram-MW4NZA55-C-Mle84F.js} +1 -1
  111. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-5eojkhoA.js → dagre-VZM6K2ZE-CXPDBITe.js} +1 -1
  112. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CpD8LWu_.js → diagram-7IWD3JNH-CC-WJQfa.js} +1 -1
  113. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B8_4qJ2a.js → diagram-B4RE2ZJO-CDAV5Vs4.js} +1 -1
  114. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-HOsXf0eu.js → diagram-LBJQPF4R-CmFiAcNz.js} +1 -1
  115. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BqwRwiWP.js → diagram-Q27KOJAE-DPBZHuyn.js} +1 -1
  116. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BRVCNZe1.js → diagram-UB23O5K3-jUlm_Ds3.js} +1 -1
  117. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Cvb1TgGh.js → ebnfDiagram-BXEA7PRR-DWhQ3mfY.js} +1 -1
  118. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DS0O0qG9.js → erDiagram-JOGREHBK-Tr2gMqet.js} +1 -1
  119. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BHIRvRr3.js → flowDiagram-UKHOOZJN-DJjVQHPA.js} +1 -1
  120. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DcCJ1JWx.js → ganttDiagram-PKOTCBZU-D74dQ4u0.js} +1 -1
  121. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-B9ia5ony.js → gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js} +1 -1
  122. package/dist/worker/console/static/assets/index-BWkIfcrK.css +1 -0
  123. package/dist/worker/console/static/assets/{index-wXlQohNs.js → index-Cdkvw_H6.js} +97 -97
  124. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-3ogpgegn.js → infoDiagram-6WML65LV-ILCbxJyb.js} +1 -1
  125. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-4tgpQ5SL.js → ishikawaDiagram-WSZJBQD7-DeuWBQ89.js} +1 -1
  126. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DNfgHqFx.js → journeyDiagram-NVQOT4AX-CF5ih8Fk.js} +1 -1
  127. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Dz7ngC8z.js → kanban-definition-27J2QSJJ-C7yOSuRO.js} +1 -1
  128. package/dist/worker/console/static/assets/{linear-DI7Jv62E.js → linear-BDZ9riWi.js} +1 -1
  129. package/dist/worker/console/static/assets/{mermaid.core-CQA4Nzmo.js → mermaid.core-7pKqYtpZ.js} +5 -5
  130. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CtsMc8Mf.js → mindmap-definition-FAOFIHXS-LeJDybSU.js} +1 -1
  131. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-ByppE9yH.js → pegDiagram-VL7TDLO6-BJvT3pMD.js} +1 -1
  132. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B5PtgFgt.js → pieDiagram-7S7Q4E2Y-_rGqpMan.js} +1 -1
  133. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cq4p0k2K.js → quadrantDiagram-CIZ2JOQS-DPcSv5aA.js} +1 -1
  134. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DjfkyljN.js → railroadDiagram-AXF67PYL-Drxx4hkJ.js} +1 -1
  135. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DzynNRSl.js → requirementDiagram-LRYGKXZP-BCTUdU4z.js} +1 -1
  136. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cj7TCpTJ.js → sankeyDiagram-W5VNT64P-B6wzbZmB.js} +1 -1
  137. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-C73C6ykA.js → sequenceDiagram-SI44F4Z6-BGt8d3QQ.js} +1 -1
  138. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BiNLgN2x.js → sizeCapture-X5ZJPWSS-7nlhJKo7.js} +1 -1
  139. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-UwvjS64-.js → stateDiagram-OKZ733FA-Cv2stqMA.js} +1 -1
  140. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +1 -0
  141. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CC7DH98r.js → swimlanes-SLNWSIFB-qDAo4Yc1.js} +2 -2
  142. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +8 -0
  143. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-LSBXTR70.js → timeline-definition-Z64GVDOM-CKbDNKTr.js} +1 -1
  144. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BAuxWwGP.js → vennDiagram-T6HMQDX7-DI-9EHic.js} +1 -1
  145. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y--E5_gjZx.js → wardleyDiagram-T6FBY63Y-BrzzRDpX.js} +1 -1
  146. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-dJWNmyzF.js → xychartDiagram-ELKLHX3M-BclH5hGh.js} +1 -1
  147. package/dist/worker/console/static/index.html +2 -2
  148. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +41 -11
  149. package/dist/worker/console/static-src/operator-chat/compaction-message.js +3 -17
  150. package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +15 -5
  151. package/dist/worker/console/static-src/operator-chat/timeline-merge.js +29 -0
  152. package/dist/worker/console/static-src/operator-chat/useChatThread.js +2 -5
  153. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +2 -0
  154. package/dist/worker/console/static-src/prd-file-import.js +3 -1
  155. package/dist/worker/console/workspace-context.js +17 -1
  156. package/dist/worker/delivery/git-transaction.js +2 -0
  157. package/dist/worker/feature/fullstack-validate.js +2 -0
  158. package/dist/worker/loop-agent/loop-agent-client.js +8 -2
  159. package/dist/worker/materialize/harness-task-materializer.js +4 -1
  160. package/dist/worker/metrics/projector.js +1 -1
  161. package/dist/worker/observability/event-store.js +4 -1
  162. package/dist/worker/observability/read-model.js +4 -1
  163. package/dist/worker/observe/node-transparency.js +23 -1
  164. package/dist/worker/observe/routes.js +4 -0
  165. package/dist/worker/observe/static/operator-chrome.js +3 -1
  166. package/dist/worker/outcomes/declared-artifacts.js +2 -0
  167. package/dist/worker/preflight.js +3 -0
  168. package/dist/worker/scheduler/scheduled-goal-dispatch.js +117 -0
  169. package/dist/worker/scheduler/scheduled-goal-evidence.js +167 -0
  170. package/dist/worker/scheduler/scheduled-goal-recovery.js +62 -0
  171. package/dist/worker/scheduler/scheduled-goal-store.js +699 -0
  172. package/dist/worker/scheduler/scheduled-goal-supervisor.js +132 -0
  173. package/dist/worker/scheduler/scheduled-goal-time.js +102 -0
  174. package/dist/worker/scheduler/scheduled-goal-types.js +95 -0
  175. package/dist/worker/task-spec/validate.js +4 -1
  176. package/dist/workflows/dag/backend-test-markdown-workflow.js +6 -24
  177. package/dist/workflows/dag/backend-test-plan-protocol.js +82 -5
  178. package/dist/workflows/dag/dag-retry-schema.js +11 -0
  179. package/dist/workflows/dag/decision-envelope.js +2 -0
  180. package/dist/workflows/dag/dynamic-runtime/shared.js +2 -2
  181. package/dist/workflows/dag/frontend-closeout.js +4 -2
  182. package/dist/workflows/dag/frontend-committed-facts.js +461 -0
  183. package/dist/workflows/dag/frontend-durable-tools.js +15 -3
  184. package/dist/workflows/dag/frontend-implementation-contract.js +365 -8
  185. package/dist/workflows/dag/frontend-plan-canary.js +53 -0
  186. package/dist/workflows/dag/frontend-plan-decision-contract.js +803 -0
  187. package/dist/workflows/dag/frontend-plan-render.js +0 -2
  188. package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
  189. package/dist/workflows/dag/frontend-provider-capability-matrix.js +8 -61
  190. package/dist/workflows/dag/frontend-recovery-run.js +57 -0
  191. package/dist/workflows/dag/frontend-review-context.js +43 -70
  192. package/dist/workflows/dag/frontend-risk.js +92 -10
  193. package/dist/workflows/dag/frontend-session-budget.js +117 -3
  194. package/dist/workflows/dag/frontend-shape.js +11 -55
  195. package/dist/workflows/dag/frontend-test-execution-evidence.js +35 -10
  196. package/dist/workflows/dag/frontend-typed-event-store.js +32 -25
  197. package/dist/workflows/dag/frontend-verification-trace.js +25 -12
  198. package/dist/workflows/dag/frontend-writer-admission.js +3 -47
  199. package/dist/workflows/dag/frontend-writer-status.js +0 -23
  200. package/dist/workflows/dag/init-hybrid.js +202 -111
  201. package/dist/workflows/dag/interrupt-request.js +2 -0
  202. package/dist/workflows/dag/lifecycle.js +11 -2
  203. package/dist/workflows/dag/node-execution.js +50 -13
  204. package/dist/workflows/dag/reconcile-run.js +2 -0
  205. package/dist/workflows/dag/repair-artifact.js +3 -1
  206. package/dist/workflows/dag/report.js +2 -0
  207. package/dist/workflows/dag/rerun-plan.js +10 -0
  208. package/dist/workflows/dag/rerun-task.js +79 -1
  209. package/dist/workflows/dag/retry-policy.js +18 -0
  210. package/dist/workflows/dag/scheduler.js +2 -4
  211. package/dist/workflows/dag/types.js +44 -6
  212. package/dist/workflows/dag/validate.js +4 -0
  213. package/docs/README.md +1 -0
  214. package/docs/governance/README.md +1 -0
  215. package/docs/init-surface.manifest.json +1 -0
  216. package/docs/operations/README.md +2 -0
  217. package/docs/templates/README.md +1 -1
  218. package/docs/templates/agent-dag.schema.json +2 -2
  219. package/docs/templates/backend-test-dag.json +14 -10
  220. package/docs/templates/frontend-implementation-contract.schema.json +0 -7
  221. package/docs/templates/frontend-implementation-dag.json +8 -9
  222. package/package.json +8 -3
  223. package/skills/frontend-bounded-implement/SKILL.md +3 -4
  224. package/skills/frontend-design-review/SKILL.md +7 -12
  225. package/skills/frontend-design-review/references/review-checklist.md +7 -7
  226. package/skills/frontend-plan/SKILL.md +8 -18
  227. package/skills/frontend-plan/references/decision-contract.md +19 -26
  228. package/skills/frontend-review/SKILL.md +21 -25
  229. package/skills/frontend-review/references/review-findings.md +48 -24
  230. package/dist/worker/console/static/assets/channel-BxdTlb_5.js +0 -1
  231. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-_ByA1Nf8.js +0 -1
  232. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-_ByA1Nf8.js +0 -1
  233. package/dist/worker/console/static/assets/index-B9goN3S5.css +0 -1
  234. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-nLgMZ0D3.js +0 -1
  235. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BcYqTady.js +0 -8
  236. package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -975
@@ -3,6 +3,8 @@
3
3
  * Definitions and execute() live here; ConsolePiRuntime owns caching and
4
4
  * composition with session-bound tools.
5
5
  */
6
+ import { zodToJsonSchema } from "zod-to-json-schema";
7
+ import { scheduledGoalBookingSchema } from "../../scheduled-goal-booking.js";
6
8
  import { authorizeChatSessionTool, DEFAULT_CONSOLE_CHAT_SESSION_MODE, } from "../../session-mode.js";
7
9
  import { buildModelCallableToolSchemas } from "../../tool-adapter.js";
8
10
  /**
@@ -17,7 +19,7 @@ export async function buildCustomOperatorTools(options) {
17
19
  import("typebox"),
18
20
  import("@earendil-works/pi-coding-agent"),
19
21
  ]);
20
- const schemas = buildModelCallableToolSchemas();
22
+ const schemas = buildModelCallableToolSchemas().filter((schema) => schema.toolId !== "prepareScheduledTask");
21
23
  const ctx = options.actionContext;
22
24
  return schemas.map((schema) => {
23
25
  // TypeBox schema is built dynamically; cast through unknown to the builder
@@ -29,22 +31,28 @@ export async function buildCustomOperatorTools(options) {
29
31
  ? { description: param.description }
30
32
  : {};
31
33
  let t;
32
- switch (param.type) {
33
- case "string":
34
- t = Type.String(desc);
35
- break;
36
- case "boolean":
37
- t = Type.Boolean(desc);
38
- break;
39
- case "number":
40
- t = Type.Number(desc);
41
- break;
42
- case "array":
43
- t = Type.Array(Type.String({}), Object.keys(desc).length ? desc : {});
44
- break;
45
- default:
46
- t = Type.Object({}, { additionalProperties: true });
34
+ if (schema.toolId === "prepareScheduledGoal" && param.name === "booking") {
35
+ // Provider adapters need every nested type (not an opaque object).
36
+ // Keep the strict service schema as the single input contract.
37
+ t = zodToJsonSchema(scheduledGoalBookingSchema, { $refStrategy: "none" });
47
38
  }
39
+ else
40
+ switch (param.type) {
41
+ case "string":
42
+ t = Type.String(desc);
43
+ break;
44
+ case "boolean":
45
+ t = Type.Boolean(desc);
46
+ break;
47
+ case "number":
48
+ t = Type.Number(desc);
49
+ break;
50
+ case "array":
51
+ t = Type.Array(Type.String({}), Object.keys(desc).length ? desc : {});
52
+ break;
53
+ default:
54
+ t = Type.Object({}, { additionalProperties: true });
55
+ }
48
56
  acc[param.name] = param.required ? t : Type.Optional(t);
49
57
  return acc;
50
58
  }, {}), { additionalProperties: false });
@@ -0,0 +1,22 @@
1
+ import { zodToJsonSchema } from "zod-to-json-schema";
2
+ import { scheduledTaskRequestSchema } from "../../scheduled-goal-request.js";
3
+ import { buildModelCallableToolSchemas } from "../../tool-adapter.js";
4
+ /** Host-bound identity is not a model parameter and cannot leak across sessions. */
5
+ export async function buildScheduledGoalTools(options) {
6
+ const { Type } = await import("typebox");
7
+ const { defineTool } = await import("@earendil-works/pi-coding-agent");
8
+ const schema = buildModelCallableToolSchemas().find((tool) => tool.action === "prepareScheduledTask");
9
+ if (!schema)
10
+ throw new Error("scheduled task capability missing");
11
+ return [defineTool({
12
+ name: schema.toolId, label: "准备定时任务", description: schema.description, promptSnippet: schema.description,
13
+ parameters: Type.Object({ request: zodToJsonSchema(scheduledTaskRequestSchema, { $refStrategy: "none" }) }, { additionalProperties: false }),
14
+ async execute(callId, params) {
15
+ const { dispatchChatToolCall } = await import("../../session-store.js");
16
+ const result = await dispatchChatToolCall({ ...options.actionContext, chatSessionRef: options.sessionId }, schema.toolId, params, callId, "workflow");
17
+ const value = result.ok ? result.result : { ok: false, error: result.message, code: result.errorCode,
18
+ nextAction: "Correct the business request and retry prepareScheduledTask. Do not edit PRD/runtime files, substitute sleep/immediate execution, or claim a booking exists." };
19
+ return { isError: !result.ok, content: [{ type: "text", text: JSON.stringify(value) }], details: value };
20
+ },
21
+ })];
22
+ }
@@ -0,0 +1,195 @@
1
+ /** Model exposure only. Capability registry and dispatch remain the authority. */
2
+ import { Type } from "typebox";
3
+ import { buildModelCallableToolSchemas } from "../tool-adapter.js";
4
+ import { computeChatActiveToolNames } from "../session-mode.js";
5
+ import { filterActiveInterviewTools } from "../../interview/tools.js";
6
+ export const TOOL_EXPOSURE_ENTRY = "console-tool-exposure";
7
+ export const TOOL_CATALOG_VERSION = 1;
8
+ export const CORE_WORKFLOW_TOOLS = Object.freeze(`prepareScheduledTask scheduledGoalGet scheduledGoalList newTask importPrd sourceInstructions contractDraftSave contractShow contractValidate contractDiff taskAdvance prepareDagConfirmation prepareDagExecution runDag operationGet operationList operationEventSummary operationWait observeLink dagReport dagDoctor dagRerunPlan dagRerun prepareMutationGate standaloneTaskRerun workerTaskRetry status planList operatorCapabilities`.split(" "));
9
+ const SCHEDULED_BOOKING_READ_TOOLS = new Set(["read", "find", "ls", "grep"]);
10
+ export function isScheduledBookingTurn(text) {
11
+ return /(?:预约|定时|稍后|分钟后|小时后|schedule(?:d)?|\blater\b)/i.test(text);
12
+ }
13
+ /** Keep booking turns on typed workflow actions and local read-only inspection. */
14
+ export function pinScheduledBookingTurnTools(session) {
15
+ const allowed = session.getActiveToolNames().filter(name => CORE_WORKFLOW_TOOLS.includes(name) || SCHEDULED_BOOKING_READ_TOOLS.has(name));
16
+ session.setActiveToolsByName(allowed);
17
+ return allowed;
18
+ }
19
+ export const WORKFLOW_TOOL_GROUPS = [
20
+ { id: "scheduling", examples: ["prepareScheduledGoal"], description: "高级预约:已有 DAG/Task 的冻结预约", tools: ["prepareScheduledGoal"] },
21
+ { id: "diagnostics", examples: ["inspect", "doctor", "dagValidate"], description: "DAG 深度诊断、校验、决策、上下文与初始化检查", tools: ["inspect", "doctor", "statsContext", "spineCheck", "dagRunTask", "dagValidate", "dagPromptOverrideConfig", "dagInterruptEligibility", "dagStatus", "dagDecisionInspect", "dagDecisionValidate"] },
22
+ { id: "governance", examples: ["planCreate", "docsAudit", "closeoutTask"], description: "计划管理、文档审计、交接覆盖与任务收口", tools: ["planCreate", "planComplete", "planCheck", "docsAudit", "handoffCheck", "handoffCoverage", "coverageReport", "stats", "promoteRun", "closeoutTask", "dagCloseoutDraft"] },
23
+ { id: "knowledge", examples: ["knowledgeQuery"], description: "查询项目知识", tools: ["knowledgeQuery"] },
24
+ { id: "worker", examples: ["workerSchedulerStatus", "workerPoolDoctor", "workerReportMorning"], description: "Worker 任务池、功能审查、调度诊断与报表", tools: ["workerPoolDoctor", "workerFeatureReview", "workerFeatureDoctor", "workerTaskValidate", "workerBatchPlanReady", "workerReportMorning", "workerReportMetrics", "workerAdmissionShow", "workerSchedulerList", "workerSchedulerStatus", "workerSchedulerLedger", "workerSchedulerDoctor"] },
25
+ ];
26
+ export function configuredToolExposure(value) {
27
+ if (value === undefined || value === "")
28
+ return undefined;
29
+ if (value === "full" || value === "progressive")
30
+ return value;
31
+ throw new Error("CONSOLE_CHAT_TOOL_EXPOSURE must be full or progressive");
32
+ }
33
+ /** Fail closed on catalog drift, including duplicate or human-only entries. */
34
+ export function validateWorkflowToolCatalog() {
35
+ const expected = new Set(buildModelCallableToolSchemas().map(t => t.toolId));
36
+ const names = [...CORE_WORKFLOW_TOOLS, ...WORKFLOW_TOOL_GROUPS.flatMap(g => [...g.tools])];
37
+ if (names.length !== new Set(names).size || names.length !== expected.size || names.some(n => !expected.has(n)))
38
+ throw new Error("WORKFLOW_TOOL_CATALOG_DRIFT");
39
+ }
40
+ export const TOOL_DISCOVERY_DESCRIPTION = "Load a workflow capability missing from the visible tools. Choose target by the user's intent from the capability directory, even when no tool name was mentioned. Pass a group ID or exact tool name; omit target if unsure. After loading, call the matching typed tool in the NEXT model response. Discovery alone does not fulfill the request, execute work or grant approval. Prefer discovery over recreating a workflow with shell or file edits.";
41
+ export class WorkflowToolExposure {
42
+ override;
43
+ state;
44
+ session;
45
+ interview = false;
46
+ manager;
47
+ // Failed persistence must not let the next turn silently undo full fallback.
48
+ // The active branch must still contain this ancestor; sibling branches do not inherit it.
49
+ volatileFallback;
50
+ constructor(manager, override) {
51
+ this.override = override;
52
+ this.manager = (manager ?? {});
53
+ this.state = { version: TOOL_CATALOG_VERSION, mode: override ?? "full", groups: [] };
54
+ try {
55
+ this.restore();
56
+ }
57
+ catch {
58
+ this.state = { version: TOOL_CATALOG_VERSION, mode: "full", groups: [], fallbackReason: "TOOL_EXPOSURE_STATE_READ_FAILED" };
59
+ }
60
+ }
61
+ get mode() { return this.state.mode; }
62
+ get snapshot() { return { ...this.state, groups: [...this.state.groups] }; }
63
+ restore() {
64
+ const entries = this.manager.getBranch?.() ?? [];
65
+ const latest = entries.filter(e => e.type === "custom" && e.customType === TOOL_EXPOSURE_ENTRY).at(-1);
66
+ if (!latest) {
67
+ this.state = { version: TOOL_CATALOG_VERSION, mode: this.override ?? "full", groups: [] };
68
+ return;
69
+ }
70
+ const raw = latest.data;
71
+ if (raw?.version !== TOOL_CATALOG_VERSION || !["full", "progressive"].includes(raw.mode ?? "") || !Array.isArray(raw.groups) || raw.groups.some(id => !WORKFLOW_TOOL_GROUPS.some(g => g.id === id))) {
72
+ this.state = { version: TOOL_CATALOG_VERSION, mode: "full", groups: [], fallbackReason: "INVALID_SAVED_TOOL_EXPOSURE" };
73
+ return;
74
+ }
75
+ const mode = raw.mode === "full" && raw.fallbackReason ? "full" : (this.override ?? raw.mode);
76
+ this.state = { version: TOOL_CATALOG_VERSION, mode, groups: [...new Set(raw.groups)], ...(mode === "full" && raw.fallbackReason ? { fallbackReason: raw.fallbackReason } : {}) };
77
+ }
78
+ persist() {
79
+ if (!this.manager.appendCustomEntry || !this.manager.getBranch)
80
+ throw new Error("TOOL_EXPOSURE_PERSISTENCE_UNAVAILABLE");
81
+ const previous = this.manager.getBranch().filter(e => e.type === "custom" && e.customType === TOOL_EXPOSURE_ENTRY).at(-1)?.data;
82
+ if (JSON.stringify(previous) !== JSON.stringify(this.state))
83
+ this.manager.appendCustomEntry(TOOL_EXPOSURE_ENTRY, this.snapshot);
84
+ }
85
+ exposed() {
86
+ if (this.state.mode === "full")
87
+ return undefined;
88
+ return [...CORE_WORKFLOW_TOOLS, ...WORKFLOW_TOOL_GROUPS.filter(g => this.state.groups.includes(g.id)).flatMap(g => [...g.tools])];
89
+ }
90
+ apply() {
91
+ if (!this.session)
92
+ throw new Error("TOOL_EXPOSURE_SESSION_UNAVAILABLE");
93
+ const names = computeChatActiveToolNames(this.session, "workflow", this.exposed());
94
+ const allowed = this.interview ? filterActiveInterviewTools(names).allowed : names;
95
+ this.session.setActiveToolsByName(allowed);
96
+ // SDK silently ignores unknown names: verify actual registration, not intent.
97
+ if (this.session.getAllTools) {
98
+ const registered = new Set(this.session.getAllTools().map(t => t.name));
99
+ const actual = new Set(this.session.getActiveToolNames());
100
+ const required = this.interview ? [] : (this.exposed() ?? CORE_WORKFLOW_TOOLS);
101
+ if (required.some(name => !registered.has(name) || !actual.has(name)))
102
+ throw new Error("TOOL_EXPOSURE_REQUIRED_TOOL_MISSING");
103
+ if (allowed.some(n => registered.has(n) && !actual.has(n)) || [...actual].some(n => !allowed.includes(n)))
104
+ throw new Error("TOOL_EXPOSURE_ACTIVATION_MISMATCH");
105
+ }
106
+ }
107
+ fallback(reason) {
108
+ this.state = { version: TOOL_CATALOG_VERSION, mode: "full", groups: [], fallbackReason: reason };
109
+ this.apply(); // A failure here is fatal; never pretend full mode is active.
110
+ try {
111
+ this.persist();
112
+ this.volatileFallback = undefined;
113
+ }
114
+ catch {
115
+ let anchor;
116
+ try {
117
+ anchor = this.manager.getBranch?.().at(-1);
118
+ }
119
+ catch { /* Conservatively keep full when branch cannot be read. */ }
120
+ this.volatileFallback = { anchor, state: this.snapshot };
121
+ }
122
+ }
123
+ pin(session, interview = false) {
124
+ this.session = session;
125
+ this.interview = interview;
126
+ // Reconstruct from the active branch, including after compaction/reopen.
127
+ try {
128
+ const pending = this.volatileFallback;
129
+ const branch = this.manager.getBranch?.() ?? [];
130
+ if (pending && (!pending.anchor || branch.some(entry => entry === pending.anchor ||
131
+ (pending.anchor?.id !== undefined && entry.id === pending.anchor.id)))) {
132
+ this.state = { ...pending.state, groups: [...pending.state.groups] };
133
+ }
134
+ else {
135
+ this.volatileFallback = undefined;
136
+ this.restore();
137
+ }
138
+ validateWorkflowToolCatalog();
139
+ this.apply();
140
+ // Full mode also records the session policy when supported.
141
+ if (this.mode === "progressive" || this.manager.appendCustomEntry) {
142
+ this.persist();
143
+ this.volatileFallback = undefined;
144
+ }
145
+ }
146
+ catch {
147
+ this.fallback(this.state.fallbackReason ?? "TOOL_EXPOSURE_RESTORE_OR_ACTIVATION_FAILED");
148
+ }
149
+ }
150
+ discover(target) {
151
+ if (this.interview)
152
+ return { ok: false, code: "TOOL_DISCOVERY_INTERVIEW_DENIED" };
153
+ const catalog = WORKFLOW_TOOL_GROUPS.map(g => ({ id: g.id, description: g.description, examples: [...g.examples], active: this.mode === "full" || this.state.groups.includes(g.id) }));
154
+ if (!target)
155
+ return { ok: true, ...this.snapshot, catalog };
156
+ if (CORE_WORKFLOW_TOOLS.includes(target)) {
157
+ if (!this.session?.getActiveToolNames().includes(target))
158
+ return { ok: false, code: "TOOL_EXPOSURE_REQUIRED_TOOL_MISSING" };
159
+ return { ok: true, alreadyActive: true, tools: [target] };
160
+ }
161
+ const group = WORKFLOW_TOOL_GROUPS.find(g => g.id === target || g.tools.includes(target));
162
+ if (!group)
163
+ return { ok: false, code: "TOOL_DISCOVERY_NOT_FOUND", catalog };
164
+ if (!this.session)
165
+ return { ok: false, code: "TOOL_EXPOSURE_SESSION_UNAVAILABLE" };
166
+ const registered = new Set(this.session.getAllTools?.().map(t => t.name) ?? []);
167
+ if (group.tools.some(n => !registered.has(n))) {
168
+ this.fallback("TOOL_EXPOSURE_GROUP_UNAVAILABLE");
169
+ return { ok: false, code: "TOOL_EXPOSURE_GROUP_UNAVAILABLE", ...this.snapshot };
170
+ }
171
+ try {
172
+ this.state = { ...this.state, groups: [...new Set([...this.state.groups, group.id])] };
173
+ this.apply();
174
+ this.persist();
175
+ }
176
+ catch {
177
+ this.fallback("TOOL_EXPOSURE_LOAD_FAILED");
178
+ }
179
+ return { ok: true, ...this.snapshot, group: group.id, tools: [...group.tools], effective: "next-model-response", guidance: "Use the activated typed tools in your next response. Loading has not executed any business action." };
180
+ }
181
+ buildTool(fullTool) {
182
+ return { name: "operatorCapabilities", label: "发现工作流工具", description: TOOL_DISCOVERY_DESCRIPTION,
183
+ // One short navigation line; detailed semantics live in schema description.
184
+ promptSnippet: "工作流能力目录:按用户意图选组;所需能力未显示时先调用 operatorCapabilities(target=组ID),再调用加载后的业务工具完成请求:" + WORKFLOW_TOOL_GROUPS.map(g => `${g.id}: ${g.description} (${g.examples.join(", ")})`).join("; "),
185
+ parameters: Type.Object({ target: Type.Optional(Type.String({ maxLength: 80, description: "Group ID or exact tool name; omit to list groups." })) }, { additionalProperties: false }),
186
+ execute: async (_id, params, signal) => {
187
+ if (signal?.aborted)
188
+ throw new Error("Tool discovery aborted");
189
+ if (this.mode === "full" && !this.interview && !params.target && fullTool)
190
+ return fullTool.execute(_id, {});
191
+ const result = this.discover(params.target);
192
+ return { isError: result.ok === false, content: [{ type: "text", text: JSON.stringify(result) }], details: result };
193
+ } };
194
+ }
195
+ }
@@ -37,6 +37,7 @@ import { authorizeChatSessionTool, DEFAULT_CONSOLE_CHAT_SESSION_MODE, chatActive
37
37
  import { buildAgentCustomTool, ExploreOrchestrator, } from "./subagents/index.js";
38
38
  import { createOperatorChatResourceLoader, } from "./resource-loader.js";
39
39
  import { applySkillAutoInvocationPreferences, loadResourcePreferences, } from "./resource-preferences-store.js";
40
+ import { buildPiModeLoopIsolationOptions } from "./pi-mode-loop-isolation.js";
40
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";
@@ -49,6 +50,8 @@ import { buildPersistentTerminalTools } from "./terminal-tools.js";
49
50
  import { buildScmTools } from "./scm-tools.js";
50
51
  import { buildGoalContinuationContext, } from "./goals.js";
51
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";
52
55
  import { buildCustomOperatorTools } from "./pi-runtime/custom-tools/operator-tools.js";
53
56
  import { buildBrowserTools } from "./pi-runtime/custom-tools/browser-tools.js";
54
57
  import { buildAskUserQuestionTool } from "./pi-runtime/custom-tools/ask-user-question.js";
@@ -80,13 +83,14 @@ import { createDefaultPiSdkBindings, safeGetAgentDir, } from "./pi-runtime/sdk-b
80
83
  */
81
84
  export const OPERATOR_CHAT_SYSTEM_PROMPT_BASE = [
82
85
  "You are General Operator Chat.",
83
- "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. Browser_* drives the visible Browser panel only (static http(s), no workspace edits/scripts).",
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).",
84
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.",
85
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.",
86
- "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.",
87
- "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.",
88
- "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.",
89
- "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.",
90
94
  ].join("\n");
91
95
  /**
92
96
  * Built-in SDK tool names registered for every Chat session (ADR 0011).
@@ -387,6 +391,7 @@ export class ConsolePiRuntime {
387
391
  /** Non-sensitive models.json metadata used to avoid reparsing unchanged config. */
388
392
  modelConfigFiles = new Map();
389
393
  sessionManagers = new Map();
394
+ toolExposures = new Map();
390
395
  /** Per-session SDK service scope (resourceLoader / settingsManager) used by the live runtime snapshot. */
391
396
  serviceScopes = new Map();
392
397
  /** Snapshot revision: bumped on create / reopen / model / thinking / reload. */
@@ -418,11 +423,17 @@ export class ConsolePiRuntime {
418
423
  exploreOrchestrator;
419
424
  bindings;
420
425
  browserAutomation;
426
+ ownsBrowserAutomation;
421
427
  constructor(options) {
422
428
  this.options = options;
423
429
  this.loader = createOperatorChatResourceLoader();
424
430
  this.bindings = options.bindings ?? createDefaultPiSdkBindings();
425
- this.browserAutomation = options.browserAutomation ?? new BrowserAutomationService();
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
+ });
426
437
  this.exploreOrchestrator = new ExploreOrchestrator({
427
438
  cwd: this.options.cwd,
428
439
  bindings: this.bindings,
@@ -483,7 +494,7 @@ export class ConsolePiRuntime {
483
494
  this.sessionModes.set(sessionId, sessionMode);
484
495
  return sessionMode;
485
496
  }
486
- async buildResourceLoaderOptions(base = {}, sessionId) {
497
+ async buildResourceLoaderOptions(base = {}, sessionId, sessionMode = DEFAULT_CONSOLE_CHAT_SESSION_MODE) {
487
498
  const inheritedFactories = Array.isArray(base.extensionFactories)
488
499
  ? base.extensionFactories
489
500
  : [];
@@ -514,6 +525,7 @@ export class ConsolePiRuntime {
514
525
  pressureExtension,
515
526
  wizardLocalHeadersExtension,
516
527
  ].filter((item) => Boolean(item));
528
+ const piMode = sessionMode === "pi";
517
529
  return {
518
530
  ...base,
519
531
  ...(extraFactories.length > 0
@@ -521,18 +533,23 @@ export class ConsolePiRuntime {
521
533
  extensionFactories: [...inheritedFactories, ...extraFactories],
522
534
  }
523
535
  : {}),
524
- skillsOverride: (current) => {
536
+ ...buildPiModeLoopIsolationOptions({
537
+ enabled: piMode,
538
+ cwd: this.options.cwd,
525
539
  // Synchronous override surface: use last-known prefs via a sync
526
540
  // fail-closed path. Prefer async cache filled by ensurePrefs.
527
- const prefs = this.cachedSkillPrefs ?? {
528
- schemaVersion: 1,
529
- skills: {},
530
- };
531
- return {
532
- skills: applySkillAutoInvocationPreferences(current.skills, prefs),
533
- diagnostics: current.diagnostics,
534
- };
535
- },
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
+ }),
536
553
  };
537
554
  }
538
555
  /** Last successfully loaded skill prefs (sync override + snapshot projection). */
@@ -852,6 +869,9 @@ export class ConsolePiRuntime {
852
869
  const aborted = () => Boolean(init.abortIfDisposed && this.disposedSessions.has(init.sessionId));
853
870
  if (aborted())
854
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
+ }
855
875
  const agentDir = await safeGetAgentDir(this.bindings);
856
876
  // Capture before services parse models.json. A concurrent edit can therefore
857
877
  // cause at most one harmless extra refresh, never a missed next-turn refresh.
@@ -865,7 +885,7 @@ export class ConsolePiRuntime {
865
885
  const { services } = await this.bindings.createServices({
866
886
  cwd: this.options.cwd,
867
887
  agentDir,
868
- resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt }, init.sessionId),
888
+ resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt }, init.sessionId, init.sessionMode),
869
889
  });
870
890
  if (aborted())
871
891
  return undefined;
@@ -907,8 +927,19 @@ export class ConsolePiRuntime {
907
927
  }
908
928
  return undefined;
909
929
  }
910
- await bindSessionExtensions(session);
911
- pinOperatorChatTools(session, init.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
+ }
912
943
  this.applySessionRecoveryOverrides(init.sessionId, services, session);
913
944
  return {
914
945
  session,
@@ -957,6 +988,7 @@ export class ConsolePiRuntime {
957
988
  }
958
989
  this.sessions.delete(sessionId);
959
990
  this.sessionManagers.delete(sessionId);
991
+ this.toolExposures.delete(sessionId);
960
992
  this.modelRuntimes.delete(sessionId);
961
993
  this.modelConfigFiles.delete(sessionId);
962
994
  this.sessionModels.delete(sessionId);
@@ -966,6 +998,20 @@ export class ConsolePiRuntime {
966
998
  this.thinkingSelections.delete(sessionId);
967
999
  }
968
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; }
969
1015
  /** Cached session-less custom tool definitions (operator + explore). */
970
1016
  async loadCustomTools(sessionId) {
971
1017
  const sessionMode = this.sessionModeOf(sessionId);
@@ -976,6 +1022,8 @@ export class ConsolePiRuntime {
976
1022
  : Promise.resolve([]),
977
1023
  this.loadExploreCustomTools(),
978
1024
  ]);
1025
+ const scheduledGoalTools = loadWorkflowTools && this.options.actionContext
1026
+ ? await buildScheduledGoalTools({ sessionId, actionContext: this.options.actionContext }) : [];
979
1027
  const browserTools = await buildBrowserTools({
980
1028
  automation: this.browserAutomation,
981
1029
  });
@@ -1029,8 +1077,14 @@ export class ConsolePiRuntime {
1029
1077
  process.stderr.write(`[console] chat Agent tool build failed: ${error instanceof Error ? error.message : String(error)}\n`);
1030
1078
  return undefined;
1031
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);
1032
1084
  return [
1033
- ...operatorCustomTools,
1085
+ ...operatorCustomTools.filter(tool => !discovery || tool.name !== "operatorCapabilities"),
1086
+ ...(discovery ? [discovery] : []),
1087
+ ...scheduledGoalTools,
1034
1088
  ...exploreCustomTools,
1035
1089
  ...(questionTool ? [questionTool] : []),
1036
1090
  ...(todoTool ? [todoTool] : []),
@@ -1120,6 +1174,7 @@ export class ConsolePiRuntime {
1120
1174
  const entry = input.summary
1121
1175
  ? manager.branchWithSummary?.(input.branchFromEntryId, input.summary)
1122
1176
  : (manager.branch(input.branchFromEntryId), undefined);
1177
+ this.pinSessionTools(input.sessionId, session);
1123
1178
  return {
1124
1179
  branchedFromEntryId: input.branchFromEntryId,
1125
1180
  ...(entry?.id ? { branchSummaryEntryId: entry.id } : {}),
@@ -1131,6 +1186,9 @@ export class ConsolePiRuntime {
1131
1186
  throw new Error(`chat session not active: ${sessionId}`);
1132
1187
  manager.branch(this.mainlineLeaves.get(sessionId));
1133
1188
  this.mainlineLeaves.delete(sessionId);
1189
+ const session = this.sessions.get(sessionId);
1190
+ if (session)
1191
+ this.pinSessionTools(sessionId, session);
1134
1192
  }
1135
1193
  getBranchContext(sessionId) {
1136
1194
  const manager = this.sessionManagers.get(sessionId);
@@ -1390,7 +1448,7 @@ export class ConsolePiRuntime {
1390
1448
  if (services)
1391
1449
  this.applySessionRecoveryOverrides(sessionId, services, session);
1392
1450
  await this.refreshModelConfiguration(sessionId, "force");
1393
- pinOperatorChatTools(session, this.sessionModeOf(sessionId));
1451
+ this.pinSessionTools(sessionId, session);
1394
1452
  }
1395
1453
  catch (error) {
1396
1454
  return {
@@ -1532,7 +1590,14 @@ export class ConsolePiRuntime {
1532
1590
  extensions: inventories.extensions,
1533
1591
  packages: inventories.packages,
1534
1592
  mcp: inventories.mcp,
1535
- diagnostics: inventories.diagnostics,
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
+ ],
1536
1601
  hasBash: true,
1537
1602
  });
1538
1603
  }
@@ -1849,11 +1914,10 @@ export class ConsolePiRuntime {
1849
1914
  // explicitly restores the normal Operator Chat set (baseline + extension
1850
1915
  // tools, ADR 0012).
1851
1916
  const sessionMode = this.sessionModeOf(sessionId);
1852
- const requestedTools = chatActiveToolBaseline(sessionMode);
1853
- const activeTools = options?.mode === "requirement-interview" && sessionMode === "workflow"
1854
- ? filterActiveInterviewTools(requestedTools).allowed
1855
- : computeChatActiveToolNames(session, sessionMode);
1856
- session.setActiveToolsByName(activeTools);
1917
+ this.pinSessionTools(sessionId, session, options?.mode === "requirement-interview" && sessionMode === "workflow");
1918
+ if (sessionMode === "workflow" && isScheduledBookingTurn(text)) {
1919
+ pinScheduledBookingTurnTools(session);
1920
+ }
1857
1921
  // Final pre-prompt fence: abort after tool pin must still skip session.prompt.
1858
1922
  if (options?.signal?.aborted) {
1859
1923
  return;
@@ -2186,6 +2250,7 @@ export class ConsolePiRuntime {
2186
2250
  }
2187
2251
  this.sessions.delete(sessionId);
2188
2252
  this.sessionManagers.delete(sessionId);
2253
+ this.toolExposures.delete(sessionId);
2189
2254
  this.mainlineLeaves.delete(sessionId);
2190
2255
  this.modelRuntimes.delete(sessionId);
2191
2256
  this.modelConfigFiles.delete(sessionId);
@@ -2202,6 +2267,8 @@ export class ConsolePiRuntime {
2202
2267
  for (const id of [...this.sessions.keys()])
2203
2268
  this.dispose(id);
2204
2269
  void this.exploreOrchestrator.abortAll();
2270
+ if (this.ownsBrowserAutomation)
2271
+ void this.browserAutomation.dispose();
2205
2272
  }
2206
2273
  }
2207
2274
  function mapAssistantMessageEvent(type, sessionId, message) {
@@ -134,6 +134,16 @@ export function projectStoredMessagesWithEventOrder(messages, events) {
134
134
  if (event.kind !== expectedKind)
135
135
  continue;
136
136
  const data = event.data;
137
+ // Exact alignment wins: since 2026-09-13 message_end frames carry the
138
+ // durable assistant message id, so restore matches by id, never by
139
+ // text (duplicate assistant texts would mis-align within a turn).
140
+ if (typeof data.messageId === "string" && data.messageId) {
141
+ if (data.messageId === message.id) {
142
+ assistantCursor = index + 1;
143
+ return event;
144
+ }
145
+ continue;
146
+ }
137
147
  const eventText = typeof data.text === "string"
138
148
  ? data.text
139
149
  : typeof data.message === "string"
@@ -271,6 +281,15 @@ function turnEventToStorePartial(event) {
271
281
  text: String(event.text ?? ""),
272
282
  ...(content ? { content } : {}),
273
283
  ...(timing ? { timing } : {}),
284
+ // Durable assistant message id (2026-09-13): the session
285
+ // store persists message_end BEFORE framing, so a reconnecting
286
+ // client and the restore projection can align on this id
287
+ // instead of matching text within a time window.
288
+ ...(typeof event.messageId ===
289
+ "string" &&
290
+ event.messageId
291
+ ? { messageId: event.messageId }
292
+ : {}),
274
293
  },
275
294
  };
276
295
  }
@@ -4279,7 +4298,7 @@ async function dispatchChatRequest(req, res, deps, pathname, workspaceScope) {
4279
4298
  return true;
4280
4299
  }
4281
4300
  }
4282
- // Browser probe API (read-only; explains iframe refusals to the panel).
4301
+ // Shared real-browser API for the Browser panel and chat browser tools.
4283
4302
  if (await handleBrowserRequest(req, res, {
4284
4303
  automation: deps.browserAutomation,
4285
4304
  requireWorkspace: () => workspaceScope