@tea-agent/loop-agent 0.38.0 → 0.38.2

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 (147) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/dist/application/dag/generate-task-dag.js +9 -0
  3. package/dist/build-stamp.json +3 -3
  4. package/dist/commands/init-upgrade.js +97 -19
  5. package/dist/executors/dag-pi-executor.js +42 -4
  6. package/dist/executors/shell-executor.js +269 -15
  7. package/dist/infrastructure/harness/atomic-write.js +2 -1
  8. package/dist/shared/openspec-spec.js +76 -0
  9. package/dist/shared/operator/capabilities.js +149 -27
  10. package/dist/task/config-types.js +43 -0
  11. package/dist/task/frontend-project-capability.js +292 -38
  12. package/dist/worker/console/chat/deferred-turn.js +91 -0
  13. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  14. package/dist/worker/console/chat/pi-runtime.js +165 -83
  15. package/dist/worker/console/chat/repo-walk.js +10 -1
  16. package/dist/worker/console/chat/routes.js +477 -55
  17. package/dist/worker/console/chat/semantic-activity.js +20 -8
  18. package/dist/worker/console/chat/session-store.js +146 -2
  19. package/dist/worker/console/chat/turn-process.js +17 -30
  20. package/dist/worker/console/harness-pi-model-matrix.js +137 -0
  21. package/dist/worker/console/operator-actions.js +128 -4
  22. package/dist/worker/console/operator-user-error.js +19 -0
  23. package/dist/worker/console/pi-readiness.js +8 -1
  24. package/dist/worker/console/recovery-cta.js +4 -4
  25. package/dist/worker/console/recovery-error-copy.js +4 -4
  26. package/dist/worker/console/security.js +20 -1
  27. package/dist/worker/console/server.js +6 -1
  28. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-C4bmo3iY.js} +1 -1
  29. package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-SAD7McOS.js} +1 -1
  30. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-DAVLUdK1.js} +1 -1
  31. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-CPPdv4vj.js} +1 -1
  32. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-D1GRGZax.js} +1 -1
  33. package/dist/worker/console/static/assets/channel-R6oQIDkw.js +1 -0
  34. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-D_Bq2ZqK.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-CPLVtamK.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-DnaZWcL3.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-BRTmFgnq.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-BRlfYZoH.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-CjNybD4u.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-D3e-74fG.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-Br3ir6f2.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-1NgVuRbh.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-BTYuvuWN.js} +1 -1
  44. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +1 -0
  45. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +1 -0
  46. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-BC6Bgosf.js} +1 -1
  47. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-DV-HypfL.js} +1 -1
  48. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-DhTaddO-.js} +1 -1
  49. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-BnEcOmiI.js} +1 -1
  50. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-xphGRVKp.js} +1 -1
  51. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-CrdD1mRG.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-D4YdLsaF.js} +1 -1
  53. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-LCKyGZ5U.js} +1 -1
  54. package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-BeiTe7t0.js} +1 -1
  55. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-BO2cq8GS.js} +1 -1
  56. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-CmWww3Nt.js} +1 -1
  57. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-C2kkUYg5.js} +1 -1
  58. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-DY_JZ2iQ.js} +1 -1
  59. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-BizhCSw9.js} +1 -1
  60. package/dist/worker/console/static/assets/index-BXN89VPX.js +325 -0
  61. package/dist/worker/console/static/assets/index-CuH5CNFu.css +1 -0
  62. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-Dlo2eK_d.js} +1 -1
  63. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js} +1 -1
  64. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-DiMDe5Yh.js} +1 -1
  65. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-sN553oIN.js} +1 -1
  66. package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-D9Py31Ld.js} +1 -1
  67. package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-D7Occzk_.js} +5 -5
  68. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-DjDRboiR.js} +1 -1
  69. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-Cgc4qlzp.js} +1 -1
  70. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-DxZj8pKd.js} +1 -1
  71. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-CJ50MngR.js} +1 -1
  72. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-BY-OD8Tr.js} +1 -1
  73. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-CQFButfK.js} +1 -1
  74. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-BB_QUK_b.js} +1 -1
  75. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-CqhXZjyx.js} +1 -1
  76. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-2l1U4rgJ.js} +1 -1
  77. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-BX1FbrhJ.js} +1 -1
  78. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +1 -0
  79. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DOSnT7hb.js} +2 -2
  80. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +8 -0
  81. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-DVJQA7AN.js} +1 -1
  82. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-74uxl9gx.js} +1 -1
  83. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-DsBxlcg_.js} +1 -1
  84. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-snuW9Mjw.js} +1 -1
  85. package/dist/worker/console/static/index.html +2 -2
  86. package/dist/worker/console/static-src/app/console-types.js +39 -0
  87. package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
  88. package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
  89. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +82 -22
  90. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
  91. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  92. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
  93. package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
  94. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +18 -0
  95. package/dist/worker/console/static-src/operator-chat/useChatStream.js +216 -10
  96. package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
  97. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  98. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +755 -51
  99. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  100. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  101. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  102. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  103. package/dist/workflows/dag/backend-test-layout.js +133 -0
  104. package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
  105. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  106. package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
  107. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  108. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  109. package/dist/workflows/dag/backend-test-scenario-partitions.js +249 -0
  110. package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
  111. package/dist/workflows/dag/contract-validator-registrations.js +3 -1
  112. package/dist/workflows/dag/convergence/controller.js +141 -0
  113. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  114. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  115. package/dist/workflows/dag/failure-routing.js +9 -4
  116. package/dist/workflows/dag/frontend-implementation-contract.js +386 -0
  117. package/dist/workflows/dag/frontend-plan-render.js +90 -0
  118. package/dist/workflows/dag/frontend-prewrite-gate.js +479 -28
  119. package/dist/workflows/dag/init-hybrid.js +920 -110
  120. package/dist/workflows/dag/lifecycle.js +4 -0
  121. package/dist/workflows/dag/node-execution.js +77 -8
  122. package/dist/workflows/dag/prompt.js +45 -0
  123. package/dist/workflows/dag/report.js +6 -0
  124. package/dist/workflows/dag/rerun-feedback.js +212 -0
  125. package/dist/workflows/dag/rerun-plan.js +93 -27
  126. package/dist/workflows/dag/rerun-run.js +3 -0
  127. package/dist/workflows/dag/rerun-task.js +61 -2
  128. package/dist/workflows/dag/retry-policy.js +33 -0
  129. package/dist/workflows/dag/types.js +148 -4
  130. package/dist/workflows/dag/validate.js +7 -2
  131. package/docs/templates/backend-test-dag.json +60 -35
  132. package/docs/templates/frontend-design-contract.md +40 -14
  133. package/docs/templates/frontend-implementation-contract.schema.json +9 -2
  134. package/docs/templates/frontend-task-constraints.md +40 -13
  135. package/docs/templates/frontend-task-requirement.md +54 -20
  136. package/harness.json +3 -1
  137. package/package.json +1 -1
  138. package/skills/frontend-design-review/references/review-checklist.md +2 -0
  139. package/skills/frontend-implementation/references/design-spec.md +16 -19
  140. package/skills/frontend-implementation/references/node-contracts.md +15 -11
  141. package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
  142. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
  143. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
  144. package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
  145. package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
  146. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
  147. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Read-only MCP inventory for Operator Chat.
3
+ *
4
+ * Pi has no built-in MCP catalog. Console projects two real facts:
5
+ * 1. mcp.json server specs (configured, may or may not have registered tools)
6
+ * 2. tools already on the live session that look like MCP registrations
7
+ *
8
+ * Never fabricate servers or tools. Missing files stay empty.
9
+ */
10
+ import { readFile } from "node:fs/promises";
11
+ import path from "node:path";
12
+ const MCP_CONFIG_NAMES = ["mcp.json"];
13
+ export function isMcpToolName(name) {
14
+ const lower = name.trim().toLowerCase();
15
+ return lower === "mcp" || lower.startsWith("mcp_") || lower.startsWith("mcp-");
16
+ }
17
+ export function inferMcpServerName(toolName, knownServers = []) {
18
+ const name = toolName.trim();
19
+ if (/^mcp$/i.test(name))
20
+ return undefined;
21
+ const rest = name.replace(/^mcp[_-]/i, "");
22
+ if (!rest || rest === name || knownServers.length === 0)
23
+ return undefined;
24
+ const ranked = [...knownServers].sort((left, right) => right.length - left.length);
25
+ const lower = rest.toLowerCase();
26
+ return ranked.find((server) => {
27
+ const key = server.toLowerCase();
28
+ return lower === key || lower.startsWith(`${key}_`) || lower.startsWith(`${key}-`);
29
+ });
30
+ }
31
+ export function looksLikeMcpTool(tool) {
32
+ const name = typeof tool.name === "string" ? tool.name : "";
33
+ if (!name)
34
+ return false;
35
+ if (isMcpToolName(name))
36
+ return true;
37
+ const source = `${tool.sourceInfo?.source ?? ""} ${tool.sourceInfo?.path ?? ""}`.toLowerCase();
38
+ return source.includes("mcp");
39
+ }
40
+ function asRecord(value) {
41
+ return value && typeof value === "object" && !Array.isArray(value)
42
+ ? value
43
+ : undefined;
44
+ }
45
+ function readEnabled(entry) {
46
+ return entry.enabled !== false;
47
+ }
48
+ function readTransport(entry) {
49
+ if (typeof entry.transport === "string" && entry.transport.trim()) {
50
+ return entry.transport.trim();
51
+ }
52
+ if (typeof entry.type === "string" && entry.type.trim()) {
53
+ return entry.type.trim();
54
+ }
55
+ if (typeof entry.url === "string")
56
+ return "http";
57
+ if (Array.isArray(entry.command) || typeof entry.command === "string") {
58
+ return "stdio";
59
+ }
60
+ return undefined;
61
+ }
62
+ function readCommand(entry) {
63
+ if (typeof entry.command === "string" && entry.command.trim()) {
64
+ const args = Array.isArray(entry.args)
65
+ ? entry.args.filter((item) => typeof item === "string").join(" ")
66
+ : "";
67
+ return args ? `${entry.command} ${args}` : entry.command;
68
+ }
69
+ if (Array.isArray(entry.command)) {
70
+ return entry.command
71
+ .filter((item) => typeof item === "string")
72
+ .join(" ")
73
+ .trim();
74
+ }
75
+ return undefined;
76
+ }
77
+ function extractServerMap(parsed) {
78
+ const root = asRecord(parsed);
79
+ if (!root)
80
+ return {};
81
+ const mcpServers = asRecord(root.mcpServers);
82
+ if (mcpServers)
83
+ return mcpServers;
84
+ const mcp = asRecord(root.mcp);
85
+ if (mcp)
86
+ return mcp;
87
+ const servers = asRecord(root.servers);
88
+ if (servers)
89
+ return servers;
90
+ return {};
91
+ }
92
+ export function parseMcpConfig(raw, scope, configPath) {
93
+ let parsed;
94
+ try {
95
+ parsed = JSON.parse(raw);
96
+ }
97
+ catch {
98
+ return [];
99
+ }
100
+ const servers = [];
101
+ for (const [name, value] of Object.entries(extractServerMap(parsed))) {
102
+ if (!name.trim())
103
+ continue;
104
+ const entry = asRecord(value) ?? {};
105
+ const command = readCommand(entry);
106
+ const url = typeof entry.url === "string" ? entry.url.trim() : undefined;
107
+ servers.push({
108
+ name: name.trim(),
109
+ scope,
110
+ enabled: readEnabled(entry),
111
+ ...(readTransport(entry) ? { transport: readTransport(entry) } : {}),
112
+ ...(command ? { command } : {}),
113
+ ...(url ? { url } : {}),
114
+ configPath,
115
+ });
116
+ }
117
+ return servers;
118
+ }
119
+ export function projectSessionMcpTools(tools, activeToolNames = [], knownServers = []) {
120
+ const active = new Set(activeToolNames.map((name) => name.toLowerCase()));
121
+ const seen = new Set();
122
+ const projected = [];
123
+ for (const tool of tools) {
124
+ if (!looksLikeMcpTool(tool) || typeof tool.name !== "string")
125
+ continue;
126
+ const name = tool.name.trim();
127
+ if (!name || seen.has(name))
128
+ continue;
129
+ seen.add(name);
130
+ const server = inferMcpServerName(name, knownServers);
131
+ projected.push({
132
+ name,
133
+ description: typeof tool.description === "string" ? tool.description : "",
134
+ ...(server ? { server } : {}),
135
+ source: tool.sourceInfo?.source ?? "",
136
+ scope: tool.sourceInfo?.scope ?? "session",
137
+ ...(tool.sourceInfo?.path ? { path: tool.sourceInfo.path } : {}),
138
+ active: active.has(name.toLowerCase()),
139
+ });
140
+ }
141
+ return projected;
142
+ }
143
+ export function assembleMcpInventory(servers, tools) {
144
+ const byServer = new Map();
145
+ const unassigned = [];
146
+ for (const tool of tools) {
147
+ const key = tool.server?.toLowerCase();
148
+ if (!key) {
149
+ unassigned.push(tool);
150
+ continue;
151
+ }
152
+ const list = byServer.get(key) ?? [];
153
+ list.push(tool);
154
+ byServer.set(key, list);
155
+ }
156
+ const views = servers.map((server) => {
157
+ const matched = byServer.get(server.name.toLowerCase()) ?? [];
158
+ byServer.delete(server.name.toLowerCase());
159
+ const status = !server.enabled
160
+ ? "disabled"
161
+ : matched.length > 0
162
+ ? "loaded"
163
+ : "configured";
164
+ return {
165
+ name: server.name,
166
+ scope: server.scope,
167
+ status,
168
+ enabled: server.enabled,
169
+ ...(server.transport ? { transport: server.transport } : {}),
170
+ ...(server.command ? { command: server.command } : {}),
171
+ ...(server.url ? { url: server.url } : {}),
172
+ configPath: server.configPath,
173
+ tools: matched,
174
+ };
175
+ });
176
+ for (const [name, matched] of byServer) {
177
+ views.push({
178
+ name,
179
+ scope: "session",
180
+ status: "loaded",
181
+ enabled: true,
182
+ tools: matched,
183
+ });
184
+ }
185
+ return { servers: views, unassignedTools: unassigned };
186
+ }
187
+ async function readConfigFile(filePath) {
188
+ try {
189
+ return await readFile(filePath, "utf8");
190
+ }
191
+ catch {
192
+ return undefined;
193
+ }
194
+ }
195
+ export async function loadMcpServerSpecs(options) {
196
+ const candidates = [
197
+ { path: path.join(options.agentDir, ...MCP_CONFIG_NAMES), scope: "user" },
198
+ { path: path.join(options.cwd, ".pi", ...MCP_CONFIG_NAMES), scope: "project" },
199
+ { path: path.join(options.cwd, ".mcp.json"), scope: "project" },
200
+ ];
201
+ const servers = [];
202
+ const seen = new Set();
203
+ for (const candidate of candidates) {
204
+ const raw = await readConfigFile(candidate.path);
205
+ if (!raw)
206
+ continue;
207
+ for (const server of parseMcpConfig(raw, candidate.scope, candidate.path)) {
208
+ const key = `${server.scope}:${server.name.toLowerCase()}`;
209
+ if (seen.has(key))
210
+ continue;
211
+ seen.add(key);
212
+ servers.push(server);
213
+ }
214
+ }
215
+ return servers;
216
+ }
217
+ export async function readMcpInventory(options) {
218
+ const servers = await loadMcpServerSpecs(options);
219
+ const tools = projectSessionMcpTools(options.tools ?? [], options.activeToolNames ?? [], servers.map((server) => server.name));
220
+ return assembleMcpInventory(servers, tools);
221
+ }
@@ -45,24 +45,26 @@ import { RUNTIME_CONTEXT_TEXT_MAX, redactRuntimeText, } from "./runtime-context.
45
45
  * Project governance (operator-only policy, allowedPaths/forbiddenPaths,
46
46
  * writer writeSet, taskKind routing, verification matrix, DAG recovery
47
47
  * commands, closeout) lives in the loaded target-project AGENTS.md. The
48
- * prompt below covers: Operator identity, the real tool surface (operator_*
49
- * first + Pi tools, obey AGENTS.md), denied non-Pi runtimes, safe-* probes,
50
- * init/init-upgrade fallback for uninitialized repos, DAG supervision with
51
- * 15/30/60s adaptive polling (operationGet/status/dagReport/dagDoctor), R2
52
- * recovery (fresh eligible dagRerunPlan → dagRerun; ineligible → same-task
53
- * rerun/advance, resume, Worker retry) and G2 bounded writeSet auto-approval.
48
+ * prompt below covers: Operator identity, the real tool surface (typed
49
+ * operator tools first + Pi tools, obey AGENTS.md), denied non-Pi runtimes,
50
+ * safe-* probes, init/init-upgrade fallback for uninitialized repos, DAG
51
+ * supervision with 60/90/120/180s adaptive polling
52
+ * (operationGet/status/dagReport/dagDoctor/operationWait), R2 recovery
53
+ * (dagReport dagRerunPlan dagRerun; ineligible standaloneTaskRerun or
54
+ * workerTaskRetry, never dagResume) and G2 bounded writeSet auto-approval.
54
55
  *
55
56
  * The old Human Gate tutorial paragraphs (prepare + browser Human Gate,
56
57
  * "confirmDagConfirmation is a human-only action", the long "Recovery default
57
58
  * order" table) are REMOVED — bounded DAGs start and recover autonomously
58
- * (AC-01: ≤ 1900 chars and 80% of the 2498-char baseline).
59
+ * (AC-01: ≤ 2500 chars; 2498-slimming ratio retired 2026-08-18).
59
60
  */
60
61
  export const OPERATOR_CHAT_SYSTEM_PROMPT_BASE = [
61
62
  "You are General Operator Chat.",
62
- "Use operator_* tools first. 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 sensitive probes.",
63
+ "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.",
63
64
  "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.",
64
65
  "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 with 60s → 90s → 120s → 180s backoff; reset to 60s on state change; 30-60s re-checks when stall suspected. Every operationWait continuation MUST use afterSeq=previous nextSeq (prefer recommendedNextCall); never reuse an older cursor. Heartbeats are not meaningful progress.",
65
- "On failure consume terminal runSummary first. If progress exists, wait; else use a fresh dagRerunPlan; when ineligible use same-task rerun/advance, resume, or Worker retry. A run-failed task without an open gate is terminal: do not re-advance a run-failed task. Before run start, two identical lifecycleState/blocker/revision/DAG-hash observations mean no progress: stop and report after one bounded probe; never loop identical bash/read/grep. Auto-approve only bounded writeSet inside allowedPaths, outside forbiddenPaths, with structured verification.",
66
+ "On failure consume terminal runSummary first. If progress exists, wait; else dagReport then dagRerunPlan(fromNode=primaryFailure.nodeId); eligible dagRerun (node rerun only: reruns the failed subgraph, keeps prior successes). If plan ineligible (unsafe-shell subgraph, binding/workspace drift): Worker Failed → workerTaskRetry; standalone → standaloneTaskRerun after contract adopt (regenerates the whole task, not node continue). Do not call dagResume. A run-failed task without an open gate is terminal: do not re-advance a run-failed task. Before run start, two identical lifecycleState/blocker/revision/DAG-hash observations mean no progress: stop and report after one bounded probe; never loop identical bash/read/grep. Auto-approve only bounded writeSet inside allowedPaths, outside forbiddenPaths, with structured verification.",
67
+ "Iterate toward the goal in bounded rounds: first summarize the previous round's error in one line (wrong fix, wrong node, or environment), fix its root cause — not the symptom — then rerun the failed node with a fresh plan. Never resubmit an unchanged fix; after two rounds failing the same way, stop and report evidence and remaining routes.",
66
68
  "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.",
67
69
  ].join("\n");
68
70
  /** Compose the inspectable system prompt actually injected into Operator Chat. */
@@ -151,6 +153,63 @@ export function computeOperatorChatActiveToolNames(session) {
151
153
  }
152
154
  return extras.length > 0 ? [...baseline, ...extras] : baseline;
153
155
  }
156
+ const OPERATOR_CHAT_SESSION_EXCLUDE_TOOLS = Object.freeze([
157
+ "apply_patch",
158
+ "apply-patch",
159
+ "full-tools",
160
+ "full_tools",
161
+ "coding-chat",
162
+ "coding_chat",
163
+ "shell",
164
+ ]);
165
+ function createConsoleExtensionBindings() {
166
+ return {
167
+ mode: "rpc",
168
+ onError: (error) => {
169
+ const message = error instanceof Error
170
+ ? error.message
171
+ : typeof error === "object" &&
172
+ error &&
173
+ "error" in error &&
174
+ typeof error.error === "string"
175
+ ? String(error.error)
176
+ : String(error);
177
+ process.stderr.write(`[console] extension: ${message}\n`);
178
+ },
179
+ uiContext: {
180
+ select: async () => undefined,
181
+ confirm: async () => false,
182
+ input: async () => undefined,
183
+ editor: async () => undefined,
184
+ notify: () => undefined,
185
+ onTerminalInput: () => () => undefined,
186
+ setStatus: () => undefined,
187
+ setWorkingMessage: () => undefined,
188
+ setWorkingVisible: () => undefined,
189
+ setWorkingIndicator: () => undefined,
190
+ setHiddenThinkingLabel: () => undefined,
191
+ setWidget: () => undefined,
192
+ setFooter: () => undefined,
193
+ setHeader: () => undefined,
194
+ setTitle: () => undefined,
195
+ custom: async () => undefined,
196
+ pasteToEditor: () => undefined,
197
+ setEditorText: () => undefined,
198
+ getEditorText: () => "",
199
+ addAutocompleteProvider: () => undefined,
200
+ setEditorComponent: () => undefined,
201
+ getEditorComponent: () => undefined,
202
+ },
203
+ };
204
+ }
205
+ async function bindSessionExtensions(session) {
206
+ if (typeof session.bindExtensions !== "function")
207
+ return;
208
+ await session.bindExtensions(createConsoleExtensionBindings());
209
+ }
210
+ function pinOperatorChatTools(session) {
211
+ session.setActiveToolsByName(computeOperatorChatActiveToolNames(session));
212
+ }
154
213
  /**
155
214
  * Build Pi custom ToolDefinition objects for every whitelisted operator action.
156
215
  * Each tool's execute() routes into the operator action dispatcher (Gate 3).
@@ -169,19 +228,24 @@ async function buildCustomOperatorTools(options) {
169
228
  // TypeBox schema is built dynamically; cast through unknown to the builder
170
229
  // since Static<TParams> is structural and cannot be statically inferred here.
171
230
  const parameters = Type.Object(schema.inputParams.reduce((acc, param) => {
231
+ // Carry the registry's param description into the tool schema so
232
+ // the model sees parameter semantics, not just names/types.
233
+ const desc = param.description?.trim()
234
+ ? { description: param.description }
235
+ : {};
172
236
  let t;
173
237
  switch (param.type) {
174
238
  case "string":
175
- t = Type.String();
239
+ t = Type.String(desc);
176
240
  break;
177
241
  case "boolean":
178
- t = Type.Boolean();
242
+ t = Type.Boolean(desc);
179
243
  break;
180
244
  case "number":
181
- t = Type.Number();
245
+ t = Type.Number(desc);
182
246
  break;
183
247
  case "array":
184
- t = Type.Array(Type.String());
248
+ t = Type.Array(Type.String({}), Object.keys(desc).length ? desc : {});
185
249
  break;
186
250
  default:
187
251
  t = Type.Object({}, { additionalProperties: true });
@@ -193,7 +257,7 @@ async function buildCustomOperatorTools(options) {
193
257
  name: schema.toolId,
194
258
  label: schema.toolId,
195
259
  description: `${schema.description} (kind=${schema.kind}; cli: ${schema.cli})`,
196
- promptSnippet: `${schema.toolId}: ${schema.description}`,
260
+ promptSnippet: schema.description,
197
261
  parameters: parameters,
198
262
  async execute(toolCallId, params) {
199
263
  const decision = authorizeOperatorChatTool(schema.toolId);
@@ -505,6 +569,29 @@ export function projectRuntimeSnapshot(input) {
505
569
  })),
506
570
  diagnostics: pkg.diagnostics.map((diag) => field(diag, 400) ?? ""),
507
571
  });
572
+ const projectMcpTool = (tool) => ({
573
+ name: field(tool.name, 200) ?? "",
574
+ description: field(tool.description, 600) ?? "",
575
+ ...(tool.server ? { server: field(tool.server, 200) } : {}),
576
+ source: field(tool.source, 200) ?? "",
577
+ scope: tool.scope,
578
+ ...(tool.path ? { path: field(tool.path, 400) } : {}),
579
+ active: tool.active,
580
+ });
581
+ const projectMcp = (mcp) => ({
582
+ servers: mcp.servers.map((server) => ({
583
+ name: field(server.name, 200) ?? "",
584
+ scope: server.scope,
585
+ status: server.status,
586
+ enabled: server.enabled,
587
+ ...(server.transport ? { transport: field(server.transport, 80) } : {}),
588
+ ...(server.command ? { command: field(server.command, 400) } : {}),
589
+ ...(server.url ? { url: field(server.url, 400) } : {}),
590
+ ...(server.configPath ? { configPath: field(server.configPath, 400) } : {}),
591
+ tools: server.tools.map(projectMcpTool),
592
+ })),
593
+ unassignedTools: mcp.unassignedTools.map(projectMcpTool),
594
+ });
508
595
  const projectDiagnostic = (diag) => ({
509
596
  type: diag.type,
510
597
  ...(diag.code ? { code: diag.code } : {}),
@@ -560,6 +647,7 @@ export function projectRuntimeSnapshot(input) {
560
647
  skills: input.skills.map(projectSkill),
561
648
  extensions: input.extensions.map(projectExtension),
562
649
  packages: input.packages.map(projectPackage),
650
+ mcp: projectMcp(input.mcp ?? { servers: [], unassignedTools: [] }),
563
651
  diagnostics: input.diagnostics.map(projectDiagnostic),
564
652
  hasBash: input.hasBash,
565
653
  };
@@ -903,36 +991,16 @@ export class ConsolePiRuntime {
903
991
  const customTools = await this.loadCustomTools();
904
992
  if (this.disposedSessions.has(sessionId))
905
993
  return;
906
- // The SDK filters BOTH built-in tools and customTools against
907
- // `tools` (allowed-tool names) inside _refreshToolRegistry:
908
- // isAllowedTool = (!allowedToolNames || allowedToolNames.has(name))
909
- // && !excludedToolNames.has(name)
910
- // Passing only OPERATOR_CHAT_BUILTIN_EXPLORE_TOOLS here silently dropped
911
- // every custom operator tool (prepareDagExecution / runDag /
912
- // operationWait / safe-read / git-status / ...) from the registry, so the
913
- // model could only ever see bash/read/write/edit/grep/find/ls. The
914
- // allowlist must cover the full tool surface that is later activated via
915
- // setActiveToolsByName(computeOperatorChatActiveToolNames(session)) —
916
- // operator actions + built-in explore tools + safe explore tools.
917
- // Hard write/coding channels stay blocked via excludeTools below (they
918
- // are removed even when present in the allowed list).
994
+ // pi-web parity: do NOT pass `tools`. A closed allowlist becomes
995
+ // SDK allowedToolNames and filters extension MCP tools out of the
996
+ // registry before Gate 2 can re-add them. Operator/safe customTools
997
+ // still register via customTools; non-Pi write channels stay blocked
998
+ // by excludeTools. After create, bindExtensions emits session_start
999
+ // so MCP adapters can register, then pin the active set.
919
1000
  const { session } = await this.bindings.createSessionFromServices({
920
1001
  services,
921
1002
  sessionManager: init.sessionManager,
922
- tools: [
923
- ...OPERATOR_CHAT_BUILTIN_EXPLORE_TOOLS,
924
- ...OPERATOR_CHAT_ALLOWED_TOOLS,
925
- ...OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS,
926
- ],
927
- excludeTools: [
928
- "apply_patch",
929
- "apply-patch",
930
- "full-tools",
931
- "full_tools",
932
- "coding-chat",
933
- "coding_chat",
934
- "shell",
935
- ],
1003
+ excludeTools: [...OPERATOR_CHAT_SESSION_EXCLUDE_TOOLS],
936
1004
  ...(customTools.length > 0 ? { customTools } : {}),
937
1005
  ...(resolvedModel ? { model: resolvedModel } : {}),
938
1006
  });
@@ -945,7 +1013,8 @@ export class ConsolePiRuntime {
945
1013
  }
946
1014
  return;
947
1015
  }
948
- session.setActiveToolsByName(computeOperatorChatActiveToolNames(session));
1016
+ await bindSessionExtensions(session);
1017
+ pinOperatorChatTools(session);
949
1018
  // Register then re-check dispose race: a concurrent dispose must win.
950
1019
  this.sessions.set(sessionId, session);
951
1020
  this.sessionManagers.set(sessionId, init.sessionManager);
@@ -1616,6 +1685,24 @@ export class ConsolePiRuntime {
1616
1685
  retryable: true,
1617
1686
  });
1618
1687
  }
1688
+ const { readMcpInventory } = await import("./mcp-inventory.js");
1689
+ let mcp = { servers: [], unassignedTools: [] };
1690
+ try {
1691
+ mcp = await readMcpInventory({
1692
+ agentDir: await safeGetAgentDir(this.bindings),
1693
+ cwd: this.options.cwd,
1694
+ tools: session.getAllTools?.() ?? [],
1695
+ activeToolNames: session.getActiveToolNames(),
1696
+ });
1697
+ }
1698
+ catch (error) {
1699
+ diagnostics.push({
1700
+ type: "error",
1701
+ code: "MCP_INVENTORY_FAILED",
1702
+ message: error instanceof Error ? error.message : String(error),
1703
+ retryable: true,
1704
+ });
1705
+ }
1619
1706
  return projectRuntimeSnapshot({
1620
1707
  sessionId,
1621
1708
  revision: this.revisions.get(sessionId) ?? 0,
@@ -1651,6 +1738,7 @@ export class ConsolePiRuntime {
1651
1738
  skills,
1652
1739
  extensions,
1653
1740
  packages,
1741
+ mcp,
1654
1742
  diagnostics,
1655
1743
  hasBash: true,
1656
1744
  });
@@ -1833,29 +1921,15 @@ export class ConsolePiRuntime {
1833
1921
  : undefined;
1834
1922
  assertNoWriteToolInList(OPERATOR_CHAT_ALLOWED_TOOLS);
1835
1923
  const customTools = await this.loadCustomTools();
1836
- // Same allowlist rationale as materializeSession: the SDK filters
1837
- // customTools against `tools`, so the allowed names must cover the full
1838
- // operator + explore surface or every custom tool is silently dropped.
1839
1924
  const { session } = await this.bindings.createSessionFromServices({
1840
1925
  services,
1841
1926
  sessionManager: init.sessionManager,
1842
- tools: [
1843
- ...OPERATOR_CHAT_BUILTIN_EXPLORE_TOOLS,
1844
- ...OPERATOR_CHAT_ALLOWED_TOOLS,
1845
- ...OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS,
1846
- ],
1847
- excludeTools: [
1848
- "apply_patch",
1849
- "apply-patch",
1850
- "full-tools",
1851
- "full_tools",
1852
- "coding-chat",
1853
- "coding_chat",
1854
- "shell",
1855
- ],
1927
+ excludeTools: [...OPERATOR_CHAT_SESSION_EXCLUDE_TOOLS],
1856
1928
  ...(customTools.length > 0 ? { customTools } : {}),
1857
1929
  ...(resolvedModel ? { model: resolvedModel } : {}),
1858
1930
  });
1931
+ await bindSessionExtensions(session);
1932
+ pinOperatorChatTools(session);
1859
1933
  return {
1860
1934
  session,
1861
1935
  modelRuntime,
@@ -1950,6 +2024,32 @@ export class ConsolePiRuntime {
1950
2024
  if (options?.signal?.aborted) {
1951
2025
  return;
1952
2026
  }
2027
+ const emitContextUsage = () => {
2028
+ try {
2029
+ const contextUsage = session.getSessionStats?.()?.contextUsage;
2030
+ if (!contextUsage)
2031
+ return;
2032
+ if (contextUsage.contextWindow <= 0 &&
2033
+ contextUsage.percent === null &&
2034
+ contextUsage.tokens === null) {
2035
+ return;
2036
+ }
2037
+ onEvent({
2038
+ type: "usage",
2039
+ sessionId,
2040
+ usage: {
2041
+ inputTokens: 0,
2042
+ outputTokens: 0,
2043
+ totalTokens: 0,
2044
+ contextUsage,
2045
+ },
2046
+ });
2047
+ }
2048
+ catch {
2049
+ // Stats getter is best-effort; never break the event stream.
2050
+ }
2051
+ };
2052
+ emitContextUsage();
1953
2053
  unsub = session.subscribe((event) => {
1954
2054
  const mapped = mapSdkEvent(sessionId, event);
1955
2055
  if (mapped)
@@ -1962,31 +2062,13 @@ export class ConsolePiRuntime {
1962
2062
  const usage = extractUsageSample(event);
1963
2063
  if (usage)
1964
2064
  onEvent({ type: "usage", sessionId, usage });
2065
+ emitContextUsage();
1965
2066
  }
1966
- // Context usage rides the usage event: refresh on agent_settled so the
1967
- // bar reflects the post-turn context window fill (pi-web parity).
1968
- if (event.type === "agent_settled") {
1969
- try {
1970
- const stats = session.getSessionStats?.();
1971
- const contextUsage = stats?.contextUsage;
1972
- if (contextUsage) {
1973
- onEvent({
1974
- type: "usage",
1975
- sessionId,
1976
- usage: {
1977
- inputTokens: 0,
1978
- outputTokens: 0,
1979
- totalTokens: 0,
1980
- ...(contextUsage.tokens === null
1981
- ? {}
1982
- : { contextUsage }),
1983
- },
1984
- });
1985
- }
1986
- }
1987
- catch {
1988
- // Stats getter is best-effort; never break the event stream.
1989
- }
2067
+ // Keep the context fill bar live through the turn (pi-web SessionInfoBar).
2068
+ if (event.type === "agent_start" ||
2069
+ event.type === "agent_settled" ||
2070
+ event.type === "tool_execution_end") {
2071
+ emitContextUsage();
1990
2072
  }
1991
2073
  });
1992
2074
  options?.signal?.addEventListener("abort", onAbort);
@@ -77,6 +77,7 @@ function isBinaryExt(p) {
77
77
  export async function walkRepoFiles(repoRoot, options) {
78
78
  const skipSensitive = options?.skipSensitive ?? true;
79
79
  const maxFiles = options?.maxFiles ?? 5000;
80
+ const includeDirs = options?.includeDirs ?? false;
80
81
  const filter = options?.glob ? globToRe(options.glob) : undefined;
81
82
  const out = [];
82
83
  async function walk(dir, prefix) {
@@ -96,7 +97,15 @@ export async function walkRepoFiles(repoRoot, options) {
96
97
  if (entry.isDirectory()) {
97
98
  if (SKIP_DIRS.has(name))
98
99
  continue;
99
- await walk(path.join(dir, name), `${prefix}${name}/`);
100
+ const dirRelative = `${prefix}${name}/`;
101
+ if (includeDirs) {
102
+ const dirEntry = {
103
+ abs: path.join(dir, name),
104
+ repoRelative: dirRelative,
105
+ };
106
+ out.push(dirEntry);
107
+ }
108
+ await walk(path.join(dir, name), dirRelative);
100
109
  continue;
101
110
  }
102
111
  if (!entry.isFile())