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

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 (187) hide show
  1. package/CHANGELOG.md +9 -140
  2. package/dist/application/task-lifecycle/advance.js +4 -17
  3. package/dist/application/task-lifecycle/observe.js +29 -43
  4. package/dist/application/task-lifecycle/plan-transitions.js +4 -5
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/commands/client-recovery.js +55 -20
  7. package/dist/commands/new-task.js +5 -0
  8. package/dist/commands/status.js +57 -0
  9. package/dist/commands/task-contract.js +269 -0
  10. package/dist/executors/dag-pi-executor.js +230 -269
  11. package/dist/executors/pi-executor.js +0 -2
  12. package/dist/executors/pi-sdk-executor.js +39 -77
  13. package/dist/executors/shell-executor.js +2 -3
  14. package/dist/executors/shell-write-guard.js +32 -27
  15. package/dist/shared/pi-context-pressure/extension.js +16 -26
  16. package/dist/shared/pi-retry-settings.js +0 -40
  17. package/dist/shared/prompts.js +26 -0
  18. package/dist/task/config.js +2 -0
  19. package/dist/task/contract/validate-draft.js +106 -0
  20. package/dist/task/goal.js +3 -0
  21. package/dist/task/index.js +12 -0
  22. package/dist/task/lifecycle.js +1 -0
  23. package/dist/task/operator/capabilities.js +6 -0
  24. package/dist/task/operator/envelope.js +2 -0
  25. package/dist/task/operator/index.js +5 -0
  26. package/dist/task/operator/registry.js +2 -0
  27. package/dist/task/operator/types.js +1 -0
  28. package/dist/task/paths.js +1 -0
  29. package/dist/task/source-prepare/source-fidelity-pi.js +2 -2
  30. package/dist/task/source-references.js +23 -48
  31. package/dist/task/source-state.js +1 -0
  32. package/dist/task/subagent-guidance.js +1 -0
  33. package/dist/worker/console/chat/assistant-content.js +2 -23
  34. package/dist/worker/console/chat/explore-tools.js +0 -13
  35. package/dist/worker/console/chat/pi-runtime.js +1182 -221
  36. package/dist/worker/console/chat/routes.js +71 -213
  37. package/dist/worker/console/chat/sdd-data-alignment.js +9 -65
  38. package/dist/worker/console/chat/session-catalog.js +0 -32
  39. package/dist/worker/console/chat/session-store.js +6 -22
  40. package/dist/worker/console/chat/shortcuts.js +0 -6
  41. package/dist/worker/console/chat/sift-bridge.js +1 -0
  42. package/dist/worker/console/chat/tool-preview.js +0 -11
  43. package/dist/worker/console/chat/tools.js +0 -6
  44. package/dist/worker/console/console-update-and-init.js +19 -1
  45. package/dist/worker/console/console-update-runtime.js +20 -5
  46. package/dist/worker/console/index.js +20 -0
  47. package/dist/worker/console/operator-actions.js +1 -13
  48. package/dist/worker/console/routes.js +0 -8
  49. package/dist/worker/console/server.js +29 -11
  50. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4NXyedy.js → abnfDiagram-N423BO3Z-hUmM_GRf.js} +1 -1
  51. package/dist/worker/console/static/assets/{arc-NZ3qXvBI.js → arc-Bt-xuieW.js} +1 -1
  52. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-COZSRvml.js → architectureDiagram-T3A2C74G-DTqkL4DJ.js} +1 -1
  53. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Ck0Gxi9C.js → blockDiagram-VBNYF7ZC-Cy0MNnzV.js} +1 -1
  54. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BX5HQ4Ox.js → c4Diagram-5PPSVZJV-DDdlT0Pd.js} +1 -1
  55. package/dist/worker/console/static/assets/channel-BpzRnZmU.js +1 -0
  56. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CoviemSL.js → chunk-2GRJ4B5K-3JawwNZ6.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DbOdOkbZ.js → chunk-2Q5K7J3B-DoTp7Cri.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BsG2pmdQ.js → chunk-5RXB4S5H-CcirhZaS.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DGdvrNm8.js → chunk-5VM5RSS4-VrG21gnP.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BAy7Yd6y.js → chunk-6Q2QTUOP-DDOgIS70.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D9UA7hUb.js → chunk-GF5L2VYU-CBjmgvJ_.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DPz7SZ8V.js → chunk-JWPE2WC7-CCQSht8M.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BmuEOIh7.js → chunk-KBJHAD2P-BpGMkUUV.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CIhBWnd8.js → chunk-RYQCIY6F-DNDT41QL.js} +1 -1
  65. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CScR6mDD.js → chunk-XXDRQBXY-euXnDFbn.js} +1 -1
  66. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlMVqCc.js +1 -0
  67. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlMVqCc.js +1 -0
  68. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BjpfWaUr.js → cose-bilkent-JH36ORCC-BJ6NAeql.js} +1 -1
  69. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BFQohJgT.js → cynefin-VYW2F7L2-F5JKIvIq.js} +1 -1
  70. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-D3gzRJnS.js → cynefinDiagram-MW4NZA55-Cwmkl_Gi.js} +1 -1
  71. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-5eojkhoA.js → dagre-VZM6K2ZE-BmCZ8815.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CpD8LWu_.js → diagram-7IWD3JNH-zWA3hUnX.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B8_4qJ2a.js → diagram-B4RE2ZJO-BzA-888F.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-HOsXf0eu.js → diagram-LBJQPF4R-tPNheoch.js} +1 -1
  75. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BqwRwiWP.js → diagram-Q27KOJAE-Cdi7Fyqp.js} +1 -1
  76. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BRVCNZe1.js → diagram-UB23O5K3-DFK9VBf1.js} +1 -1
  77. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Cvb1TgGh.js → ebnfDiagram-BXEA7PRR-xCUr1kpg.js} +1 -1
  78. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DS0O0qG9.js → erDiagram-JOGREHBK-BANQHY_A.js} +1 -1
  79. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BHIRvRr3.js → flowDiagram-UKHOOZJN-VHJYEMkP.js} +1 -1
  80. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DcCJ1JWx.js → ganttDiagram-PKOTCBZU-DiyZQ0C6.js} +1 -1
  81. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-B9ia5ony.js → gitGraphDiagram-DS77QQ5N-CZtIeBBa.js} +1 -1
  82. package/dist/worker/console/static/assets/index-BlN6ytLr.css +1 -0
  83. package/dist/worker/console/static/assets/index-CA-jfWlE.js +449 -0
  84. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-3ogpgegn.js → infoDiagram-6WML65LV-BuSspC0X.js} +1 -1
  85. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-4tgpQ5SL.js → ishikawaDiagram-WSZJBQD7-DRJZyPFy.js} +1 -1
  86. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DNfgHqFx.js → journeyDiagram-NVQOT4AX-DIfXIVuE.js} +1 -1
  87. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Dz7ngC8z.js → kanban-definition-27J2QSJJ-CT10_7Lt.js} +1 -1
  88. package/dist/worker/console/static/assets/{linear-DI7Jv62E.js → linear-ufemj74H.js} +1 -1
  89. package/dist/worker/console/static/assets/{mermaid.core-CQA4Nzmo.js → mermaid.core-DEQe8-_m.js} +5 -5
  90. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CtsMc8Mf.js → mindmap-definition-FAOFIHXS-BRLUrbrC.js} +1 -1
  91. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-ByppE9yH.js → pegDiagram-VL7TDLO6-BgibKtiF.js} +1 -1
  92. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B5PtgFgt.js → pieDiagram-7S7Q4E2Y-D3eyfdV9.js} +1 -1
  93. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cq4p0k2K.js → quadrantDiagram-CIZ2JOQS-DXeTk5Px.js} +1 -1
  94. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DjfkyljN.js → railroadDiagram-AXF67PYL-BgXdFEe7.js} +1 -1
  95. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DzynNRSl.js → requirementDiagram-LRYGKXZP-CseC2A7R.js} +1 -1
  96. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cj7TCpTJ.js → sankeyDiagram-W5VNT64P-D80PFzfJ.js} +1 -1
  97. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-C73C6ykA.js → sequenceDiagram-SI44F4Z6-BWzHW7LS.js} +1 -1
  98. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BiNLgN2x.js → sizeCapture-X5ZJPWSS-xWQYTLAn.js} +1 -1
  99. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-UwvjS64-.js → stateDiagram-OKZ733FA-CLxtpZeL.js} +1 -1
  100. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CiVoq4G-.js +1 -0
  101. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CC7DH98r.js → swimlanes-SLNWSIFB-DwugtmWV.js} +2 -2
  102. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BkdgdpfR.js +8 -0
  103. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-LSBXTR70.js → timeline-definition-Z64GVDOM-DHEOwHIe.js} +1 -1
  104. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BAuxWwGP.js → vennDiagram-T6HMQDX7-pmbZKlBK.js} +1 -1
  105. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y--E5_gjZx.js → wardleyDiagram-T6FBY63Y-CivHoM5q.js} +1 -1
  106. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-dJWNmyzF.js → xychartDiagram-ELKLHX3M-BfHIE7o6.js} +1 -1
  107. package/dist/worker/console/static/index.html +2 -2
  108. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +10 -122
  109. package/dist/worker/console/static-src/operator-chat/compaction-message.js +0 -7
  110. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +15 -35
  111. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +62 -136
  112. package/dist/worker/console/static-src/operator-chat/useChatStream.js +68 -159
  113. package/dist/worker/console/static-src/operator-chat/useChatThread.js +56 -75
  114. package/dist/worker/console/static-src/operator-chat/useComposer.js +78 -206
  115. package/dist/worker/console/workspace-context.js +1 -21
  116. package/dist/worker/observe/static/router.js +1 -1
  117. package/dist/worker/observe/static/styles.css +4 -7
  118. package/dist/worker/observe/static/views/dag-inspector.js +19 -12
  119. package/dist/worker/observe/static/views/dag.js +2 -2
  120. package/dist/worker/scheduler/index.js +25 -0
  121. package/dist/workflows/dag/facts.js +4 -0
  122. package/dist/workflows/dag/frontend-closeout.js +1 -1
  123. package/dist/workflows/dag/frontend-design-policy.js +1 -2
  124. package/dist/workflows/dag/frontend-implementation-contract.js +4 -8
  125. package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
  126. package/dist/workflows/dag/frontend-repair.js +76 -55
  127. package/dist/workflows/dag/frontend-review-context.js +1 -2
  128. package/dist/workflows/dag/frontend-review-scopes.js +12 -34
  129. package/dist/workflows/dag/frontend-shape-facts.js +2 -12
  130. package/dist/workflows/dag/frontend-verification-trace.js +42 -8
  131. package/dist/workflows/dag/index.js +6 -0
  132. package/dist/workflows/dag/init-hybrid.js +160 -97
  133. package/dist/workflows/dynamic/index.js +7 -0
  134. package/docs/architecture/runtime-boundaries.md +3 -3
  135. package/docs/templates/frontend-implementation-dag.json +8 -9
  136. package/package.json +3 -7
  137. package/skills/frontend-bounded-implement/SKILL.md +3 -4
  138. package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
  139. package/skills/frontend-contract/references/contract-protocol.md +1 -3
  140. package/skills/frontend-design-review/SKILL.md +7 -12
  141. package/skills/frontend-design-review/references/review-checklist.md +7 -7
  142. package/skills/frontend-plan/SKILL.md +0 -19
  143. package/skills/frontend-plan/references/decision-contract.md +19 -26
  144. package/skills/frontend-review/SKILL.md +18 -12
  145. package/skills/frontend-review/references/review-findings.md +43 -24
  146. package/dist/shared/pi-provider-recovery.js +0 -182
  147. package/dist/worker/console/chat/browser-automation.js +0 -363
  148. package/dist/worker/console/chat/browser-policy.js +0 -113
  149. package/dist/worker/console/chat/browser-routes.js +0 -162
  150. package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +0 -44
  151. package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +0 -89
  152. package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +0 -202
  153. package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +0 -79
  154. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +0 -86
  155. package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +0 -38
  156. package/dist/worker/console/chat/pi-runtime/inspection-types.js +0 -28
  157. package/dist/worker/console/chat/pi-runtime/inspection.js +0 -439
  158. package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +0 -153
  159. package/dist/worker/console/chat/pi-runtime/sdk-session.js +0 -5
  160. package/dist/worker/console/chat/scm-routes.js +0 -217
  161. package/dist/worker/console/chat/scm-service.js +0 -283
  162. package/dist/worker/console/chat/scm-tools.js +0 -111
  163. package/dist/worker/console/chat/session-mode-view.js +0 -49
  164. package/dist/worker/console/chat/session-mode.js +0 -220
  165. package/dist/worker/console/chat/terminal-routes.js +0 -216
  166. package/dist/worker/console/chat/terminal-sessions.js +0 -348
  167. package/dist/worker/console/chat/terminal-tools.js +0 -211
  168. package/dist/worker/console/static/assets/channel-BxdTlb_5.js +0 -1
  169. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-_ByA1Nf8.js +0 -1
  170. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-_ByA1Nf8.js +0 -1
  171. package/dist/worker/console/static/assets/index-B9goN3S5.css +0 -1
  172. package/dist/worker/console/static/assets/index-wXlQohNs.js +0 -469
  173. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-nLgMZ0D3.js +0 -1
  174. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BcYqTady.js +0 -8
  175. package/dist/worker/console/static-src/operator-chat/active-turn-tail.js +0 -10
  176. package/dist/worker/console/static-src/operator-chat/chat-link.js +0 -94
  177. package/dist/worker/console/static-src/operator-chat/chat-scroll-position.js +0 -24
  178. package/dist/worker/console/static-src/operator-chat/composer-session-ledger.js +0 -192
  179. package/dist/worker/console/static-src/operator-chat/process-label.js +0 -41
  180. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +0 -13
  181. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +0 -10
  182. package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +0 -51
  183. package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +0 -21
  184. package/dist/worker/console/workspace-initialization.js +0 -209
  185. package/dist/workflows/dag/frontend-capacity.js +0 -9
  186. package/dist/workflows/dag/frontend-plan-completeness.js +0 -186
  187. package/dist/workflows/dag/frontend-plan-recovery-policy.js +0 -18
@@ -26,38 +26,25 @@
26
26
  * `createDefaultPiSdkBindings()`.
27
27
  */
28
28
  import { createHash, randomBytes } from "node:crypto";
29
- import { stat } from "node:fs/promises";
29
+ import { access, mkdir, rm, stat, writeFile } 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
- import { OPERATOR_CHAT_ALLOWED_TOOLS, authorizeOperatorChatTool, assertNoWriteToolInList, } from "./tools.js";
36
- import { authorizeChatSessionTool, DEFAULT_CONSOLE_CHAT_SESSION_MODE, chatActiveToolBaseline, composeChatSystemPrompt, composeChatSystemPromptParts, computeChatActiveToolNames, shouldLoadWorkflowBusinessTools, } from "./session-mode.js";
35
+ import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_BUILTIN_CUSTOM_TOOLS, authorizeOperatorChatTool, assertNoWriteToolInList, } from "./tools.js";
37
36
  import { buildAgentCustomTool, ExploreOrchestrator, } from "./subagents/index.js";
38
37
  import { createOperatorChatResourceLoader, } from "./resource-loader.js";
39
- import { applySkillAutoInvocationPreferences, loadResourcePreferences, } from "./resource-preferences-store.js";
40
- import { applyProcessRetryOverride, } from "../../../shared/pi-retry-settings.js";
38
+ import { applySkillAutoInvocationPreferences, isAutoInvocationEnabled, loadResourcePreferences, skillResourceId, } from "./resource-preferences-store.js";
39
+ import { buildModelCallableToolSchemas } from "./tool-adapter.js";
41
40
  import { applyProcessCompactionOverride, createCompactionController, createContextPressureExtension, createSiftBridgeState, housekeepSiftStash, isConsoleSiftEnabled, parseCompactionPolicy, parseSiftBridgeConfig, projectPromptPressure, deriveEffectiveWindow, projectSafeTelemetry, emptyContextPressureCounters, } from "../../../shared/pi-context-pressure/index.js";
42
41
  import { incrementCounter as incrementPressureCounter } from "../../../shared/pi-context-pressure/telemetry.js";
43
42
  import { resolveDefaultChatModel, resolveLowChatModel, } from "./model-resolver.js";
44
43
  import { filterActiveInterviewTools } from "../interview/tools.js";
45
- import { redactRuntimeText } from "./runtime-context.js";
46
- import { BrowserAutomationService } from "./browser-automation.js";
44
+ import { RUNTIME_CONTEXT_TEXT_MAX, redactRuntimeText, } from "./runtime-context.js";
47
45
  import { buildTodoContinuationContext, } from "./todos.js";
48
- import { buildPersistentTerminalTools } from "./terminal-tools.js";
49
- import { buildScmTools } from "./scm-tools.js";
50
46
  import { buildGoalContinuationContext, } from "./goals.js";
51
47
  import { injectWizardLocalHeaders, } from "./sdd-data-alignment.js";
52
- import { buildCustomOperatorTools } from "./pi-runtime/custom-tools/operator-tools.js";
53
- import { buildBrowserTools } from "./pi-runtime/custom-tools/browser-tools.js";
54
- import { buildAskUserQuestionTool } from "./pi-runtime/custom-tools/ask-user-question.js";
55
- import { buildTodoWriteTool } from "./pi-runtime/custom-tools/todo-write.js";
56
- import { buildGoalTools } from "./pi-runtime/custom-tools/goal-tools.js";
57
- import { buildExploreCustomTools } from "./pi-runtime/custom-tools/explore-tools.js";
58
- import { THINKING_LEVELS, RuntimeSnapshotUnavailableError, } from "./pi-runtime/inspection-types.js";
59
- import { collectRuntimeInventories, collectSlashCommands, projectRuntimeSnapshot, } from "./pi-runtime/inspection.js";
60
- import { createDefaultPiSdkBindings, safeGetAgentDir, } from "./pi-runtime/sdk-bindings.js";
61
48
  /**
62
49
  * Base system prompt fragment every General Operator Chat session receives.
63
50
  *
@@ -80,7 +67,7 @@ import { createDefaultPiSdkBindings, safeGetAgentDir, } from "./pi-runtime/sdk-b
80
67
  */
81
68
  export const OPERATOR_CHAT_SYSTEM_PROMPT_BASE = [
82
69
  "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).",
70
+ "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.",
84
71
  "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.",
85
72
  "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
73
  "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.",
@@ -88,6 +75,15 @@ export const OPERATOR_CHAT_SYSTEM_PROMPT_BASE = [
88
75
  "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
76
  "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
77
  ].join("\n");
78
+ /** Compose the inspectable system prompt actually injected into Operator Chat. */
79
+ export function composeOperatorChatSystemPrompt(input) {
80
+ return [
81
+ OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
82
+ ...(input.systemPromptSuffix?.trim()
83
+ ? [input.systemPromptSuffix.trim()]
84
+ : []),
85
+ ].join("\n\n");
86
+ }
91
87
  /**
92
88
  * Built-in SDK tool names registered for every Chat session (ADR 0011).
93
89
  * Full Pi builtins: read/write/edit/bash/grep/find/ls. safe-* custom tools
@@ -116,8 +112,27 @@ export const OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS = Object.freeze([
116
112
  "git-diff",
117
113
  ]);
118
114
  export function OPERATOR_CHAT_ACTIVE_TOOL_NAMES() {
119
- return chatActiveToolBaseline("workflow");
115
+ return [
116
+ ...OPERATOR_CHAT_ALLOWED_TOOLS,
117
+ ...OPERATOR_CHAT_BUILTIN_EXPLORE_TOOLS,
118
+ ...OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS,
119
+ ...OPERATOR_CHAT_BUILTIN_CUSTOM_TOOLS,
120
+ ];
120
121
  }
122
+ /**
123
+ * Non-Pi write / coding channels that must never be activated in Operator
124
+ * Chat even when a user/project extension registers them (ADR 0011 / 0012).
125
+ * Mirrors the NON_PI_WRITE_CHANNELS guard in tools.ts for the Gate 2 pin.
126
+ */
127
+ const OPERATOR_CHAT_EXTENSION_DENIED_TOOL_IDS = new Set([
128
+ "apply_patch",
129
+ "apply-patch",
130
+ "full-tools",
131
+ "full_tools",
132
+ "coding-chat",
133
+ "coding_chat",
134
+ "shell",
135
+ ].map((t) => t.toLowerCase()));
121
136
  /**
122
137
  * Gate 2 active-tool set for a live session (ADR 0012): the operator-chat
123
138
  * baseline (operator actions + Pi builtins + safe explore) PLUS extension
@@ -128,8 +143,24 @@ export function OPERATOR_CHAT_ACTIVE_TOOL_NAMES() {
128
143
  * coding-chat). Unknown/denied names are never activated; Gate 3 still
129
144
  * re-authorizes every tool call per invocation.
130
145
  */
131
- export function computeOperatorChatActiveToolNames(session, sessionMode = DEFAULT_CONSOLE_CHAT_SESSION_MODE) {
132
- return computeChatActiveToolNames(session, sessionMode);
146
+ export function computeOperatorChatActiveToolNames(session) {
147
+ const baseline = OPERATOR_CHAT_ACTIVE_TOOL_NAMES();
148
+ if (!session?.getAllTools)
149
+ return baseline;
150
+ const baselineLower = new Set(baseline.map((t) => t.toLowerCase()));
151
+ const extras = [];
152
+ for (const tool of session.getAllTools()) {
153
+ const name = tool?.name;
154
+ if (!name || typeof name !== "string")
155
+ continue;
156
+ const lower = name.toLowerCase();
157
+ if (baselineLower.has(lower))
158
+ continue;
159
+ if (OPERATOR_CHAT_EXTENSION_DENIED_TOOL_IDS.has(lower))
160
+ continue;
161
+ extras.push(name);
162
+ }
163
+ return extras.length > 0 ? [...baseline, ...extras] : baseline;
133
164
  }
134
165
  const OPERATOR_CHAT_SESSION_EXCLUDE_TOOLS = Object.freeze([
135
166
  "apply_patch",
@@ -185,8 +216,8 @@ async function bindSessionExtensions(session) {
185
216
  return;
186
217
  await session.bindExtensions(createConsoleExtensionBindings());
187
218
  }
188
- function pinOperatorChatTools(session, sessionMode = DEFAULT_CONSOLE_CHAT_SESSION_MODE) {
189
- session.setActiveToolsByName(computeChatActiveToolNames(session, sessionMode));
219
+ function pinOperatorChatTools(session) {
220
+ session.setActiveToolsByName(computeOperatorChatActiveToolNames(session));
190
221
  }
191
222
  /**
192
223
  * Pi has no hook result that merely appends instructions to its built-in
@@ -288,8 +319,592 @@ export function createWizardLocalHeadersExtension(options) {
288
319
  },
289
320
  };
290
321
  }
291
- export { THINKING_LEVELS, RuntimeSnapshotUnavailableError, projectRuntimeSnapshot, };
292
- export { createDefaultPiSdkBindings } from "./pi-runtime/sdk-bindings.js";
322
+ /**
323
+ * Build Pi custom ToolDefinition objects for every whitelisted operator action.
324
+ * Each tool's execute() routes into the operator action dispatcher (Gate 3).
325
+ * Requires the `typebox` + `defineTool` packages (both bundled with the SDK).
326
+ */
327
+ async function buildCustomOperatorTools(options) {
328
+ // typebox is a transitive dep of @earendil-works/pi-coding-agent; Node's
329
+ // resolver hoists to the SDK's nested copy. defineTool is re-exported by SDK.
330
+ const [{ Type }, { defineTool }] = await Promise.all([
331
+ import("typebox"),
332
+ import("@earendil-works/pi-coding-agent"),
333
+ ]);
334
+ const schemas = buildModelCallableToolSchemas();
335
+ const ctx = options.actionContext;
336
+ return schemas.map((schema) => {
337
+ // TypeBox schema is built dynamically; cast through unknown to the builder
338
+ // since Static<TParams> is structural and cannot be statically inferred here.
339
+ const parameters = Type.Object(schema.inputParams.reduce((acc, param) => {
340
+ // Carry the registry's param description into the tool schema so
341
+ // the model sees parameter semantics, not just names/types.
342
+ const desc = param.description?.trim()
343
+ ? { description: param.description }
344
+ : {};
345
+ let t;
346
+ switch (param.type) {
347
+ case "string":
348
+ t = Type.String(desc);
349
+ break;
350
+ case "boolean":
351
+ t = Type.Boolean(desc);
352
+ break;
353
+ case "number":
354
+ t = Type.Number(desc);
355
+ break;
356
+ case "array":
357
+ t = Type.Array(Type.String({}), Object.keys(desc).length ? desc : {});
358
+ break;
359
+ default:
360
+ t = Type.Object({}, { additionalProperties: true });
361
+ }
362
+ acc[param.name] = param.required ? t : Type.Optional(t);
363
+ return acc;
364
+ }, {}), { additionalProperties: false });
365
+ return defineTool({
366
+ name: schema.toolId,
367
+ label: schema.toolId,
368
+ description: `${schema.description} (kind=${schema.kind}; cli: ${schema.cli})`,
369
+ promptSnippet: schema.description,
370
+ parameters: parameters,
371
+ async execute(toolCallId, params) {
372
+ const decision = authorizeOperatorChatTool(schema.toolId);
373
+ if (!decision.ok) {
374
+ return {
375
+ content: [
376
+ {
377
+ type: "text",
378
+ text: JSON.stringify({
379
+ error: decision.message,
380
+ code: decision.code,
381
+ }),
382
+ },
383
+ ],
384
+ details: { error: decision.message, code: decision.code },
385
+ };
386
+ }
387
+ // Defer require to avoid circular import at module load.
388
+ const { dispatchChatToolCall } = await import("./session-store.js");
389
+ const result = await dispatchChatToolCall(ctx, decision.toolId, params, toolCallId);
390
+ const payload = result.ok
391
+ ? result.result
392
+ : { error: result.message, code: result.errorCode };
393
+ return {
394
+ content: [{ type: "text", text: JSON.stringify(payload) }],
395
+ details: payload,
396
+ };
397
+ },
398
+ });
399
+ });
400
+ }
401
+ const ASK_USER_QUESTION_DESCRIPTION = "Ask the user concise questions when a confirmation, choice, or missing detail is required before proceeding. " +
402
+ "The tool pauses until the user answers. Use it instead of writing questions as ordinary assistant text. " +
403
+ "Send one or more questions with stable ids; recommended options should be first and end with (Recommended).";
404
+ async function buildAskUserQuestionTool(options) {
405
+ const [{ Type }, { defineTool }] = await Promise.all([
406
+ import("typebox"),
407
+ import("@earendil-works/pi-coding-agent"),
408
+ ]);
409
+ return defineTool({
410
+ name: "ask_user_question",
411
+ label: "Ask user",
412
+ description: ASK_USER_QUESTION_DESCRIPTION,
413
+ promptSnippet: ASK_USER_QUESTION_DESCRIPTION,
414
+ parameters: Type.Object({
415
+ questions: Type.Array(Type.Object({
416
+ id: Type.String({ description: "Stable question id echoed in the answer." }),
417
+ question: Type.String({ description: "Specific user-facing question." }),
418
+ header: Type.Optional(Type.String({ description: "Optional short heading." })),
419
+ detail: Type.Optional(Type.String({ description: "Optional Markdown context shown below the question." })),
420
+ options: Type.Optional(Type.Array(Type.Object({
421
+ label: Type.String({ description: "Short user-facing option label." }),
422
+ description: Type.Optional(Type.String({ description: "One sentence explaining impact or tradeoff." })),
423
+ }, { additionalProperties: false }))),
424
+ multi_select: Type.Optional(Type.Boolean({ description: "Allow more than one option." })),
425
+ }, { additionalProperties: false }), { minItems: 1, maxItems: 20 }),
426
+ }, { additionalProperties: false }),
427
+ async execute(toolCallId, params, signal) {
428
+ const answer = await options.registry.ask({
429
+ sessionId: options.sessionId,
430
+ toolCallId,
431
+ questions: params.questions,
432
+ signal,
433
+ });
434
+ return {
435
+ content: [{ type: "text", text: JSON.stringify(answer) }],
436
+ details: answer,
437
+ };
438
+ },
439
+ });
440
+ }
441
+ 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`.";
442
+ async function buildTodoWriteTool(options) {
443
+ const [{ Type }, { defineTool }] = await Promise.all([
444
+ import("typebox"),
445
+ import("@earendil-works/pi-coding-agent"),
446
+ ]);
447
+ return defineTool({
448
+ name: "todo_write",
449
+ label: "Update todo list",
450
+ description: TODO_WRITE_DESCRIPTION,
451
+ promptSnippet: TODO_WRITE_DESCRIPTION,
452
+ parameters: Type.Object({
453
+ todos: Type.Array(Type.Object({
454
+ content: Type.String({ description: "Short imperative task description." }),
455
+ status: Type.Union([
456
+ Type.Literal("pending"),
457
+ Type.Literal("in_progress"),
458
+ Type.Literal("completed"),
459
+ ]),
460
+ }, { additionalProperties: false })),
461
+ }, { additionalProperties: false }),
462
+ async execute(toolCallId, params) {
463
+ const result = options.registry.write(options.sessionId, toolCallId, params.todos);
464
+ return {
465
+ content: [{ type: "text", text: JSON.stringify(result) }],
466
+ details: result,
467
+ };
468
+ },
469
+ });
470
+ }
471
+ 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.";
472
+ 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.";
473
+ 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.";
474
+ async function buildGoalTools(options) {
475
+ const [{ Type }, { defineTool }] = await Promise.all([
476
+ import("typebox"),
477
+ import("@earendil-works/pi-coding-agent"),
478
+ ]);
479
+ const textResult = (value) => ({
480
+ content: [{ type: "text", text: JSON.stringify(value) }],
481
+ details: value,
482
+ });
483
+ return [
484
+ defineTool({
485
+ name: "get_goal",
486
+ label: "Get goal",
487
+ description: GET_GOAL_DESCRIPTION,
488
+ promptSnippet: GET_GOAL_DESCRIPTION,
489
+ parameters: Type.Object({}, { additionalProperties: false }),
490
+ async execute() {
491
+ return textResult({ goal: options.registry.get(options.sessionId) });
492
+ },
493
+ }),
494
+ defineTool({
495
+ name: "create_goal",
496
+ label: "Create goal",
497
+ description: CREATE_GOAL_DESCRIPTION,
498
+ promptSnippet: CREATE_GOAL_DESCRIPTION,
499
+ parameters: Type.Object({
500
+ objective: Type.String({
501
+ description: "Concrete final objective and completion standard explicitly requested by the user.",
502
+ }),
503
+ }, { additionalProperties: false }),
504
+ async execute(_toolCallId, params) {
505
+ if (!options.registry.consumeModelCreateGrant(options.sessionId)) {
506
+ return textResult({
507
+ ok: false,
508
+ code: "GOAL_EXPLICIT_REQUEST_REQUIRED",
509
+ message: "create_goal requires an explicit user request to create or start a Goal",
510
+ });
511
+ }
512
+ return textResult(options.registry.create({
513
+ sessionId: options.sessionId,
514
+ objective: params.objective,
515
+ }));
516
+ },
517
+ }),
518
+ defineTool({
519
+ name: "update_goal",
520
+ label: "Update goal",
521
+ description: UPDATE_GOAL_DESCRIPTION,
522
+ promptSnippet: UPDATE_GOAL_DESCRIPTION,
523
+ parameters: Type.Object({
524
+ goalId: Type.String(),
525
+ revision: Type.Number({ minimum: 1 }),
526
+ status: Type.Union([
527
+ Type.Literal("complete"),
528
+ Type.Literal("blocked"),
529
+ ]),
530
+ blocker: Type.Optional(Type.String()),
531
+ }, { additionalProperties: false }),
532
+ async execute(_toolCallId, params) {
533
+ return textResult(options.registry.update({
534
+ sessionId: options.sessionId,
535
+ goalId: params.goalId,
536
+ expectedRevision: params.revision,
537
+ phase: params.status,
538
+ blocker: params.blocker,
539
+ arm: false,
540
+ }));
541
+ },
542
+ }),
543
+ ];
544
+ }
545
+ /**
546
+ * Build the safe explore custom tools (roadmap M0-A / G16): safe-read,
547
+ * safe-grep, gitStatus, gitDiff. These replace the bare SDK bash/read/grep so
548
+ * the model can probe the repo WITHOUT write escapes (echo redirect, tee,
549
+ * sed -i, node -e) and WITHOUT reading sensitive files (env files, key/pem,
550
+ * auth.json, sessions dir) — and every returned byte is secret-scrubbed
551
+ * before reaching the model context.
552
+ *
553
+ * gitStatus/gitDiff use a FIXED argv (typed, no shell) so they cannot be
554
+ * turned into an arbitrary-shell vector.
555
+ */
556
+ async function buildExploreCustomTools(options) {
557
+ const [{ Type }, { defineTool }] = await Promise.all([
558
+ import("typebox"),
559
+ import("@earendil-works/pi-coding-agent"),
560
+ ]);
561
+ const { execFileSync } = await import("node:child_process");
562
+ const repoRoot = options.repoRoot;
563
+ const { safeReadFile, safeGrep, scrubSecrets } = await import("./explore-tools.js");
564
+ function safeGit(args) {
565
+ try {
566
+ const stdout = execFileSync("git", args, {
567
+ cwd: repoRoot,
568
+ encoding: "utf8",
569
+ // Fixed argv, no shell, bounded output.
570
+ maxBuffer: 512 * 1024,
571
+ timeout: 10_000,
572
+ windowsHide: true,
573
+ });
574
+ return { ok: true, stdout };
575
+ }
576
+ catch (e) {
577
+ return {
578
+ ok: false,
579
+ code: "GIT_FAILED",
580
+ message: e instanceof Error ? e.message : String(e),
581
+ };
582
+ }
583
+ }
584
+ const safeReadTool = defineTool({
585
+ name: "safe-read",
586
+ label: "safe-read",
587
+ 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.",
588
+ promptSnippet: "safe-read: read a repo file (safe; secrets scrubbed)",
589
+ parameters: Type.Object({
590
+ path: Type.String({ description: "repo-relative file path" }),
591
+ }, { additionalProperties: false }),
592
+ async execute(_toolCallId, params) {
593
+ const raw = String(params.path ?? "");
594
+ try {
595
+ const result = await safeReadFile(repoRoot, raw);
596
+ return {
597
+ content: [{ type: "text", text: JSON.stringify(result) }],
598
+ details: result,
599
+ };
600
+ }
601
+ catch (e) {
602
+ const err = e;
603
+ return {
604
+ content: [
605
+ {
606
+ type: "text",
607
+ text: JSON.stringify({
608
+ error: err.message ?? String(e),
609
+ code: err.code ?? "READ_FAILED",
610
+ }),
611
+ },
612
+ ],
613
+ details: {
614
+ error: err.message ?? String(e),
615
+ code: err.code ?? "READ_FAILED",
616
+ },
617
+ };
618
+ }
619
+ },
620
+ });
621
+ const safeGrepTool = defineTool({
622
+ name: "safe-grep",
623
+ label: "safe-grep",
624
+ 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.",
625
+ promptSnippet: "safe-grep: search repo files (safe; secrets scrubbed)",
626
+ parameters: Type.Object({
627
+ pattern: Type.String({ description: "regex source (no shell)" }),
628
+ caseInsensitive: Type.Optional(Type.Boolean()),
629
+ glob: Type.Optional(Type.String({ description: "optional glob filter e.g. *.ts" })),
630
+ }, { additionalProperties: false }),
631
+ async execute(_toolCallId, params) {
632
+ try {
633
+ const result = await safeGrep(repoRoot, String(params.pattern ?? ""), {
634
+ caseInsensitive: Boolean(params.caseInsensitive),
635
+ glob: params.glob ? String(params.glob) : undefined,
636
+ });
637
+ return {
638
+ content: [{ type: "text", text: JSON.stringify(result) }],
639
+ details: result,
640
+ };
641
+ }
642
+ catch (e) {
643
+ const err = e;
644
+ return {
645
+ content: [
646
+ {
647
+ type: "text",
648
+ text: JSON.stringify({
649
+ error: err.message ?? String(e),
650
+ code: err.code ?? "GREP_FAILED",
651
+ }),
652
+ },
653
+ ],
654
+ details: {
655
+ error: err.message ?? String(e),
656
+ code: err.code ?? "GREP_FAILED",
657
+ },
658
+ };
659
+ }
660
+ },
661
+ });
662
+ const gitStatusTool = defineTool({
663
+ name: "git-status",
664
+ label: "git-status",
665
+ description: "Typed `git status --porcelain` (fixed argv, no shell). Returns scrubbed working-tree status.",
666
+ promptSnippet: "git-status: working tree status (read-only)",
667
+ parameters: Type.Object({}, { additionalProperties: false }),
668
+ async execute() {
669
+ const r = safeGit(["status", "--porcelain"]);
670
+ if (!r.ok) {
671
+ return {
672
+ content: [
673
+ {
674
+ type: "text",
675
+ text: JSON.stringify({ error: r.message, code: r.code }),
676
+ },
677
+ ],
678
+ details: { error: r.message, code: r.code },
679
+ };
680
+ }
681
+ const { scrubbed, redactions } = scrubSecrets(r.stdout);
682
+ const payload = { status: scrubbed, redactions };
683
+ return {
684
+ content: [{ type: "text", text: JSON.stringify(payload) }],
685
+ details: payload,
686
+ };
687
+ },
688
+ });
689
+ const gitDiffTool = defineTool({
690
+ name: "git-diff",
691
+ label: "git-diff",
692
+ description: "Typed `git diff` (fixed argv: --stat or a single -- path; no shell). Returns scrubbed diff. path must be repo-relative and not sensitive.",
693
+ promptSnippet: "git-diff: diff (read-only, scrubbed)",
694
+ parameters: Type.Object({
695
+ path: Type.Optional(Type.String({
696
+ description: "optional repo-relative path to diff (default: whole tree --stat)",
697
+ })),
698
+ }, { additionalProperties: false }),
699
+ async execute(_toolCallId, params) {
700
+ let rawPath = params.path ? String(params.path) : undefined;
701
+ // If a path is supplied, validate it via the read-path guard so git diff
702
+ // cannot be aimed at sensitive files (.env* / auth.json / sessions).
703
+ if (rawPath) {
704
+ const { guardReadPath } = await import("./explore-tools.js");
705
+ const g = await guardReadPath(repoRoot, rawPath);
706
+ if (!g.ok) {
707
+ return {
708
+ content: [
709
+ {
710
+ type: "text",
711
+ text: JSON.stringify({ error: g.message, code: g.code }),
712
+ },
713
+ ],
714
+ details: { error: g.message, code: g.code },
715
+ };
716
+ }
717
+ rawPath = g.repoRelative;
718
+ }
719
+ const args = rawPath ? ["diff", "--", rawPath] : ["diff", "--stat"];
720
+ const r = safeGit(args);
721
+ if (!r.ok) {
722
+ return {
723
+ content: [
724
+ {
725
+ type: "text",
726
+ text: JSON.stringify({ error: r.message, code: r.code }),
727
+ },
728
+ ],
729
+ details: { error: r.message, code: r.code },
730
+ };
731
+ }
732
+ const { scrubbed, redactions } = scrubSecrets(r.stdout);
733
+ const payload = { diff: scrubbed, redactions };
734
+ return {
735
+ content: [{ type: "text", text: JSON.stringify(payload) }],
736
+ details: payload,
737
+ };
738
+ },
739
+ });
740
+ return [safeReadTool, safeGrepTool, gitStatusTool, gitDiffTool];
741
+ }
742
+ export const THINKING_LEVELS = [
743
+ "off",
744
+ "minimal",
745
+ "low",
746
+ "medium",
747
+ "high",
748
+ "xhigh",
749
+ "max",
750
+ ];
751
+ /**
752
+ * Structured critical-failure error for snapshot projection (RF-02). Model
753
+ * enumeration or system prompt projection failures throw this so consuming
754
+ * routes can return 503 PI_RUNTIME_UNAVAILABLE instead of a fake-empty 200.
755
+ * Partial ResourceLoader / package-inventory failures are NOT critical — they
756
+ * keep the 200 snapshot and surface as structured diagnostics instead.
757
+ */
758
+ export class RuntimeSnapshotUnavailableError extends Error {
759
+ code = "PI_RUNTIME_UNAVAILABLE";
760
+ constructor(message) {
761
+ super(message);
762
+ this.name = "RuntimeSnapshotUnavailableError";
763
+ }
764
+ }
765
+ /**
766
+ * Browser-safe projection of a live runtime snapshot (design §2.2 / RF-03).
767
+ *
768
+ * Every browser-visible string runs the shared secret-scrub chain
769
+ * (scrubSecrets + ghp_/Bearer redaction via redactRuntimeText) and a fixed
770
+ * per-field length limit: 200 (names/spec/source/origin), 400 (paths/errors/
771
+ * diagnostic messages), 600 (skill descriptions), 80 (package versions);
772
+ * truncation uses the fixed `…[truncated N chars …]` marker. The server-only
773
+ * `raw` prompt and the RAW-based `sha256`/`characters` pass through untouched,
774
+ * and real empty lists stay empty.
775
+ *
776
+ * Pure + deterministic and exported so the route harness reuses the exact
777
+ * production implementation (same code path as getRuntimeSnapshot).
778
+ */
779
+ export function projectRuntimeSnapshot(input) {
780
+ const field = (value, max) => value === undefined ? undefined : redactRuntimeText(value, max);
781
+ const projectSkill = (skill) => ({
782
+ name: field(skill.name, 200) ?? "",
783
+ description: field(skill.description, 600) ?? "",
784
+ path: field(skill.path, 400) ?? "",
785
+ ...(skill.baseDir ? { baseDir: field(skill.baseDir, 400) } : {}),
786
+ scope: skill.scope,
787
+ source: field(skill.source, 200) ?? "",
788
+ resourceId: field(skill.resourceId, 200) ?? "",
789
+ autoInvocationEnabled: skill.autoInvocationEnabled,
790
+ diagnostics: skill.diagnostics.map((diag) => field(diag, 400) ?? ""),
791
+ });
792
+ const projectExtension = (ext) => ({
793
+ name: field(ext.name, 200) ?? "",
794
+ path: field(ext.path, 400) ?? "",
795
+ ...(ext.resolvedPath
796
+ ? { resolvedPath: field(ext.resolvedPath, 400) }
797
+ : {}),
798
+ source: field(ext.source, 200) ?? "",
799
+ scope: ext.scope,
800
+ ...(ext.origin ? { origin: field(ext.origin, 200) } : {}),
801
+ enabled: ext.enabled,
802
+ ...(ext.error ? { error: field(ext.error, 400) } : {}),
803
+ });
804
+ const projectPackage = (pkg) => ({
805
+ spec: field(pkg.spec, 200) ?? "",
806
+ scope: pkg.scope,
807
+ enabled: pkg.enabled,
808
+ ...(pkg.version ? { version: field(pkg.version, 80) } : {}),
809
+ ...(pkg.configuredVersion
810
+ ? { configuredVersion: field(pkg.configuredVersion, 80) }
811
+ : {}),
812
+ ...(pkg.packageName ? { packageName: field(pkg.packageName, 200) } : {}),
813
+ ...(pkg.path ? { path: field(pkg.path, 400) } : {}),
814
+ ...(pkg.installedPath ? { installedPath: field(pkg.installedPath, 400) } : {}),
815
+ ...(pkg.cwd ? { cwd: field(pkg.cwd, 400) } : {}),
816
+ resolved: pkg.resolved.map((resource) => ({
817
+ type: resource.type,
818
+ name: field(resource.name, 200) ?? "",
819
+ path: field(resource.path, 400) ?? "",
820
+ source: field(resource.source, 200) ?? "",
821
+ scope: resource.scope,
822
+ })),
823
+ diagnostics: pkg.diagnostics.map((diag) => field(diag, 400) ?? ""),
824
+ });
825
+ const projectMcpTool = (tool) => ({
826
+ name: field(tool.name, 200) ?? "",
827
+ description: field(tool.description, 600) ?? "",
828
+ ...(tool.server ? { server: field(tool.server, 200) } : {}),
829
+ source: field(tool.source, 200) ?? "",
830
+ scope: tool.scope,
831
+ ...(tool.path ? { path: field(tool.path, 400) } : {}),
832
+ active: tool.active,
833
+ });
834
+ const projectMcp = (mcp) => ({
835
+ servers: mcp.servers.map((server) => ({
836
+ name: field(server.name, 200) ?? "",
837
+ scope: server.scope,
838
+ status: server.status,
839
+ enabled: server.enabled,
840
+ ...(server.transport ? { transport: field(server.transport, 80) } : {}),
841
+ ...(server.command ? { command: field(server.command, 400) } : {}),
842
+ ...(server.url ? { url: field(server.url, 400) } : {}),
843
+ ...(server.configPath ? { configPath: field(server.configPath, 400) } : {}),
844
+ tools: server.tools.map(projectMcpTool),
845
+ })),
846
+ unassignedTools: mcp.unassignedTools.map(projectMcpTool),
847
+ });
848
+ const projectDiagnostic = (diag) => ({
849
+ type: diag.type,
850
+ ...(diag.code ? { code: diag.code } : {}),
851
+ message: field(diag.message, 400) ?? "",
852
+ ...(diag.path ? { path: field(diag.path, 400) } : {}),
853
+ ...(diag.retryable !== undefined
854
+ ? { retryable: diag.retryable }
855
+ : {}),
856
+ });
857
+ return {
858
+ sessionId: input.sessionId,
859
+ revision: input.revision,
860
+ loadedAt: input.loadedAt,
861
+ selection: {
862
+ ...(input.selection.model
863
+ ? {
864
+ model: {
865
+ provider: input.selection.model.provider,
866
+ id: input.selection.model.id,
867
+ ...(input.selection.model.name
868
+ ? { name: field(input.selection.model.name, 200) }
869
+ : {}),
870
+ },
871
+ }
872
+ : {}),
873
+ thinkingSelection: input.selection.thinkingSelection,
874
+ ...(input.selection.effectiveThinkingLevel
875
+ ? { effectiveThinkingLevel: input.selection.effectiveThinkingLevel }
876
+ : {}),
877
+ },
878
+ models: input.models.map((model) => ({
879
+ provider: model.provider,
880
+ id: model.id,
881
+ ...(model.name ? { name: field(model.name, 200) } : {}),
882
+ ...(model.hasCredentials !== undefined
883
+ ? { hasCredentials: model.hasCredentials }
884
+ : {}),
885
+ })),
886
+ thinkingLevels: input.thinkingLevels.map((option) => ({ ...option })),
887
+ systemPrompt: {
888
+ ...(input.systemPrompt.raw !== undefined
889
+ ? { raw: input.systemPrompt.raw }
890
+ : {}),
891
+ text: redactRuntimeText(input.systemPrompt.text, RUNTIME_CONTEXT_TEXT_MAX),
892
+ sha256: input.systemPrompt.sha256,
893
+ characters: input.systemPrompt.characters,
894
+ },
895
+ activeTools: [...input.activeTools],
896
+ contextFiles: input.contextFiles.map((file) => ({
897
+ path: field(file.path, 400) ?? "",
898
+ characters: file.characters,
899
+ })),
900
+ skills: input.skills.map(projectSkill),
901
+ extensions: input.extensions.map(projectExtension),
902
+ packages: input.packages.map(projectPackage),
903
+ mcp: projectMcp(input.mcp ?? { servers: [], unassignedTools: [] }),
904
+ diagnostics: input.diagnostics.map(projectDiagnostic),
905
+ hasBash: input.hasBash,
906
+ };
907
+ }
293
908
  export { extractAssistantContentBlocks, extractAssistantText, isAssistantSdkMessage };
294
909
  function readSessionManagerId(manager) {
295
910
  if (!manager || typeof manager !== "object")
@@ -406,23 +1021,19 @@ export class ConsolePiRuntime {
406
1021
  sessionInit = new Map();
407
1022
  /** Resolved model binding after materialization (create may return before this is known). */
408
1023
  sessionModels = new Map();
409
- /** Durable session mode; record is the source of truth, this is the live handle cache. */
410
- sessionModes = new Map();
411
1024
  /** SessionIds disposed while materialization was still in flight. */
412
1025
  disposedSessions = new Set();
413
1026
  compactionControllers = new Map();
414
1027
  siftStates = new Map();
415
1028
  /** Session-less tool definition caches (no AgentSession state). */
416
- operatorCustomToolsByMode = new Map();
1029
+ operatorCustomToolsPromise;
417
1030
  exploreCustomToolsPromise;
418
1031
  exploreOrchestrator;
419
1032
  bindings;
420
- browserAutomation;
421
1033
  constructor(options) {
422
1034
  this.options = options;
423
1035
  this.loader = createOperatorChatResourceLoader();
424
1036
  this.bindings = options.bindings ?? createDefaultPiSdkBindings();
425
- this.browserAutomation = options.browserAutomation ?? new BrowserAutomationService();
426
1037
  this.exploreOrchestrator = new ExploreOrchestrator({
427
1038
  cwd: this.options.cwd,
428
1039
  bindings: this.bindings,
@@ -443,9 +1054,6 @@ export class ConsolePiRuntime {
443
1054
  },
444
1055
  });
445
1056
  }
446
- async syncAimaxAuthToken() {
447
- await this.options.sddDataAlignment?.auth?.getToken();
448
- }
449
1057
  /** Absolute path for repo-scoped resource preferences (may be undefined in unit tests). */
450
1058
  get resourcePreferencesPath() {
451
1059
  return this.options.resourcePreferencesPath;
@@ -474,15 +1082,6 @@ export class ConsolePiRuntime {
474
1082
  * after a PATCH sees the latest resource-preferences.json (not create-time
475
1083
  * closure state).
476
1084
  */
477
- sessionModeOf(sessionId) {
478
- if (!sessionId)
479
- return DEFAULT_CONSOLE_CHAT_SESSION_MODE;
480
- return this.sessionModes.get(sessionId) ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE;
481
- }
482
- rememberSessionMode(sessionId, sessionMode = DEFAULT_CONSOLE_CHAT_SESSION_MODE) {
483
- this.sessionModes.set(sessionId, sessionMode);
484
- return sessionMode;
485
- }
486
1085
  async buildResourceLoaderOptions(base = {}, sessionId) {
487
1086
  const inheritedFactories = Array.isArray(base.extensionFactories)
488
1087
  ? base.extensionFactories
@@ -582,11 +1181,9 @@ export class ConsolePiRuntime {
582
1181
  policy,
583
1182
  controller: this.getCompactionController(sessionId),
584
1183
  sift,
585
- normalizeTransientProviderErrors: true,
586
1184
  });
587
1185
  }
588
- applySessionRecoveryOverrides(sessionId, services, session) {
589
- applyProcessRetryOverride(services?.settingsManager);
1186
+ applySessionCompactionOverride(sessionId, services, session) {
590
1187
  const manager = services?.settingsManager;
591
1188
  const window = session?.getSessionStats?.()?.contextUsage?.contextWindow;
592
1189
  applyProcessCompactionOverride(manager, {
@@ -604,7 +1201,7 @@ export class ConsolePiRuntime {
604
1201
  const window = stats?.contextUsage?.contextWindow;
605
1202
  const tokens = stats?.contextUsage?.tokens;
606
1203
  if (typeof window === "number" && window > 0) {
607
- this.applySessionRecoveryOverrides(sessionId, this.serviceScopes.get(sessionId) ?? {}, session);
1204
+ this.applySessionCompactionOverride(sessionId, this.serviceScopes.get(sessionId) ?? {}, session);
608
1205
  }
609
1206
  const projection = projectPromptPressure({
610
1207
  currentContextTokens: tokens,
@@ -744,10 +1341,8 @@ export class ConsolePiRuntime {
744
1341
  * `initializing: true`; dependent routes wait on the same promise.
745
1342
  */
746
1343
  async createSession(init) {
747
- await this.syncAimaxAuthToken();
748
1344
  const sessionId = `operator-chat-${randomBytes(12).toString("hex")}`;
749
1345
  this.disposedSessions.delete(sessionId);
750
- const sessionMode = this.rememberSessionMode(sessionId, init?.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE);
751
1346
  const sessionManager = await this.bindings.createSessionManager({
752
1347
  cwd: this.options.cwd,
753
1348
  sessionDir: this.options.sessionDir,
@@ -773,7 +1368,6 @@ export class ConsolePiRuntime {
773
1368
  sessionManager,
774
1369
  model: init?.model,
775
1370
  systemPromptSuffix: init?.systemPromptSuffix,
776
- sessionMode,
777
1371
  // Create path must resolve default model from the SAME services object
778
1372
  // used for formal materialization (no throwaway createServices).
779
1373
  resolveDefaultModel: !init?.model,
@@ -783,8 +1377,7 @@ export class ConsolePiRuntime {
783
1377
  sessionFile,
784
1378
  createdAt: new Date().toISOString(),
785
1379
  model: init?.model,
786
- activeTools: chatActiveToolBaseline(sessionMode),
787
- sessionMode,
1380
+ activeTools: OPERATOR_CHAT_ACTIVE_TOOL_NAMES(),
788
1381
  initializing: true,
789
1382
  };
790
1383
  }
@@ -842,33 +1435,28 @@ export class ConsolePiRuntime {
842
1435
  this.sessionInit.set(sessionId, { status: "initializing", promise });
843
1436
  return promise;
844
1437
  }
845
- /**
846
- * Shared session preparation for create/reopen/fork: config capture, prompt
847
- * composition, resource loading, model resolution, tool assembly, SDK session
848
- * create, extension bind, tool pin, compaction override. Lifecycle maps stay
849
- * owned by the caller.
850
- */
851
- async prepareLiveSession(init) {
852
- const aborted = () => Boolean(init.abortIfDisposed && this.disposedSessions.has(init.sessionId));
853
- if (aborted())
854
- return undefined;
1438
+ async materializeSession(sessionId, init) {
1439
+ if (this.disposedSessions.has(sessionId))
1440
+ return;
855
1441
  const agentDir = await safeGetAgentDir(this.bindings);
856
1442
  // Capture before services parse models.json. A concurrent edit can therefore
857
1443
  // cause at most one harmless extra refresh, never a missed next-turn refresh.
858
1444
  const modelConfigFile = await captureModelConfigFile(agentDir);
859
- const appendSystemPrompt = composeChatSystemPromptParts({
860
- sessionMode: init.sessionMode,
861
- workflowPrompt: OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
862
- systemPromptSuffix: init.systemPromptSuffix,
863
- });
1445
+ const appendSystemPrompt = [
1446
+ OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
1447
+ ...(init.systemPromptSuffix ? [init.systemPromptSuffix] : []),
1448
+ ];
1449
+ // Single services construction for formal materialization (+ optional
1450
+ // default-model resolve from the same modelRuntime). Repo-scoped skill
1451
+ // auto-invocation prefs inject skillsOverride without touching SKILL.md.
864
1452
  await this.refreshSkillPrefsCache().catch(() => undefined);
865
1453
  const { services } = await this.bindings.createServices({
866
1454
  cwd: this.options.cwd,
867
1455
  agentDir,
868
- resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt }, init.sessionId),
1456
+ resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt }, sessionId),
869
1457
  });
870
- if (aborted())
871
- return undefined;
1458
+ if (this.disposedSessions.has(sessionId))
1459
+ return;
872
1460
  const modelRuntime = services.modelRuntime;
873
1461
  let model = init.model;
874
1462
  if (!model && init.resolveDefaultModel) {
@@ -882,9 +1470,9 @@ export class ConsolePiRuntime {
882
1470
  }) ?? undefined)
883
1471
  : undefined;
884
1472
  assertNoWriteToolInList(OPERATOR_CHAT_ALLOWED_TOOLS);
885
- const customTools = await this.loadCustomTools(init.sessionId);
886
- if (aborted())
887
- return undefined;
1473
+ const customTools = await this.loadCustomTools(sessionId);
1474
+ if (this.disposedSessions.has(sessionId))
1475
+ return;
888
1476
  // pi-web parity: do NOT pass `tools`. A closed allowlist becomes
889
1477
  // SDK allowedToolNames and filters extension MCP tools out of the
890
1478
  // registry before Gate 2 can re-add them. Operator/safe customTools
@@ -898,59 +1486,35 @@ export class ConsolePiRuntime {
898
1486
  ...(customTools.length > 0 ? { customTools } : {}),
899
1487
  ...(resolvedModel ? { model: resolvedModel } : {}),
900
1488
  });
901
- if (aborted()) {
1489
+ if (this.disposedSessions.has(sessionId)) {
902
1490
  try {
903
1491
  session.dispose();
904
1492
  }
905
1493
  catch {
906
1494
  // ignore late dispose cleanup
907
1495
  }
908
- return undefined;
1496
+ return;
909
1497
  }
910
1498
  await bindSessionExtensions(session);
911
- pinOperatorChatTools(session, init.sessionMode);
912
- this.applySessionRecoveryOverrides(init.sessionId, services, session);
913
- return {
914
- session,
915
- modelRuntime,
916
- modelConfigFile,
917
- model,
918
- services: {
919
- resourceLoader: services.resourceLoader,
920
- settingsManager: services.settingsManager,
921
- },
922
- };
923
- }
924
- async materializeSession(sessionId, init) {
925
- if (this.disposedSessions.has(sessionId))
926
- return;
927
- await this.syncAimaxAuthToken();
928
- const sessionMode = this.rememberSessionMode(sessionId, init.sessionMode ?? this.sessionModeOf(sessionId));
929
- const prepared = await this.prepareLiveSession({
930
- sessionId,
931
- sessionManager: init.sessionManager,
932
- model: init.model,
933
- systemPromptSuffix: init.systemPromptSuffix,
934
- sessionMode,
935
- resolveDefaultModel: init.resolveDefaultModel,
936
- abortIfDisposed: true,
937
- });
938
- if (!prepared)
939
- return;
1499
+ pinOperatorChatTools(session);
940
1500
  // Register then re-check dispose race: a concurrent dispose must win.
941
- this.sessions.set(sessionId, prepared.session);
1501
+ this.sessions.set(sessionId, session);
942
1502
  this.sessionManagers.set(sessionId, init.sessionManager);
943
- this.modelRuntimes.set(sessionId, prepared.modelRuntime);
944
- this.modelConfigFiles.set(sessionId, prepared.modelConfigFile);
945
- this.sessionModels.set(sessionId, prepared.model);
946
- this.serviceScopes.set(sessionId, prepared.services);
1503
+ this.modelRuntimes.set(sessionId, modelRuntime);
1504
+ this.modelConfigFiles.set(sessionId, modelConfigFile);
1505
+ this.sessionModels.set(sessionId, model);
1506
+ this.serviceScopes.set(sessionId, {
1507
+ resourceLoader: services.resourceLoader,
1508
+ settingsManager: services.settingsManager,
1509
+ });
1510
+ this.applySessionCompactionOverride(sessionId, services, session);
947
1511
  this.revisions.set(sessionId, 1);
948
1512
  if (!this.thinkingSelections.has(sessionId)) {
949
1513
  this.thinkingSelections.set(sessionId, "auto");
950
1514
  }
951
1515
  if (this.disposedSessions.has(sessionId)) {
952
1516
  try {
953
- prepared.session.dispose();
1517
+ session.dispose();
954
1518
  }
955
1519
  catch {
956
1520
  // ignore
@@ -960,7 +1524,6 @@ export class ConsolePiRuntime {
960
1524
  this.modelRuntimes.delete(sessionId);
961
1525
  this.modelConfigFiles.delete(sessionId);
962
1526
  this.sessionModels.delete(sessionId);
963
- this.sessionModes.delete(sessionId);
964
1527
  this.serviceScopes.delete(sessionId);
965
1528
  this.revisions.delete(sessionId);
966
1529
  this.thinkingSelections.delete(sessionId);
@@ -968,17 +1531,10 @@ export class ConsolePiRuntime {
968
1531
  }
969
1532
  /** Cached session-less custom tool definitions (operator + explore). */
970
1533
  async loadCustomTools(sessionId) {
971
- const sessionMode = this.sessionModeOf(sessionId);
972
- const loadWorkflowTools = shouldLoadWorkflowBusinessTools(sessionMode);
973
1534
  const [operatorCustomTools, exploreCustomTools] = await Promise.all([
974
- loadWorkflowTools
975
- ? this.loadOperatorCustomTools(sessionMode)
976
- : Promise.resolve([]),
1535
+ this.loadOperatorCustomTools(),
977
1536
  this.loadExploreCustomTools(),
978
1537
  ]);
979
- const browserTools = await buildBrowserTools({
980
- automation: this.browserAutomation,
981
- });
982
1538
  const questionTool = this.options.questionRegistry
983
1539
  ? await buildAskUserQuestionTool({
984
1540
  sessionId,
@@ -986,31 +1542,11 @@ export class ConsolePiRuntime {
986
1542
  })
987
1543
  : undefined;
988
1544
  const todoTool = this.options.todoRegistry
989
- ? await buildTodoWriteTool({
990
- sessionId,
991
- registry: this.options.todoRegistry,
992
- sessionMode,
993
- })
1545
+ ? await buildTodoWriteTool({ sessionId, registry: this.options.todoRegistry })
994
1546
  : undefined;
995
1547
  const goalTools = this.options.goalRegistry
996
1548
  ? await buildGoalTools({ sessionId, registry: this.options.goalRegistry })
997
1549
  : [];
998
- const terminalTools = this.options.terminalRegistry
999
- ? await buildPersistentTerminalTools({
1000
- ownerSessionId: sessionId,
1001
- registry: this.options.terminalRegistry,
1002
- defaultCwd: this.options.cwd,
1003
- }).catch((error) => {
1004
- process.stderr.write(`[console] chat terminal-tools build failed: ${error instanceof Error ? error.message : String(error)}\n`);
1005
- return [];
1006
- })
1007
- : [];
1008
- const scmTools = this.options.scmService
1009
- ? await buildScmTools({ scm: this.options.scmService }).catch((error) => {
1010
- process.stderr.write(`[console] chat scm-tools build failed: ${error instanceof Error ? error.message : String(error)}\n`);
1011
- return [];
1012
- })
1013
- : [];
1014
1550
  const agentTool = await buildAgentCustomTool({
1015
1551
  parentSessionId: sessionId,
1016
1552
  orchestrator: this.exploreOrchestrator,
@@ -1035,29 +1571,23 @@ export class ConsolePiRuntime {
1035
1571
  ...(questionTool ? [questionTool] : []),
1036
1572
  ...(todoTool ? [todoTool] : []),
1037
1573
  ...goalTools,
1038
- ...terminalTools,
1039
- ...scmTools,
1040
- ...browserTools,
1041
1574
  ...(agentTool ? [agentTool] : []),
1042
1575
  ];
1043
1576
  }
1044
- loadOperatorCustomTools(sessionMode) {
1577
+ loadOperatorCustomTools() {
1045
1578
  if (!this.options.actionContext)
1046
1579
  return Promise.resolve([]);
1047
- const cached = this.operatorCustomToolsByMode.get(sessionMode);
1048
- if (cached)
1049
- return cached;
1050
- const promise = buildCustomOperatorTools({
1051
- actionContext: this.options.actionContext,
1052
- sessionMode,
1053
- }).catch((error) => {
1054
- process.stderr.write(`[console] chat custom-tools build failed: ${error instanceof Error ? error.message : String(error)}\n`);
1055
- // Allow a later create/reopen to retry after a transient failure.
1056
- this.operatorCustomToolsByMode.delete(sessionMode);
1057
- return [];
1058
- });
1059
- this.operatorCustomToolsByMode.set(sessionMode, promise);
1060
- return promise;
1580
+ if (!this.operatorCustomToolsPromise) {
1581
+ this.operatorCustomToolsPromise = buildCustomOperatorTools({
1582
+ actionContext: this.options.actionContext,
1583
+ }).catch((error) => {
1584
+ process.stderr.write(`[console] chat custom-tools build failed: ${error instanceof Error ? error.message : String(error)}\n`);
1585
+ // Allow a later create/reopen to retry after a transient failure.
1586
+ this.operatorCustomToolsPromise = undefined;
1587
+ return [];
1588
+ });
1589
+ }
1590
+ return this.operatorCustomToolsPromise;
1061
1591
  }
1062
1592
  loadExploreCustomTools() {
1063
1593
  if (!this.exploreCustomToolsPromise) {
@@ -1075,7 +1605,6 @@ export class ConsolePiRuntime {
1075
1605
  if (!this.bindings.forkSessionManager)
1076
1606
  throw new Error("SESSION_FORK_UNSUPPORTED");
1077
1607
  const sessionId = `operator-chat-${randomBytes(12).toString("hex")}`;
1078
- const sessionMode = this.rememberSessionMode(sessionId, init.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE);
1079
1608
  const sessionManager = await this.bindings.forkSessionManager({
1080
1609
  sourcePath: init.sourceSessionFile,
1081
1610
  targetCwd: this.options.cwd,
@@ -1086,8 +1615,8 @@ export class ConsolePiRuntime {
1086
1615
  sessionId,
1087
1616
  sessionManager,
1088
1617
  model: init.model,
1089
- sessionMode,
1090
1618
  });
1619
+ built.session.setActiveToolsByName(computeOperatorChatActiveToolNames(built.session));
1091
1620
  this.sessions.set(sessionId, built.session);
1092
1621
  this.sessionManagers.set(sessionId, sessionManager);
1093
1622
  this.modelRuntimes.set(sessionId, built.modelRuntime);
@@ -1103,7 +1632,6 @@ export class ConsolePiRuntime {
1103
1632
  createdAt: new Date().toISOString(),
1104
1633
  model: init.model,
1105
1634
  activeTools: built.session.getActiveToolNames(),
1106
- sessionMode,
1107
1635
  };
1108
1636
  }
1109
1637
  async branchSession(input) {
@@ -1282,22 +1810,15 @@ export class ConsolePiRuntime {
1282
1810
  async getRuntimeSystemPrompt(sessionId) {
1283
1811
  await this.ensureSessionReady(sessionId).catch(() => undefined);
1284
1812
  const session = this.sessions.get(sessionId);
1285
- if (!session) {
1286
- return composeChatSystemPrompt({
1287
- sessionMode: this.sessionModeOf(sessionId),
1288
- workflowPrompt: OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
1289
- });
1290
- }
1813
+ if (!session)
1814
+ return composeOperatorChatSystemPrompt({});
1291
1815
  const fromState = session.state?.systemPrompt;
1292
1816
  const fromGetter = session.systemPrompt;
1293
1817
  const loader = this.serviceScopes.get(sessionId)?.resourceLoader;
1294
1818
  return (fromState ||
1295
1819
  fromGetter ||
1296
1820
  loader?.getSystemPrompt?.() ||
1297
- composeChatSystemPrompt({
1298
- sessionMode: this.sessionModeOf(sessionId),
1299
- workflowPrompt: OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
1300
- }));
1821
+ composeOperatorChatSystemPrompt({}));
1301
1822
  }
1302
1823
  /**
1303
1824
  * Read the RAW system prompt for the snapshot, converting any projection
@@ -1388,9 +1909,8 @@ export class ConsolePiRuntime {
1388
1909
  await session.reload();
1389
1910
  const services = this.serviceScopes.get(sessionId);
1390
1911
  if (services)
1391
- this.applySessionRecoveryOverrides(sessionId, services, session);
1912
+ this.applySessionCompactionOverride(sessionId, services, session);
1392
1913
  await this.refreshModelConfiguration(sessionId, "force");
1393
- pinOperatorChatTools(session, this.sessionModeOf(sessionId));
1394
1914
  }
1395
1915
  catch (error) {
1396
1916
  return {
@@ -1421,14 +1941,91 @@ export class ConsolePiRuntime {
1421
1941
  * Never returns prompt/skill file bodies.
1422
1942
  */
1423
1943
  async listSlashCommands(sessionId) {
1944
+ const scrub = (value) => {
1945
+ const raw = String(value ?? "");
1946
+ return raw
1947
+ .replace(/\/Users\/[^\s]+/gi, "~")
1948
+ .replace(/[A-Za-z]:\\Users\\[^\s]+/gi, "~")
1949
+ .slice(0, 240);
1950
+ };
1424
1951
  // Fail soft at the HTTP layer: bubble readiness errors so routes can attach
1425
1952
  // a compact warning while the UI keeps local Operator/Pi Web commands.
1426
1953
  await this.ensureSessionReady(sessionId);
1427
1954
  const session = this.sessions.get(sessionId);
1428
1955
  if (!session)
1429
1956
  return { commands: [] };
1430
- const resourceLoader = this.serviceScopes.get(sessionId)?.resourceLoader;
1431
- return collectSlashCommands({ session, resourceLoader });
1957
+ // Same-name conflict priority (AC-3): extension > skill > prompt.
1958
+ // Enum order must not decide the winner — prompt-before-skill still loses.
1959
+ const SOURCE_RANK = {
1960
+ extension: 0,
1961
+ skill: 1,
1962
+ prompt: 2,
1963
+ };
1964
+ const byKey = new Map();
1965
+ const sourceWarnings = [];
1966
+ const push = (command, label, description, source) => {
1967
+ const name = command.startsWith("/") ? command : `/${command}`;
1968
+ const key = name.toLowerCase();
1969
+ if (!key || key === "/")
1970
+ return;
1971
+ const next = {
1972
+ command: name,
1973
+ label: (label || name).slice(0, 80),
1974
+ description: scrub(description),
1975
+ source,
1976
+ };
1977
+ const existing = byKey.get(key);
1978
+ if (!existing || SOURCE_RANK[source] < SOURCE_RANK[existing.source]) {
1979
+ byKey.set(key, next);
1980
+ }
1981
+ };
1982
+ try {
1983
+ const cmds = session.extensionRunner?.getRegisteredCommands?.() ?? [];
1984
+ for (const cmd of cmds) {
1985
+ const name = cmd.invocationName || cmd.name;
1986
+ if (!name)
1987
+ continue;
1988
+ push(String(name), String(cmd.name || name), String(cmd.description || ""), "extension");
1989
+ }
1990
+ }
1991
+ catch (error) {
1992
+ // Source isolation: keep other sources; surface compact warning (AC-3).
1993
+ sourceWarnings.push(`extension: ${scrub(error instanceof Error ? error.message : String(error))}`);
1994
+ }
1995
+ try {
1996
+ // Enumerate prompts before skills on purpose: priority must still let skill win.
1997
+ for (const tpl of session.promptTemplates ?? []) {
1998
+ if (!tpl?.name)
1999
+ continue;
2000
+ push(String(tpl.name), String(tpl.name), String(tpl.description || ""), "prompt");
2001
+ }
2002
+ }
2003
+ catch (error) {
2004
+ sourceWarnings.push(`prompt: ${scrub(error instanceof Error ? error.message : String(error))}`);
2005
+ }
2006
+ try {
2007
+ const services = this.serviceScopes.get(sessionId);
2008
+ const resourceLoader = services?.resourceLoader;
2009
+ const skills = resourceLoader?.getSkills?.()?.skills ?? [];
2010
+ for (const skill of skills) {
2011
+ if (!skill?.name)
2012
+ continue;
2013
+ const command = skill.name.startsWith("skill:")
2014
+ ? `/${skill.name}`
2015
+ : `/skill:${skill.name}`;
2016
+ push(command, skill.name, String(skill.description || ""), "skill");
2017
+ }
2018
+ }
2019
+ catch (error) {
2020
+ sourceWarnings.push(`skill: ${scrub(error instanceof Error ? error.message : String(error))}`);
2021
+ }
2022
+ const warning = sourceWarnings.length > 0
2023
+ ? sourceWarnings.join("; ").slice(0, 160)
2024
+ : undefined;
2025
+ return {
2026
+ commands: [...byKey.values()],
2027
+ ...(warning ? { warning } : {}),
2028
+ };
1432
2029
  }
1433
2030
  async getRuntimeSnapshot(sessionId, options) {
1434
2031
  try {
@@ -1472,6 +2069,9 @@ export class ConsolePiRuntime {
1472
2069
  throw new RuntimeSnapshotUnavailableError(`model enumeration failed: ${error instanceof Error ? error.message : String(error)}`);
1473
2070
  }
1474
2071
  }
2072
+ // Thinking options: auto is always offered; explicit levels only when the
2073
+ // runtime session supports setThinkingLevel (xhigh/max ship with the SDK
2074
+ // and therefore surface only when this runtime provides them).
1475
2075
  const supportedLevels = typeof session.setThinkingLevel === "function"
1476
2076
  ? [...THINKING_LEVELS]
1477
2077
  : [];
@@ -1479,25 +2079,220 @@ export class ConsolePiRuntime {
1479
2079
  { value: "auto" },
1480
2080
  ...supportedLevels.map((value) => ({ value })),
1481
2081
  ];
1482
- let skillPrefs = { schemaVersion: 1, skills: {} };
1483
- let skillPrefsError;
2082
+ // Resource inventories from the session ResourceLoader (ADR 0012 open
2083
+ // surface). Missing loader → real empty lists, never fabricated entries.
2084
+ // Each subface is isolated (RF-02): a throwing subface keeps the other
2085
+ // inventories AND surfaces a structured retryable diagnostic instead of
2086
+ // silently dropping the whole resource projection.
2087
+ const contextFiles = [];
2088
+ const skills = [];
2089
+ const extensions = [];
2090
+ const diagnostics = [];
2091
+ const resolvedResources = [];
2092
+ const resourceFailure = (subface, error) => {
2093
+ diagnostics.push({
2094
+ type: "error",
2095
+ code: "RESOURCE_LOAD_FAILED",
2096
+ message: `${subface} inventory failed: ${error instanceof Error ? error.message : String(error)}`,
2097
+ retryable: true,
2098
+ });
2099
+ };
1484
2100
  if (resourceLoader) {
2101
+ try {
2102
+ const agentsFiles = resourceLoader.getAgentsFiles?.();
2103
+ for (const file of agentsFiles?.agentsFiles ?? []) {
2104
+ if (!file.path)
2105
+ continue;
2106
+ contextFiles.push({
2107
+ path: file.path,
2108
+ characters: file.content?.length ?? 0,
2109
+ });
2110
+ }
2111
+ }
2112
+ catch (error) {
2113
+ resourceFailure("context files", error);
2114
+ }
2115
+ // Preferences are the source of truth for UI auto-invocation state;
2116
+ // loader disableModelInvocation may also reflect frontmatter. Prefer
2117
+ // explicit preference (default on) when projecting autoInvocationEnabled.
2118
+ let skillPrefs = { schemaVersion: 1, skills: {} };
1485
2119
  try {
1486
2120
  skillPrefs = await this.loadSkillAutoInvocationPreferences();
1487
2121
  }
1488
2122
  catch (error) {
1489
- skillPrefsError = error;
2123
+ diagnostics.push({
2124
+ type: "error",
2125
+ code: "PI_RESOURCE_PREFERENCES_MALFORMED",
2126
+ message: error instanceof Error ? error.message : String(error),
2127
+ retryable: true,
2128
+ });
2129
+ }
2130
+ try {
2131
+ const skillsResult = resourceLoader.getSkills?.();
2132
+ for (const skill of skillsResult?.skills ?? []) {
2133
+ const resourceId = skillResourceId({
2134
+ name: skill.name,
2135
+ filePath: skill.filePath,
2136
+ });
2137
+ // Preference off always means autoInvocationEnabled=false even if
2138
+ // loader already applied disableModelInvocation. Frontmatter-only
2139
+ // disable (no preference) also surfaces as autoInvocationEnabled=false.
2140
+ const prefEnabled = isAutoInvocationEnabled(skillPrefs, resourceId);
2141
+ const autoInvocationEnabled = prefEnabled && skill.disableModelInvocation !== true;
2142
+ skills.push({
2143
+ name: skill.name,
2144
+ description: skill.description ?? "",
2145
+ path: skill.filePath,
2146
+ ...(skill.baseDir ? { baseDir: skill.baseDir } : {}),
2147
+ scope: skill.sourceInfo?.scope ?? "unknown",
2148
+ source: skill.sourceInfo?.source ?? "",
2149
+ resourceId,
2150
+ autoInvocationEnabled,
2151
+ diagnostics: [],
2152
+ });
2153
+ resolvedResources.push({
2154
+ type: "skill",
2155
+ name: skill.name,
2156
+ path: skill.filePath,
2157
+ source: skill.sourceInfo?.source ?? "",
2158
+ scope: skill.sourceInfo?.scope ?? "unknown",
2159
+ });
2160
+ }
2161
+ for (const diag of skillsResult?.diagnostics ?? []) {
2162
+ if (!diag?.message)
2163
+ continue;
2164
+ diagnostics.push({
2165
+ type: diag.type ?? "warning",
2166
+ message: diag.message,
2167
+ ...(diag.path ? { path: diag.path } : {}),
2168
+ });
2169
+ }
2170
+ }
2171
+ catch (error) {
2172
+ resourceFailure("skills", error);
2173
+ }
2174
+ try {
2175
+ const extResult = resourceLoader.getExtensions?.();
2176
+ for (const ext of extResult?.extensions ?? []) {
2177
+ const path = ext.resolvedPath ?? ext.path ?? "";
2178
+ extensions.push({
2179
+ name: ext.sourceInfo?.source ?? ext.path ?? "extension",
2180
+ path,
2181
+ ...(ext.resolvedPath ? { resolvedPath: ext.resolvedPath } : {}),
2182
+ source: ext.sourceInfo?.source ?? "",
2183
+ scope: ext.sourceInfo?.scope ?? "unknown",
2184
+ ...(ext.sourceInfo?.origin ? { origin: ext.sourceInfo.origin } : {}),
2185
+ enabled: true,
2186
+ });
2187
+ resolvedResources.push({
2188
+ type: "extension",
2189
+ name: ext.sourceInfo?.source ?? ext.path ?? "extension",
2190
+ path,
2191
+ source: ext.sourceInfo?.source ?? "",
2192
+ scope: ext.sourceInfo?.scope ?? "unknown",
2193
+ });
2194
+ }
2195
+ for (const err of extResult?.errors ?? []) {
2196
+ if (!err?.error)
2197
+ continue;
2198
+ diagnostics.push({
2199
+ type: "error",
2200
+ message: err.error,
2201
+ ...(err.path ? { path: err.path } : {}),
2202
+ });
2203
+ }
2204
+ }
2205
+ catch (error) {
2206
+ resourceFailure("extensions", error);
2207
+ }
2208
+ try {
2209
+ const promptsResult = resourceLoader.getPrompts?.();
2210
+ for (const prompt of promptsResult?.prompts ?? []) {
2211
+ resolvedResources.push({
2212
+ type: "prompt",
2213
+ name: prompt.name,
2214
+ path: prompt.filePath,
2215
+ source: prompt.sourceInfo?.source ?? "",
2216
+ scope: prompt.sourceInfo?.scope ?? "unknown",
2217
+ });
2218
+ }
2219
+ for (const diag of promptsResult?.diagnostics ?? []) {
2220
+ if (!diag?.message)
2221
+ continue;
2222
+ diagnostics.push({
2223
+ type: diag.type ?? "warning",
2224
+ message: diag.message,
2225
+ ...(diag.path ? { path: diag.path } : {}),
2226
+ });
2227
+ }
2228
+ }
2229
+ catch (error) {
2230
+ resourceFailure("prompts", error);
2231
+ }
2232
+ try {
2233
+ const themesResult = resourceLoader.getThemes?.();
2234
+ for (const theme of themesResult?.themes ?? []) {
2235
+ if (!theme.path)
2236
+ continue;
2237
+ resolvedResources.push({
2238
+ type: "theme",
2239
+ name: theme.name ?? theme.path,
2240
+ path: theme.path,
2241
+ source: "",
2242
+ scope: "unknown",
2243
+ });
2244
+ }
2245
+ for (const diag of themesResult?.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("themes", error);
1490
2257
  }
1491
2258
  }
1492
- const inventories = await collectRuntimeInventories({
1493
- resourceLoader,
1494
- skillPrefs,
1495
- skillPrefsError,
1496
- agentDir: await safeGetAgentDir(this.bindings),
1497
- cwd: this.options.cwd,
1498
- tools: session.getAllTools?.() ?? [],
1499
- activeToolNames: session.getActiveToolNames(),
1500
- });
2259
+ // Packages: Pi settings specs + resolved resource metadata. Fields the SDK
2260
+ // does not expose stay absent (real missing, never fabricated). A
2261
+ // readPackageInventory failure is a PARTIAL failure (RF-02): the snapshot
2262
+ // stays 200 with a structured retryable diagnostic — never a silent
2263
+ // fake-empty package list.
2264
+ const { readPackageInventory } = await import("./pi-console-config.js");
2265
+ let packages = [];
2266
+ try {
2267
+ packages = (await readPackageInventory(await safeGetAgentDir(this.bindings), this.options.cwd, resolvedResources));
2268
+ }
2269
+ catch (error) {
2270
+ packages = [];
2271
+ diagnostics.push({
2272
+ type: "error",
2273
+ code: "PACKAGE_INVENTORY_FAILED",
2274
+ message: error instanceof Error ? error.message : String(error),
2275
+ retryable: true,
2276
+ });
2277
+ }
2278
+ const { readMcpInventory } = await import("./mcp-inventory.js");
2279
+ let mcp = { servers: [], unassignedTools: [] };
2280
+ try {
2281
+ mcp = await readMcpInventory({
2282
+ agentDir: await safeGetAgentDir(this.bindings),
2283
+ cwd: this.options.cwd,
2284
+ tools: session.getAllTools?.() ?? [],
2285
+ activeToolNames: session.getActiveToolNames(),
2286
+ });
2287
+ }
2288
+ catch (error) {
2289
+ diagnostics.push({
2290
+ type: "error",
2291
+ code: "MCP_INVENTORY_FAILED",
2292
+ message: error instanceof Error ? error.message : String(error),
2293
+ retryable: true,
2294
+ });
2295
+ }
1501
2296
  return projectRuntimeSnapshot({
1502
2297
  sessionId,
1503
2298
  revision: this.revisions.get(sessionId) ?? 0,
@@ -1522,17 +2317,19 @@ export class ConsolePiRuntime {
1522
2317
  thinkingLevels,
1523
2318
  systemPrompt: {
1524
2319
  ...(options?.includeRaw ? { raw: rawSystemPrompt } : {}),
2320
+ // The RAW prompt flows into the projection: text is scrubbed +
2321
+ // length-limited while sha256/characters stay RAW-based (design §2.2).
1525
2322
  text: rawSystemPrompt,
1526
2323
  sha256: createHash("sha256").update(rawSystemPrompt).digest("hex"),
1527
2324
  characters: rawSystemPrompt.length,
1528
2325
  },
1529
2326
  activeTools: session.getActiveToolNames(),
1530
- contextFiles: inventories.contextFiles,
1531
- skills: inventories.skills,
1532
- extensions: inventories.extensions,
1533
- packages: inventories.packages,
1534
- mcp: inventories.mcp,
1535
- diagnostics: inventories.diagnostics,
2327
+ contextFiles,
2328
+ skills,
2329
+ extensions,
2330
+ packages,
2331
+ mcp,
2332
+ diagnostics,
1536
2333
  hasBash: true,
1537
2334
  });
1538
2335
  }
@@ -1550,9 +2347,7 @@ export class ConsolePiRuntime {
1550
2347
  * than silently creating a fresh empty session and losing prior context.
1551
2348
  */
1552
2349
  async reopenSession(init) {
1553
- await this.syncAimaxAuthToken();
1554
2350
  const sessionId = init.sessionId;
1555
- const sessionMode = this.rememberSessionMode(sessionId, init.sessionMode ?? this.sessionModeOf(sessionId));
1556
2351
  // Wait for in-flight create materialization rather than racing reopen.
1557
2352
  const initState = this.sessionInit.get(sessionId);
1558
2353
  if (initState?.status === "initializing") {
@@ -1629,7 +2424,6 @@ export class ConsolePiRuntime {
1629
2424
  sessionManager,
1630
2425
  model: init.model,
1631
2426
  systemPromptSuffix: init.systemPromptSuffix,
1632
- sessionMode,
1633
2427
  });
1634
2428
  const session = built.session;
1635
2429
  // Prefer the SessionManager id (from the JSONL header). Some SDK
@@ -1652,6 +2446,7 @@ export class ConsolePiRuntime {
1652
2446
  message: `reopened Pi sessionId mismatch: expected ${sessionId}, got ${reportedId}`,
1653
2447
  };
1654
2448
  }
2449
+ session.setActiveToolsByName(computeOperatorChatActiveToolNames(session));
1655
2450
  this.sessions.set(sessionId, session);
1656
2451
  this.sessionManagers.set(sessionId, sessionManager);
1657
2452
  this.modelRuntimes.set(sessionId, built.modelRuntime);
@@ -1669,7 +2464,6 @@ export class ConsolePiRuntime {
1669
2464
  createdAt: new Date().toISOString(),
1670
2465
  model: init.model,
1671
2466
  activeTools: session.getActiveToolNames(),
1672
- sessionMode,
1673
2467
  },
1674
2468
  };
1675
2469
  }
@@ -1701,7 +2495,6 @@ export class ConsolePiRuntime {
1701
2495
  createdAt: new Date().toISOString(),
1702
2496
  model,
1703
2497
  activeTools: session.getActiveToolNames(),
1704
- sessionMode: this.sessionModeOf(sessionId),
1705
2498
  };
1706
2499
  }
1707
2500
  /**
@@ -1710,22 +2503,46 @@ export class ConsolePiRuntime {
1710
2503
  * before AgentSession is ready. Tool definitions reuse runtime-level caches.
1711
2504
  */
1712
2505
  async buildSessionWithServices(init) {
1713
- const sessionMode = this.rememberSessionMode(init.sessionId, init.sessionMode ?? this.sessionModeOf(init.sessionId));
1714
- const prepared = await this.prepareLiveSession({
1715
- sessionId: init.sessionId,
2506
+ const agentDir = await safeGetAgentDir(this.bindings);
2507
+ const modelConfigFile = await captureModelConfigFile(agentDir);
2508
+ const appendSystemPrompt = [
2509
+ OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
2510
+ ...(init.systemPromptSuffix ? [init.systemPromptSuffix] : []),
2511
+ ];
2512
+ await this.refreshSkillPrefsCache().catch(() => undefined);
2513
+ const { services } = await this.bindings.createServices({
2514
+ cwd: this.options.cwd,
2515
+ agentDir,
2516
+ resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt }, init.sessionId),
2517
+ });
2518
+ const modelRuntime = services.modelRuntime;
2519
+ const resolvedModel = init.model
2520
+ ? (this.bindings.resolveModel({
2521
+ modelRuntime,
2522
+ provider: init.model.provider,
2523
+ modelId: init.model.modelId,
2524
+ }) ?? undefined)
2525
+ : undefined;
2526
+ assertNoWriteToolInList(OPERATOR_CHAT_ALLOWED_TOOLS);
2527
+ const customTools = await this.loadCustomTools(init.sessionId);
2528
+ const { session } = await this.bindings.createSessionFromServices({
2529
+ services,
1716
2530
  sessionManager: init.sessionManager,
1717
- model: init.model,
1718
- systemPromptSuffix: init.systemPromptSuffix,
1719
- sessionMode,
2531
+ excludeTools: [...OPERATOR_CHAT_SESSION_EXCLUDE_TOOLS],
2532
+ ...(customTools.length > 0 ? { customTools } : {}),
2533
+ ...(resolvedModel ? { model: resolvedModel } : {}),
1720
2534
  });
1721
- if (!prepared) {
1722
- throw new Error(`chat session preparation aborted: ${init.sessionId}`);
1723
- }
2535
+ await bindSessionExtensions(session);
2536
+ pinOperatorChatTools(session);
2537
+ this.applySessionCompactionOverride(init.sessionId, services, session);
1724
2538
  return {
1725
- session: prepared.session,
1726
- modelRuntime: prepared.modelRuntime,
1727
- modelConfigFile: prepared.modelConfigFile,
1728
- services: prepared.services,
2539
+ session,
2540
+ modelRuntime,
2541
+ modelConfigFile,
2542
+ services: {
2543
+ resourceLoader: services.resourceLoader,
2544
+ settingsManager: services.settingsManager,
2545
+ },
1729
2546
  };
1730
2547
  }
1731
2548
  /** Re-open a previously created session (cross-request). */
@@ -1785,7 +2602,7 @@ export class ConsolePiRuntime {
1785
2602
  }
1786
2603
  /**
1787
2604
  * Send a prompt and stream events. Gate 3: each tool invocation is
1788
- * re-authorized by authorizeChatSessionTool(mode) before the dispatcher runs it
2605
+ * re-authorized by authorizeOperatorChatTool before the dispatcher runs it
1789
2606
  * (the dispatcher is wired by the HTTP layer, see chat-session.ts).
1790
2607
  *
1791
2608
  * Prompt ownership is acquired synchronously before the first await so two
@@ -1848,11 +2665,10 @@ export class ConsolePiRuntime {
1848
2665
  // existing closed interview allow/deny registry. The next ordinary turn
1849
2666
  // explicitly restores the normal Operator Chat set (baseline + extension
1850
2667
  // tools, ADR 0012).
1851
- const sessionMode = this.sessionModeOf(sessionId);
1852
- const requestedTools = chatActiveToolBaseline(sessionMode);
1853
- const activeTools = options?.mode === "requirement-interview" && sessionMode === "workflow"
2668
+ const requestedTools = OPERATOR_CHAT_ACTIVE_TOOL_NAMES();
2669
+ const activeTools = options?.mode === "requirement-interview"
1854
2670
  ? filterActiveInterviewTools(requestedTools).allowed
1855
- : computeChatActiveToolNames(session, sessionMode);
2671
+ : computeOperatorChatActiveToolNames(session);
1856
2672
  session.setActiveToolsByName(activeTools);
1857
2673
  // Final pre-prompt fence: abort after tool pin must still skip session.prompt.
1858
2674
  if (options?.signal?.aborted) {
@@ -2190,7 +3006,6 @@ export class ConsolePiRuntime {
2190
3006
  this.modelRuntimes.delete(sessionId);
2191
3007
  this.modelConfigFiles.delete(sessionId);
2192
3008
  this.sessionModels.delete(sessionId);
2193
- this.sessionModes.delete(sessionId);
2194
3009
  this.serviceScopes.delete(sessionId);
2195
3010
  this.revisions.delete(sessionId);
2196
3011
  this.thinkingSelections.delete(sessionId);
@@ -2358,9 +3173,155 @@ async function waitForIdle(session, options) {
2358
3173
  await new Promise((r) => setTimeout(r, 50));
2359
3174
  }
2360
3175
  }
3176
+ async function safeGetAgentDir(bindings) {
3177
+ try {
3178
+ return await bindings.getAgentDir();
3179
+ }
3180
+ catch {
3181
+ // Fall back to a Console-app-data scoped agent dir so we never accidentally
3182
+ // touch the user's real ~/.pi/agent config if getAgentDir throws.
3183
+ return path.join(process.cwd(), ".harness", "console", "pi-agent-dir");
3184
+ }
3185
+ }
3186
+ /**
3187
+ * Cached SDK module loader. The pi-coding-agent package is ESM-only, so we
3188
+ * must use dynamic import() (not require). Cached to avoid repeated imports.
3189
+ */
3190
+ let cachedSdk;
3191
+ async function loadSdk() {
3192
+ if (!cachedSdk) {
3193
+ cachedSdk = import("@earendil-works/pi-coding-agent");
3194
+ }
3195
+ return cachedSdk;
3196
+ }
3197
+ /**
3198
+ * Default production bindings backed by the real Pi SDK (services mode).
3199
+ * Dynamically imported so the worker bundle does not hard-fail if the SDK is
3200
+ * absent at module load (discovered lazily by pi-readiness instead).
3201
+ */
3202
+ export function createDefaultPiSdkBindings() {
3203
+ return {
3204
+ getAgentDir: async () => {
3205
+ const sdk = (await loadSdk());
3206
+ return sdk.getAgentDir();
3207
+ },
3208
+ createServices: async (opts) => {
3209
+ const sdk = (await loadSdk());
3210
+ // createAgentSessionServices returns the full AgentSessionServices object
3211
+ // directly (cwd/agentDir/modelRuntime/settingsManager/resourceLoader).
3212
+ // Wrap it so our type contract is satisfied.
3213
+ const result = await sdk.createAgentSessionServices({
3214
+ cwd: opts.cwd,
3215
+ agentDir: opts.agentDir,
3216
+ resourceLoaderOptions: opts.resourceLoaderOptions,
3217
+ });
3218
+ return { services: result };
3219
+ },
3220
+ createSessionFromServices: async (opts) => {
3221
+ const sdk = (await loadSdk());
3222
+ return sdk.createAgentSessionFromServices({
3223
+ services: opts.services,
3224
+ sessionManager: opts.sessionManager,
3225
+ ...(opts.tools ? { tools: opts.tools } : {}),
3226
+ ...(opts.noTools ? { noTools: opts.noTools } : {}),
3227
+ ...(opts.excludeTools ? { excludeTools: opts.excludeTools } : {}),
3228
+ ...(opts.customTools ? { customTools: opts.customTools } : {}),
3229
+ ...(opts.model ? { model: opts.model } : {}),
3230
+ });
3231
+ },
3232
+ createSessionManager: async (opts) => {
3233
+ const sdk = (await loadSdk());
3234
+ return sdk.SessionManager.create(opts.cwd, opts.sessionDir, {
3235
+ id: opts.sessionId,
3236
+ });
3237
+ },
3238
+ materializeSessionShell: async (opts) => {
3239
+ const sdk = (await loadSdk());
3240
+ const manager = opts.sessionManager;
3241
+ if (typeof manager.setSessionFile !== "function") {
3242
+ throw new Error("Pi SessionManager cannot materialize a session file");
3243
+ }
3244
+ let created = false;
3245
+ try {
3246
+ await mkdir(path.dirname(opts.sessionFile), { recursive: true });
3247
+ // Match Pi's documented session header. Rebinding through the public
3248
+ // SessionManager API marks it flushed, so Pi appends the first user
3249
+ // entry instead of trying to create the already-present file later.
3250
+ await writeFile(opts.sessionFile, `${JSON.stringify({
3251
+ type: "session",
3252
+ version: typeof sdk.CURRENT_SESSION_VERSION === "number"
3253
+ ? sdk.CURRENT_SESSION_VERSION
3254
+ : 3,
3255
+ id: opts.sessionId,
3256
+ timestamp: new Date().toISOString(),
3257
+ cwd: opts.cwd,
3258
+ })}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
3259
+ created = true;
3260
+ manager.setSessionFile(opts.sessionFile);
3261
+ }
3262
+ catch (error) {
3263
+ if (created)
3264
+ await rm(opts.sessionFile, { force: true }).catch(() => undefined);
3265
+ throw error;
3266
+ }
3267
+ },
3268
+ createInMemorySessionManager: async (opts) => {
3269
+ const sdk = (await loadSdk());
3270
+ return sdk.SessionManager.inMemory(opts.cwd);
3271
+ },
3272
+ forkSessionManager: async (opts) => {
3273
+ const sdk = (await loadSdk());
3274
+ return sdk.SessionManager.forkFrom(opts.sourcePath, opts.targetCwd, opts.targetSessionDir, { id: opts.targetSessionId });
3275
+ },
3276
+ openSessionManager: async (opts) => {
3277
+ const sdk = (await loadSdk());
3278
+ try {
3279
+ // Pi SessionManager.open(missingPath) does NOT throw — it creates a
3280
+ // fresh in-memory session with a random UUID. That would reopen the
3281
+ // wrong conversation under the durable Console sessionId. Fail closed
3282
+ // before open when the persisted file is absent.
3283
+ await access(opts.sessionFile);
3284
+ }
3285
+ catch (error) {
3286
+ const code = error && typeof error === "object" && "code" in error
3287
+ ? String(error.code ?? "")
3288
+ : "";
3289
+ if (code === "ENOENT") {
3290
+ throw Object.assign(new Error(`SESSION_FILE_MISSING: ${opts.sessionFile}`), { code: "SESSION_FILE_MISSING", cause: error });
3291
+ }
3292
+ // File exists but is unreadable (permissions / I/O) — caller maps
3293
+ // undefined to 503 rather than silently creating a fresh session.
3294
+ return undefined;
3295
+ }
3296
+ return sdk.SessionManager.open(opts.sessionFile);
3297
+ },
3298
+ resolveModel: (opts) => {
3299
+ const rt = opts.modelRuntime;
3300
+ if (typeof rt?.getModel !== "function")
3301
+ return undefined;
3302
+ try {
3303
+ return rt.getModel(opts.provider, opts.modelId);
3304
+ }
3305
+ catch {
3306
+ return undefined;
3307
+ }
3308
+ },
3309
+ listAvailableModels: (opts) => {
3310
+ const rt = opts.modelRuntime;
3311
+ const available = rt?.snapshot?.available ?? [];
3312
+ const configured = rt?.snapshot?.configuredProviders;
3313
+ const isConfigured = (provider) => configured instanceof Set ? configured.has(provider) : false;
3314
+ return available.map((m) => ({
3315
+ id: m.id,
3316
+ name: m.name,
3317
+ provider: m.provider,
3318
+ hasCredentials: isConfigured(m.provider),
3319
+ }));
3320
+ },
3321
+ };
3322
+ }
2361
3323
  /**
2362
- * Re-export the tool gates for the dispatcher layer so the per-tool-call gate
2363
- * (Gate 3) lives in exactly one place. authorizeChatSessionTool is the
2364
- * mode-aware gate; the legacy name stays for workflow-only callers.
3324
+ * Re-export the tool gate for the dispatcher layer so the per-tool-call gate
3325
+ * (Gate 3) lives in exactly one place.
2365
3326
  */
2366
- export { authorizeChatSessionTool, authorizeOperatorChatTool, assertNoWriteToolInList, OPERATOR_CHAT_ALLOWED_TOOLS, };
3327
+ export { authorizeOperatorChatTool, assertNoWriteToolInList, OPERATOR_CHAT_ALLOWED_TOOLS, };