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

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 (109) hide show
  1. package/CHANGELOG.md +81 -9
  2. package/dist/application/task-lifecycle/advance.js +17 -4
  3. package/dist/application/task-lifecycle/observe.js +43 -29
  4. package/dist/application/task-lifecycle/plan-transitions.js +5 -4
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/executors/dag-pi-executor.js +244 -33
  7. package/dist/executors/pi-executor.js +2 -0
  8. package/dist/executors/pi-sdk-executor.js +75 -39
  9. package/dist/task/source-prepare/source-fidelity-pi.js +2 -2
  10. package/dist/task/source-references.js +48 -23
  11. package/dist/worker/console/chat/assistant-content.js +23 -2
  12. package/dist/worker/console/chat/explore-tools.js +13 -0
  13. package/dist/worker/console/chat/pi-runtime.js +131 -95
  14. package/dist/worker/console/chat/routes.js +175 -54
  15. package/dist/worker/console/chat/scm-routes.js +193 -0
  16. package/dist/worker/console/chat/scm-service.js +253 -0
  17. package/dist/worker/console/chat/scm-tools.js +111 -0
  18. package/dist/worker/console/chat/sdd-data-alignment.js +65 -9
  19. package/dist/worker/console/chat/session-catalog.js +1 -0
  20. package/dist/worker/console/chat/session-mode-view.js +48 -0
  21. package/dist/worker/console/chat/session-mode.js +218 -0
  22. package/dist/worker/console/chat/session-store.js +22 -6
  23. package/dist/worker/console/chat/shortcuts.js +6 -0
  24. package/dist/worker/console/chat/terminal-routes.js +216 -0
  25. package/dist/worker/console/chat/terminal-sessions.js +284 -0
  26. package/dist/worker/console/chat/terminal-tools.js +199 -0
  27. package/dist/worker/console/chat/tool-preview.js +11 -0
  28. package/dist/worker/console/chat/tools.js +2 -0
  29. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-hUmM_GRf.js → abnfDiagram-N423BO3Z-BuyrlONK.js} +1 -1
  30. package/dist/worker/console/static/assets/{arc-Bt-xuieW.js → arc-BvCl_wpi.js} +1 -1
  31. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DTqkL4DJ.js → architectureDiagram-T3A2C74G-BkBpBaRq.js} +1 -1
  32. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Cy0MNnzV.js → blockDiagram-VBNYF7ZC-yve4Mcb_.js} +1 -1
  33. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DDdlT0Pd.js → c4Diagram-5PPSVZJV-9yLGboBx.js} +1 -1
  34. package/dist/worker/console/static/assets/channel-BRahIhjw.js +1 -0
  35. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-3JawwNZ6.js → chunk-2GRJ4B5K-BKP69b8r.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DoTp7Cri.js → chunk-2Q5K7J3B-NWFRnXju.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CcirhZaS.js → chunk-5RXB4S5H-BiweqLEc.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-VrG21gnP.js → chunk-5VM5RSS4-CP9GHmRY.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DDOgIS70.js → chunk-6Q2QTUOP-BiGDkBtj.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CBjmgvJ_.js → chunk-GF5L2VYU-DkJ4ZjQS.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CCQSht8M.js → chunk-JWPE2WC7-B5jjn7zq.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BpGMkUUV.js → chunk-KBJHAD2P-C6KoK9O0.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DNDT41QL.js → chunk-RYQCIY6F-BvNIfyn-.js} +1 -1
  44. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-euXnDFbn.js → chunk-XXDRQBXY-BZ33iX4W.js} +1 -1
  45. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-GOC80mhq.js +1 -0
  46. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-GOC80mhq.js +1 -0
  47. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BJ6NAeql.js → cose-bilkent-JH36ORCC-CP8eIuZd.js} +1 -1
  48. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-F5JKIvIq.js → cynefin-VYW2F7L2-DZECjpOe.js} +1 -1
  49. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Cwmkl_Gi.js → cynefinDiagram-MW4NZA55-CvVJoFqf.js} +1 -1
  50. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BmCZ8815.js → dagre-VZM6K2ZE-D_JTqdAn.js} +1 -1
  51. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zWA3hUnX.js → diagram-7IWD3JNH-Db7AQ0z0.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BzA-888F.js → diagram-B4RE2ZJO-AZPVo5CZ.js} +1 -1
  53. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-tPNheoch.js → diagram-LBJQPF4R-BgXqI3E6.js} +1 -1
  54. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Cdi7Fyqp.js → diagram-Q27KOJAE-BYjFn3Ba.js} +1 -1
  55. package/dist/worker/console/static/assets/{diagram-UB23O5K3-DFK9VBf1.js → diagram-UB23O5K3-B4rJgGGO.js} +1 -1
  56. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-xCUr1kpg.js → ebnfDiagram-BXEA7PRR-C6XWZhpw.js} +1 -1
  57. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BANQHY_A.js → erDiagram-JOGREHBK-1YdQRIQW.js} +1 -1
  58. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-VHJYEMkP.js → flowDiagram-UKHOOZJN-l3a0AwN0.js} +1 -1
  59. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiyZQ0C6.js → ganttDiagram-PKOTCBZU-CQL96F2Z.js} +1 -1
  60. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CZtIeBBa.js → gitGraphDiagram-DS77QQ5N-BxCvafRb.js} +1 -1
  61. package/dist/worker/console/static/assets/index-4wCGsXuU.js +465 -0
  62. package/dist/worker/console/static/assets/index-CiF9WBoR.css +1 -0
  63. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BuSspC0X.js → infoDiagram-6WML65LV-BUipIeDO.js} +1 -1
  64. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DRJZyPFy.js → ishikawaDiagram-WSZJBQD7-CRfomVMX.js} +1 -1
  65. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DIfXIVuE.js → journeyDiagram-NVQOT4AX-_UFvyviX.js} +1 -1
  66. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CT10_7Lt.js → kanban-definition-27J2QSJJ-CKIRoqKv.js} +1 -1
  67. package/dist/worker/console/static/assets/{linear-ufemj74H.js → linear-CjttJDCM.js} +1 -1
  68. package/dist/worker/console/static/assets/{mermaid.core-DEQe8-_m.js → mermaid.core-DPvHX155.js} +5 -5
  69. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BRLUrbrC.js → mindmap-definition-FAOFIHXS-Qmj-qwBe.js} +1 -1
  70. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BgibKtiF.js → pegDiagram-VL7TDLO6-Auw6DRgq.js} +1 -1
  71. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-D3eyfdV9.js → pieDiagram-7S7Q4E2Y-CqMrfD4c.js} +1 -1
  72. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DXeTk5Px.js → quadrantDiagram-CIZ2JOQS-BAddeggH.js} +1 -1
  73. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BgXdFEe7.js → railroadDiagram-AXF67PYL-DG1JuOu2.js} +1 -1
  74. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CseC2A7R.js → requirementDiagram-LRYGKXZP-DLYGMKlC.js} +1 -1
  75. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-D80PFzfJ.js → sankeyDiagram-W5VNT64P-g68SbLM9.js} +1 -1
  76. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BWzHW7LS.js → sequenceDiagram-SI44F4Z6-CmszLKv8.js} +1 -1
  77. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-xWQYTLAn.js → sizeCapture-X5ZJPWSS-D2tEKUCj.js} +1 -1
  78. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CLxtpZeL.js → stateDiagram-OKZ733FA-Syc5E_Jf.js} +1 -1
  79. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CwWwuPI-.js +1 -0
  80. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DwugtmWV.js → swimlanes-SLNWSIFB-Ibn0IRCa.js} +2 -2
  81. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-A7fqemr6.js +8 -0
  82. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DHEOwHIe.js → timeline-definition-Z64GVDOM-2ZW9W3iS.js} +1 -1
  83. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-pmbZKlBK.js → vennDiagram-T6HMQDX7-v50HsKUh.js} +1 -1
  84. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CivHoM5q.js → wardleyDiagram-T6FBY63Y-DqCXEoDH.js} +1 -1
  85. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BfHIE7o6.js → xychartDiagram-ELKLHX3M-vIP8Vo9d.js} +1 -1
  86. package/dist/worker/console/static/index.html +2 -2
  87. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +122 -10
  88. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +27 -15
  89. package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
  90. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
  91. package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
  92. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +20 -4
  93. package/dist/worker/console/static-src/operator-chat/useChatStream.js +18 -2
  94. package/dist/worker/console/static-src/operator-chat/useChatThread.js +39 -4
  95. package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -7
  96. package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
  97. package/dist/worker/console/workspace-context.js +15 -1
  98. package/dist/workflows/dag/frontend-capacity.js +9 -0
  99. package/dist/workflows/dag/frontend-design-policy.js +2 -1
  100. package/dist/workflows/dag/frontend-shape-facts.js +12 -2
  101. package/package.json +6 -3
  102. package/skills/frontend-plan/SKILL.md +7 -0
  103. package/dist/worker/console/static/assets/channel-BpzRnZmU.js +0 -1
  104. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlMVqCc.js +0 -1
  105. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlMVqCc.js +0 -1
  106. package/dist/worker/console/static/assets/index-BlN6ytLr.css +0 -1
  107. package/dist/worker/console/static/assets/index-CA-jfWlE.js +0 -449
  108. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CiVoq4G-.js +0 -1
  109. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BkdgdpfR.js +0 -8
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Console Chat session modes (workflow | pi).
3
+ *
4
+ * sessionMode is a durable session identity, independent of per-turn
5
+ * operator | requirement-interview and of UI layout mode. Record is the
6
+ * source of truth; runtime, catalog, list/detail and the browser cache
7
+ * all project this field.
8
+ */
9
+ import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_BUILTIN_CUSTOM_TOOLS, OPERATOR_CHAT_PI_BUILTIN_TOOLS, authorizeOperatorChatTool, isOperatorChatToolDenied, } from "./tools.js";
10
+ import { OPERATOR_CHAT_AGENT_TOOL_NAME } from "./subagents/types.js";
11
+ import { CONSOLE_CHAT_SESSION_MODES, DEFAULT_CONSOLE_CHAT_SESSION_MODE, } from "./session-mode-view.js";
12
+ export { CONSOLE_CHAT_SESSION_MODES, DEFAULT_CONSOLE_CHAT_SESSION_MODE, SESSION_MODE_COPY, } from "./session-mode-view.js";
13
+ export const SESSION_MODE_INVALID_CODE = "INVALID_SESSION_MODE";
14
+ export const SESSION_MODE_UNKNOWN_CODE = "UNKNOWN_SESSION_MODE";
15
+ export const SESSION_MODE_ACTION_DENIED_CODE = "SESSION_MODE_ACTION_DENIED";
16
+ export const SESSION_MODE_IMMUTABLE_CODE = "SESSION_MODE_IMMUTABLE";
17
+ export class InvalidChatSessionModeError extends Error {
18
+ value;
19
+ code = SESSION_MODE_INVALID_CODE;
20
+ constructor(value) {
21
+ super(`sessionMode must be "workflow" or "pi"${value === undefined || value === null || value === ""
22
+ ? ""
23
+ : `; got ${String(value)}`}`);
24
+ this.value = value;
25
+ this.name = "InvalidChatSessionModeError";
26
+ }
27
+ }
28
+ export class UnknownChatSessionModeError extends Error {
29
+ value;
30
+ code = SESSION_MODE_UNKNOWN_CODE;
31
+ constructor(value) {
32
+ super(`chat session record has unknown sessionMode ${JSON.stringify(value)}; refusing to run as workflow`);
33
+ this.value = value;
34
+ this.name = "UnknownChatSessionModeError";
35
+ }
36
+ }
37
+ const MODE_SET = new Set(CONSOLE_CHAT_SESSION_MODES);
38
+ function isSessionMode(value) {
39
+ return typeof value === "string" && MODE_SET.has(value);
40
+ }
41
+ /**
42
+ * Create-path parser. Missing / null / "" → workflow. Explicit illegal
43
+ * values fail closed and must not silently degrade.
44
+ */
45
+ export function parseCreateSessionMode(value) {
46
+ if (value === undefined || value === null || value === "") {
47
+ return DEFAULT_CONSOLE_CHAT_SESSION_MODE;
48
+ }
49
+ if (isSessionMode(value))
50
+ return value;
51
+ throw new InvalidChatSessionModeError(value);
52
+ }
53
+ /**
54
+ * Restore-path parser. Missing field → workflow (stock-data rule, not a
55
+ * compatibility shim). Present but unknown → fail closed.
56
+ */
57
+ export function readStoredSessionMode(value) {
58
+ if (value === undefined || value === null || value === "") {
59
+ return DEFAULT_CONSOLE_CHAT_SESSION_MODE;
60
+ }
61
+ if (isSessionMode(value))
62
+ return value;
63
+ throw new UnknownChatSessionModeError(value);
64
+ }
65
+ export function isWorkflowSessionMode(mode) {
66
+ return mode === "workflow";
67
+ }
68
+ export function isPiSessionMode(mode) {
69
+ return mode === "pi";
70
+ }
71
+ /** Shared Console foundations that both modes keep. Not a native-only prompt. */
72
+ export const PI_CHAT_SYSTEM_PROMPT_BASE = [
73
+ "You are Console Chat in Pi mode: a coding assistant with Pi tools plus Console shared foundations.",
74
+ "Use read/write/edit/bash/grep/find/ls to inspect and change the workspace. apply_patch, full-tools, shell and coding-chat are denied.",
75
+ "Agent (Explore) is a built-in read-only subagent for path-uncertain codebase search. Do not use it for a known single-file read. Prefer safe-read/safe-grep for secret-safe probes.",
76
+ "ask_user_question pauses for a real user answer. todo_write tracks the current work and is restored after compression; it is not a workflow auto-continue signal. The persistent terminal tool is available.",
77
+ "This session keeps Console compression (L0/L1/L2, sift, strict checkpoint) and existing Aimax / SDD access. Do not create tasks, run DAGs, drive Goals, or start requirement interviews from this session.",
78
+ "Obey repository AGENTS.md, skills and extensions when they apply. Choosing Pi mode is not extra write authorization and does not bypass Console access control or output scrubbing.",
79
+ ].join("\n");
80
+ export const CHAT_SAFE_EXPLORE_TOOL_IDS = Object.freeze([
81
+ "safe-read",
82
+ "safe-grep",
83
+ "git-status",
84
+ "git-diff",
85
+ ]);
86
+ export const PI_CHAT_GENERIC_HELPER_TOOLS = Object.freeze([
87
+ "ask_user_question",
88
+ "todo_write",
89
+ ]);
90
+ export const PI_CHAT_WORKFLOW_TOOL_IDS = Object.freeze([
91
+ "get_goal",
92
+ "create_goal",
93
+ "update_goal",
94
+ ]);
95
+ const CHAT_EXTENSION_DENIED_TOOL_IDS = new Set([
96
+ "apply_patch",
97
+ "apply-patch",
98
+ "full-tools",
99
+ "full_tools",
100
+ "coding-chat",
101
+ "coding_chat",
102
+ "shell",
103
+ ].map((name) => name.toLowerCase()));
104
+ const WORKFLOW_OPERATOR_TOOL_IDS = new Set(OPERATOR_CHAT_ALLOWED_TOOLS.map((name) => name.toLowerCase()));
105
+ const PI_GOAL_TOOL_IDS = new Set(PI_CHAT_WORKFLOW_TOOL_IDS.map((name) => name.toLowerCase()));
106
+ export function workflowActiveToolBaseline() {
107
+ return [
108
+ ...OPERATOR_CHAT_ALLOWED_TOOLS,
109
+ ...OPERATOR_CHAT_PI_BUILTIN_TOOLS,
110
+ ...CHAT_SAFE_EXPLORE_TOOL_IDS,
111
+ ...OPERATOR_CHAT_BUILTIN_CUSTOM_TOOLS,
112
+ ];
113
+ }
114
+ export function piActiveToolBaseline() {
115
+ return [
116
+ ...OPERATOR_CHAT_PI_BUILTIN_TOOLS,
117
+ ...CHAT_SAFE_EXPLORE_TOOL_IDS,
118
+ ...OPERATOR_CHAT_BUILTIN_CUSTOM_TOOLS,
119
+ ...PI_CHAT_GENERIC_HELPER_TOOLS,
120
+ ];
121
+ }
122
+ export function chatActiveToolBaseline(sessionMode) {
123
+ return sessionMode === "pi"
124
+ ? piActiveToolBaseline()
125
+ : workflowActiveToolBaseline();
126
+ }
127
+ /**
128
+ * Gate 2 pin: mode baseline plus user/project extension tools. Pi mode must
129
+ * not re-add operator actions or Goal tools via the extras pass.
130
+ */
131
+ export function computeChatActiveToolNames(session, sessionMode) {
132
+ const baseline = chatActiveToolBaseline(sessionMode);
133
+ if (!session?.getAllTools)
134
+ return baseline;
135
+ const baselineLower = new Set(baseline.map((name) => name.toLowerCase()));
136
+ const extras = [];
137
+ for (const tool of session.getAllTools()) {
138
+ const name = tool?.name;
139
+ if (!name || typeof name !== "string")
140
+ continue;
141
+ const lower = name.toLowerCase();
142
+ if (baselineLower.has(lower))
143
+ continue;
144
+ if (CHAT_EXTENSION_DENIED_TOOL_IDS.has(lower))
145
+ continue;
146
+ if (sessionMode === "pi" && WORKFLOW_OPERATOR_TOOL_IDS.has(lower))
147
+ continue;
148
+ if (sessionMode === "pi" && PI_GOAL_TOOL_IDS.has(lower))
149
+ continue;
150
+ extras.push(name);
151
+ }
152
+ return extras.length > 0 ? [...baseline, ...extras] : baseline;
153
+ }
154
+ export function authorizeChatSessionTool(toolId, sessionMode) {
155
+ if (sessionMode === "workflow")
156
+ return authorizeOperatorChatTool(toolId);
157
+ const id = toolId.trim();
158
+ if (!id) {
159
+ return {
160
+ ok: false,
161
+ code: "tool-unknown",
162
+ toolId: id,
163
+ message: "empty tool id is not allowed in Pi mode",
164
+ };
165
+ }
166
+ if (isOperatorChatToolDenied(id)) {
167
+ return {
168
+ ok: false,
169
+ code: "tool-denied",
170
+ toolId: id,
171
+ message: `tool "${id}" is denied in Pi mode (no apply_patch/full-tools/shell/coding-chat)`,
172
+ };
173
+ }
174
+ const lower = id.toLowerCase();
175
+ if (WORKFLOW_OPERATOR_TOOL_IDS.has(lower) || PI_GOAL_TOOL_IDS.has(lower)) {
176
+ return {
177
+ ok: false,
178
+ code: "tool-denied",
179
+ toolId: id,
180
+ message: `tool "${id}" is a workflow action and is not available in Pi mode`,
181
+ };
182
+ }
183
+ const authorized = new Set(piActiveToolBaseline().map((name) => name.toLowerCase()));
184
+ if (authorized.has(lower))
185
+ return { ok: true, toolId: id };
186
+ return {
187
+ ok: false,
188
+ code: "tool-unknown",
189
+ toolId: id,
190
+ message: `tool "${id}" is not registered for Pi mode`,
191
+ };
192
+ }
193
+ export function composeChatSystemPromptParts(input) {
194
+ const base = input.sessionMode === "pi"
195
+ ? PI_CHAT_SYSTEM_PROMPT_BASE
196
+ : input.workflowPrompt;
197
+ const suffix = input.sessionMode === "workflow" ? input.systemPromptSuffix?.trim() : "";
198
+ return suffix ? [base, suffix] : [base];
199
+ }
200
+ export function composeChatSystemPrompt(input) {
201
+ return composeChatSystemPromptParts(input).join("\n\n");
202
+ }
203
+ export const PI_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 advances. Skip the list for simple Q&A, read-only lookups, and single-step tasks. Statuses: `pending` / `in_progress` / `completed`.";
204
+ // Slash gating lives in the browser-safe view module (shortcuts.ts is in the
205
+ // UI bundle); re-export here for Node-side consumers.
206
+ export { WORKFLOW_EXCLUSIVE_SLASH_ACTIONS, isWorkflowExclusiveSlashAction, } from "./session-mode-view.js";
207
+ export function shouldLoadWorkflowBusinessTools(sessionMode) {
208
+ return sessionMode === "workflow";
209
+ }
210
+ export function shouldInjectGoalContinuation(sessionMode) {
211
+ return sessionMode === "workflow";
212
+ }
213
+ export function shouldAcceptOperatorSnapshot(sessionMode) {
214
+ return sessionMode === "workflow";
215
+ }
216
+ export const SESSION_MODE_ACTION_DENIED_MESSAGE = "this action is only available in workflow sessions";
217
+ /** Stable tool name used by tests and catalogs for the Explore subagent. */
218
+ export const PI_CHAT_AGENT_TOOL_NAME = OPERATOR_CHAT_AGENT_TOOL_NAME;
@@ -15,6 +15,7 @@ import { readdir, rm } from "node:fs/promises";
15
15
  import { readJsonIfExists, writeSecureJson, } from "../../../infrastructure/console/app-data.js";
16
16
  import { dispatchOperatorAction } from "../operator-actions.js";
17
17
  import { authorizeOperatorChatTool } from "./tools.js";
18
+ import { authorizeChatSessionTool, DEFAULT_CONSOLE_CHAT_SESSION_MODE, readStoredSessionMode, } from "./session-mode.js";
18
19
  import { projectOperationForChat } from "./chat-event-store.js";
19
20
  import { scrubSecrets } from "./explore-tools.js";
20
21
  import { hashDeferredTurnPayload, parseDeferredTurns, } from "./deferred-turn.js";
@@ -106,6 +107,7 @@ export class ChatSessionStore {
106
107
  ...(typeof raw.workspaceId === "string" && raw.workspaceId
107
108
  ? { workspaceId: raw.workspaceId }
108
109
  : {}),
110
+ sessionMode: readStoredSessionMode(raw.sessionMode),
109
111
  deferredTurns: normalizedDeferredTurns,
110
112
  ...(normalizedDeferredTurns[0] ? { deferredTurn: normalizedDeferredTurns[0] } : {}),
111
113
  deferredQueueRevision: Number.isSafeInteger(raw.deferredQueueRevision) && (raw.deferredQueueRevision ?? 0) >= 0
@@ -143,7 +145,17 @@ export class ChatSessionStore {
143
145
  }
144
146
  const records = await Promise.all(names
145
147
  .filter((name) => name.endsWith(".json"))
146
- .map((name) => this.get(name.slice(0, -".json".length))));
148
+ .map(async (name) => {
149
+ try {
150
+ return await this.get(name.slice(0, -".json".length));
151
+ }
152
+ catch (error) {
153
+ // Skip corrupt records (e.g. unknown sessionMode) but stay
154
+ // diagnosable: silent disappearance is untraceable otherwise.
155
+ process.stderr.write(`[console] chat session record skipped in list: ${name.slice(0, -".json".length)}: ${error instanceof Error ? error.message : String(error)}\n`);
156
+ return undefined;
157
+ }
158
+ }));
147
159
  return records
148
160
  .filter((record) => Boolean(record) && (options?.includeArchived || record.state === "active"))
149
161
  .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
@@ -172,6 +184,7 @@ export class ChatSessionStore {
172
184
  updatedAt: now,
173
185
  modelProvider: input.modelProvider,
174
186
  modelId: input.modelId,
187
+ sessionMode: input.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE,
175
188
  messages: [],
176
189
  toolInvocations: [],
177
190
  };
@@ -497,9 +510,10 @@ export class ChatSessionStore {
497
510
  /**
498
511
  * Dispatch a Chat tool invocation into the operator action layer.
499
512
  *
500
- * Gate 3 (per-call): authorizeOperatorChatTool is re-checked here. Even if a
501
- * malicious or drifted session somehow requested a denied tool, this gate
502
- * refuses before any operator action runs (V12 / V15).
513
+ * Gate 3 (per-call): authorizeChatSessionTool is re-checked here with the
514
+ * session mode. Even if a malicious or drifted session somehow requested a
515
+ * denied or workflow-exclusive tool, this gate refuses before any operator
516
+ * action runs (V12 / V15).
503
517
  *
504
518
  * High-risk actions (contractApply / runDag / etc.) are NOT registered as
505
519
  * tools, so they never reach here; but we still deny defensively.
@@ -514,8 +528,10 @@ function redactHumanGateToken(value) {
514
528
  delete clone.humanGateToken;
515
529
  return clone;
516
530
  }
517
- export async function dispatchChatToolCall(ctx, toolName, args, clientRequestId) {
518
- const decision = authorizeOperatorChatTool(toolName);
531
+ export async function dispatchChatToolCall(ctx, toolName, args, clientRequestId, sessionMode) {
532
+ const decision = sessionMode === undefined
533
+ ? authorizeOperatorChatTool(toolName)
534
+ : authorizeChatSessionTool(toolName, sessionMode);
519
535
  if (!decision.ok) {
520
536
  return {
521
537
  ok: false,
@@ -5,6 +5,7 @@
5
5
  * prompt / skill commands are merged via mergeSlashCommands with conflict
6
6
  * priority: operator/web > extension > skill > prompt.
7
7
  */
8
+ import { isWorkflowExclusiveSlashAction } from "./session-mode-view.js";
8
9
  export const SHORTCUTS = [
9
10
  {
10
11
  command: "/task",
@@ -138,7 +139,10 @@ export function mergeSlashCommands(dynamic = [], capabilities = {}) {
138
139
  byName.set(key, { ...item, command: key });
139
140
  }
140
141
  };
142
+ const hideWorkflow = capabilities.sessionMode === "pi";
141
143
  for (const item of SHORTCUTS) {
144
+ if (hideWorkflow && isWorkflowExclusiveSlashAction(item.action))
145
+ continue;
142
146
  upsert({
143
147
  command: item.command,
144
148
  label: item.label,
@@ -148,6 +152,8 @@ export function mergeSlashCommands(dynamic = [], capabilities = {}) {
148
152
  });
149
153
  }
150
154
  for (const item of WEB_SLASH_COMMANDS) {
155
+ if (hideWorkflow && isWorkflowExclusiveSlashAction(item.action))
156
+ continue;
151
157
  upsert({
152
158
  command: item.command,
153
159
  label: item.label,
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Operator Chat — terminal management HTTP API (Console UI side).
3
+ *
4
+ * Endpoints (all under /api/operator/v1/chat):
5
+ * GET /terminals list terminal sessions (all owners)
6
+ * POST /terminals spawn { shell, name?, cwd?, ownerSessionId? }
7
+ * GET /terminals/:id session info + buffered output
8
+ * POST /terminals/:id/input { data } write to the PTY
9
+ * POST /terminals/:id/resize { cols, rows }
10
+ * POST /terminals/:id/interrupt send Ctrl+C
11
+ * POST /terminals/:id/kill stop and await full exit
12
+ * GET /terminals/:id/stream SSE: buffered replay + live chunks
13
+ *
14
+ * Mutating routes require the Console boot token (same gate as other chat
15
+ * mutations). The stream is read-only and uses the same origin checks as the
16
+ * chat event stream.
17
+ */
18
+ import { TerminalSessionError, availableShellKinds, } from "./terminal-sessions.js";
19
+ function sendJson(res, status, body) {
20
+ const payload = JSON.stringify(body);
21
+ res.writeHead(status, {
22
+ "content-type": "application/json; charset=utf-8",
23
+ "cache-control": "no-store",
24
+ });
25
+ res.end(payload);
26
+ }
27
+ function mapTerminalError(res, error) {
28
+ if (error instanceof TerminalSessionError) {
29
+ const status = error.code === "TERMINAL_NOT_FOUND"
30
+ ? 404
31
+ : error.code === "TERMINAL_EXITED" || error.code === "TERMINAL_SEND_ACTIVE"
32
+ ? 409
33
+ : 500;
34
+ sendJson(res, status, {
35
+ ok: false,
36
+ error: { code: error.code, message: error.message },
37
+ });
38
+ return;
39
+ }
40
+ sendJson(res, 500, {
41
+ ok: false,
42
+ error: {
43
+ code: "TERMINAL_INTERNAL",
44
+ message: error instanceof Error ? error.message : String(error),
45
+ },
46
+ });
47
+ }
48
+ async function readJsonBody(req, maxBytes = 512 * 1024) {
49
+ const chunks = [];
50
+ let total = 0;
51
+ for await (const chunk of req) {
52
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
53
+ total += buf.length;
54
+ if (total > maxBytes)
55
+ throw new Error("request body too large");
56
+ chunks.push(buf);
57
+ }
58
+ if (chunks.length === 0)
59
+ return {};
60
+ const raw = Buffer.concat(chunks).toString("utf8");
61
+ if (!raw.trim())
62
+ return {};
63
+ return JSON.parse(raw);
64
+ }
65
+ /** Route dispatcher; returns true when the request was handled. */
66
+ export async function handleTerminalRequest(req, res, deps, pathname) {
67
+ if (!pathname.startsWith("/api/operator/v1/chat/terminals"))
68
+ return false;
69
+ const method = (req.method ?? "GET").toUpperCase();
70
+ const rest = pathname.slice("/api/operator/v1/chat/terminals".length);
71
+ const parts = rest.split("/").filter(Boolean);
72
+ try {
73
+ // GET /terminals — list (+ available shell kinds for this platform)
74
+ if (method === "GET" && parts.length === 0) {
75
+ sendJson(res, 200, {
76
+ ok: true,
77
+ data: {
78
+ terminals: deps.terminals.list(),
79
+ availableShells: availableShellKinds().kinds,
80
+ },
81
+ });
82
+ return true;
83
+ }
84
+ // POST /terminals — spawn
85
+ if (method === "POST" && parts.length === 0) {
86
+ const gate = deps.gateMutation(req);
87
+ if (!gate.ok) {
88
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
89
+ return true;
90
+ }
91
+ const body = await readJsonBody(req);
92
+ const kinds = availableShellKinds().kinds;
93
+ const shell = kinds.includes(body.shell)
94
+ ? body.shell
95
+ : undefined;
96
+ if (!shell) {
97
+ sendJson(res, 400, {
98
+ ok: false,
99
+ error: {
100
+ code: "INVALID_INPUT",
101
+ message: `shell must be one of: ${kinds.join(", ")} (available on this platform)`,
102
+ },
103
+ });
104
+ return true;
105
+ }
106
+ const info = deps.terminals.spawn({
107
+ ownerSessionId: typeof body.ownerSessionId === "string" && body.ownerSessionId
108
+ ? body.ownerSessionId
109
+ : "console-ui",
110
+ name: typeof body.name === "string" && body.name ? body.name : `${shell}-ui`,
111
+ shell: shell,
112
+ cwd: typeof body.cwd === "string" && body.cwd
113
+ ? body.cwd
114
+ : (deps.defaultCwd ?? undefined),
115
+ cols: typeof body.cols === "number" ? body.cols : undefined,
116
+ rows: typeof body.rows === "number" ? body.rows : undefined,
117
+ });
118
+ sendJson(res, 201, { ok: true, data: info });
119
+ return true;
120
+ }
121
+ const sessionId = parts[0];
122
+ if (!sessionId)
123
+ return false;
124
+ const sub = parts[1] ?? "";
125
+ // GET /terminals/:id — info + buffered output
126
+ if (method === "GET" && parts.length === 1) {
127
+ const info = deps.terminals.get(sessionId);
128
+ const output = deps.terminals.readAll(sessionId);
129
+ sendJson(res, 200, { ok: true, data: { ...info, output } });
130
+ return true;
131
+ }
132
+ // GET /terminals/:id/stream — SSE
133
+ if (method === "GET" && sub === "stream") {
134
+ // Validate existence first (404 before opening the stream).
135
+ const info = deps.terminals.get(sessionId);
136
+ void info;
137
+ res.writeHead(200, {
138
+ "content-type": "text/event-stream; charset=utf-8",
139
+ "cache-control": "no-cache, no-store",
140
+ connection: "keep-alive",
141
+ "x-accel-buffering": "no",
142
+ });
143
+ const writeFrame = (event, data) => {
144
+ if (res.writableEnded)
145
+ return;
146
+ res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
147
+ };
148
+ const buffered = deps.terminals.readAll(sessionId);
149
+ writeFrame("snapshot", {
150
+ truncated: buffered.truncated,
151
+ totalOutputChars: buffered.totalOutputChars,
152
+ text: buffered.text,
153
+ info: deps.terminals.get(sessionId),
154
+ });
155
+ const unsubscribe = deps.terminals.subscribe(sessionId, (chunk) => {
156
+ writeFrame("data", { chunk });
157
+ });
158
+ const heartbeat = setInterval(() => {
159
+ if (!res.writableEnded)
160
+ res.write(": heartbeat\n\n");
161
+ }, 15_000);
162
+ res.once("close", () => {
163
+ clearInterval(heartbeat);
164
+ unsubscribe();
165
+ });
166
+ return true;
167
+ }
168
+ // Everything else mutates → gate first.
169
+ const gate = deps.gateMutation(req);
170
+ if (!gate.ok) {
171
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
172
+ return true;
173
+ }
174
+ if (method === "POST" && sub === "input" && parts.length === 2) {
175
+ const body = await readJsonBody(req);
176
+ if (typeof body.data !== "string") {
177
+ sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: "data (string) is required" } });
178
+ return true;
179
+ }
180
+ deps.terminals.send(sessionId, body.data);
181
+ sendJson(res, 200, { ok: true });
182
+ return true;
183
+ }
184
+ if (method === "POST" && sub === "resize" && parts.length === 2) {
185
+ const body = await readJsonBody(req);
186
+ const cols = Number(body.cols);
187
+ const rows = Number(body.rows);
188
+ if (!Number.isInteger(cols) || !Number.isInteger(rows) || cols < 2 || rows < 2 || cols > 500 || rows > 200) {
189
+ sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: "cols and rows (integers 2..500/200) are required" } });
190
+ return true;
191
+ }
192
+ deps.terminals.resize(sessionId, cols, rows);
193
+ sendJson(res, 200, { ok: true });
194
+ return true;
195
+ }
196
+ if (method === "POST" && sub === "interrupt" && parts.length === 2) {
197
+ deps.terminals.interrupt(sessionId);
198
+ sendJson(res, 200, { ok: true });
199
+ return true;
200
+ }
201
+ if (method === "POST" && sub === "kill" && parts.length === 2) {
202
+ await deps.terminals.kill(sessionId);
203
+ sendJson(res, 200, { ok: true });
204
+ return true;
205
+ }
206
+ sendJson(res, 404, { ok: false, error: { code: "NOT_FOUND", message: `unknown terminal route: ${method} ${pathname}` } });
207
+ return true;
208
+ }
209
+ catch (error) {
210
+ if (!res.headersSent)
211
+ mapTerminalError(res, error);
212
+ else
213
+ res.end();
214
+ return true;
215
+ }
216
+ }