@tea-agent/loop-agent 0.38.0-next.1 → 0.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/commands/init-upgrade.js +97 -19
  4. package/dist/commands/init.js +14 -2
  5. package/dist/executors/dag-pi-executor.js +1 -0
  6. package/dist/governance/manifest-types.js +2 -0
  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 +65 -8
  10. package/dist/task/config-types.js +21 -0
  11. package/dist/task/frontend-project-capability.js +292 -38
  12. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  13. package/dist/worker/console/chat/pi-runtime.js +155 -78
  14. package/dist/worker/console/chat/repo-walk.js +10 -1
  15. package/dist/worker/console/chat/routes.js +19 -8
  16. package/dist/worker/console/chat/semantic-activity.js +17 -5
  17. package/dist/worker/console/chat/turn-process.js +17 -30
  18. package/dist/worker/console/harness-pi-model-matrix.js +146 -0
  19. package/dist/worker/console/operator-actions.js +123 -0
  20. package/dist/worker/console/operator-user-error.js +19 -0
  21. package/dist/worker/console/pi-readiness.js +7 -1
  22. package/dist/worker/console/security.js +20 -1
  23. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-Hfsot6rP.js} +1 -1
  24. package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-YGwEASin.js} +1 -1
  25. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-CmOM0F92.js} +1 -1
  26. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-DDLuaHC5.js} +1 -1
  27. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-B0Um43W4.js} +1 -1
  28. package/dist/worker/console/static/assets/channel-Dp_2Aqf1.js +1 -0
  29. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-DvBN4JZc.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-B1UVgW4E.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-Cf0GgXzX.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-DbNA24sj.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-Bi_0Be06.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-C8pPGvRd.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-BitQY8Z-.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-U67GCiEo.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-IoJwXPBA.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-YDZ_UHgE.js} +1 -1
  39. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL--CODy37j.js +1 -0
  40. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE--CODy37j.js +1 -0
  41. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-CUZgsiRC.js} +1 -1
  42. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-COmCh4pT.js} +1 -1
  43. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-Ta6FSm68.js} +1 -1
  44. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-UdcR-VW9.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-DybcO3Et.js} +1 -1
  46. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-wErDFWLw.js} +1 -1
  47. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-DkVHZWqz.js} +1 -1
  48. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-DdqWs7QN.js} +1 -1
  49. package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-C9dMVmDo.js} +1 -1
  50. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-B2T2WcBK.js} +1 -1
  51. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-Bt9rjDFq.js} +1 -1
  52. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-U8ic_tvi.js} +1 -1
  53. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-BfQJdQoO.js} +1 -1
  54. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-CpMftH4O.js} +1 -1
  55. package/dist/worker/console/static/assets/index-BA5-N5ER.css +1 -0
  56. package/dist/worker/console/static/assets/index-DKPXBrdc.js +325 -0
  57. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-DzqWXNO_.js} +1 -1
  58. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-CJmGQ5Yp.js} +1 -1
  59. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-ChPtKFLz.js} +1 -1
  60. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-PRe7wB5u.js} +1 -1
  61. package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-DpKX6QNp.js} +1 -1
  62. package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-Dnl76b7Y.js} +5 -5
  63. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-S9PCXc3J.js} +1 -1
  64. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-Du8e-FSX.js} +1 -1
  65. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-CY6OmHTc.js} +1 -1
  66. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-C65GPhpj.js} +1 -1
  67. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-U6IUiTCL.js} +1 -1
  68. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-D-I_9Qhi.js} +1 -1
  69. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-B57Fhed0.js} +1 -1
  70. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-CJOtiw21.js} +1 -1
  71. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-FyX8zxSh.js} +1 -1
  72. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-eF-pbs9t.js} +1 -1
  73. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B-mvnLEc.js +1 -0
  74. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-CcfkA-Dq.js} +2 -2
  75. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-JtpBmIv4.js +8 -0
  76. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-DH8FjnOK.js} +1 -1
  77. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-beoTagJg.js} +1 -1
  78. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-CQSfw4cK.js} +1 -1
  79. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-CQgzQ6jn.js} +1 -1
  80. package/dist/worker/console/static/index.html +2 -2
  81. package/dist/worker/console/static-src/app/console-types.js +51 -0
  82. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +44 -21
  83. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
  84. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  85. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +15 -0
  86. package/dist/worker/console/static-src/operator-chat/useChatStream.js +17 -4
  87. package/dist/worker/console/static-src/operator-chat/useChatThread.js +28 -22
  88. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  89. package/dist/workflows/dag/contract-validator-registrations.js +2 -1
  90. package/dist/workflows/dag/failure-routing.js +9 -4
  91. package/dist/workflows/dag/frontend-implementation-contract.js +158 -0
  92. package/dist/workflows/dag/frontend-plan-render.js +90 -0
  93. package/dist/workflows/dag/frontend-prewrite-gate.js +411 -20
  94. package/dist/workflows/dag/init-hybrid.js +272 -27
  95. package/dist/workflows/dag/lifecycle.js +4 -0
  96. package/dist/workflows/dag/node-execution.js +51 -9
  97. package/dist/workflows/dag/report.js +6 -0
  98. package/dist/workflows/dag/retry-policy.js +15 -0
  99. package/dist/workflows/dag/types.js +50 -4
  100. package/dist/workflows/dag/validate.js +7 -2
  101. package/docs/templates/frontend-design-contract.md +40 -14
  102. package/docs/templates/frontend-implementation-contract.schema.json +9 -2
  103. package/docs/templates/frontend-task-constraints.md +40 -13
  104. package/docs/templates/frontend-task-requirement.md +54 -20
  105. package/docs/templates/harness.schema.json +8 -0
  106. package/harness.json +11 -2
  107. package/package.json +1 -1
  108. package/skills/frontend-design-review/references/review-checklist.md +2 -0
  109. package/skills/frontend-implementation/references/design-spec.md +16 -19
  110. package/skills/frontend-implementation/references/node-contracts.md +15 -11
  111. package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
  112. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
  113. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
  114. package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
  115. package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
  116. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
  117. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
@@ -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).
@@ -505,6 +564,29 @@ export function projectRuntimeSnapshot(input) {
505
564
  })),
506
565
  diagnostics: pkg.diagnostics.map((diag) => field(diag, 400) ?? ""),
507
566
  });
567
+ const projectMcpTool = (tool) => ({
568
+ name: field(tool.name, 200) ?? "",
569
+ description: field(tool.description, 600) ?? "",
570
+ ...(tool.server ? { server: field(tool.server, 200) } : {}),
571
+ source: field(tool.source, 200) ?? "",
572
+ scope: tool.scope,
573
+ ...(tool.path ? { path: field(tool.path, 400) } : {}),
574
+ active: tool.active,
575
+ });
576
+ const projectMcp = (mcp) => ({
577
+ servers: mcp.servers.map((server) => ({
578
+ name: field(server.name, 200) ?? "",
579
+ scope: server.scope,
580
+ status: server.status,
581
+ enabled: server.enabled,
582
+ ...(server.transport ? { transport: field(server.transport, 80) } : {}),
583
+ ...(server.command ? { command: field(server.command, 400) } : {}),
584
+ ...(server.url ? { url: field(server.url, 400) } : {}),
585
+ ...(server.configPath ? { configPath: field(server.configPath, 400) } : {}),
586
+ tools: server.tools.map(projectMcpTool),
587
+ })),
588
+ unassignedTools: mcp.unassignedTools.map(projectMcpTool),
589
+ });
508
590
  const projectDiagnostic = (diag) => ({
509
591
  type: diag.type,
510
592
  ...(diag.code ? { code: diag.code } : {}),
@@ -560,6 +642,7 @@ export function projectRuntimeSnapshot(input) {
560
642
  skills: input.skills.map(projectSkill),
561
643
  extensions: input.extensions.map(projectExtension),
562
644
  packages: input.packages.map(projectPackage),
645
+ mcp: projectMcp(input.mcp ?? { servers: [], unassignedTools: [] }),
563
646
  diagnostics: input.diagnostics.map(projectDiagnostic),
564
647
  hasBash: input.hasBash,
565
648
  };
@@ -903,36 +986,16 @@ export class ConsolePiRuntime {
903
986
  const customTools = await this.loadCustomTools();
904
987
  if (this.disposedSessions.has(sessionId))
905
988
  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).
989
+ // pi-web parity: do NOT pass `tools`. A closed allowlist becomes
990
+ // SDK allowedToolNames and filters extension MCP tools out of the
991
+ // registry before Gate 2 can re-add them. Operator/safe customTools
992
+ // still register via customTools; non-Pi write channels stay blocked
993
+ // by excludeTools. After create, bindExtensions emits session_start
994
+ // so MCP adapters can register, then pin the active set.
919
995
  const { session } = await this.bindings.createSessionFromServices({
920
996
  services,
921
997
  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
- ],
998
+ excludeTools: [...OPERATOR_CHAT_SESSION_EXCLUDE_TOOLS],
936
999
  ...(customTools.length > 0 ? { customTools } : {}),
937
1000
  ...(resolvedModel ? { model: resolvedModel } : {}),
938
1001
  });
@@ -945,7 +1008,8 @@ export class ConsolePiRuntime {
945
1008
  }
946
1009
  return;
947
1010
  }
948
- session.setActiveToolsByName(computeOperatorChatActiveToolNames(session));
1011
+ await bindSessionExtensions(session);
1012
+ pinOperatorChatTools(session);
949
1013
  // Register then re-check dispose race: a concurrent dispose must win.
950
1014
  this.sessions.set(sessionId, session);
951
1015
  this.sessionManagers.set(sessionId, init.sessionManager);
@@ -1616,6 +1680,24 @@ export class ConsolePiRuntime {
1616
1680
  retryable: true,
1617
1681
  });
1618
1682
  }
1683
+ const { readMcpInventory } = await import("./mcp-inventory.js");
1684
+ let mcp = { servers: [], unassignedTools: [] };
1685
+ try {
1686
+ mcp = await readMcpInventory({
1687
+ agentDir: await safeGetAgentDir(this.bindings),
1688
+ cwd: this.options.cwd,
1689
+ tools: session.getAllTools?.() ?? [],
1690
+ activeToolNames: session.getActiveToolNames(),
1691
+ });
1692
+ }
1693
+ catch (error) {
1694
+ diagnostics.push({
1695
+ type: "error",
1696
+ code: "MCP_INVENTORY_FAILED",
1697
+ message: error instanceof Error ? error.message : String(error),
1698
+ retryable: true,
1699
+ });
1700
+ }
1619
1701
  return projectRuntimeSnapshot({
1620
1702
  sessionId,
1621
1703
  revision: this.revisions.get(sessionId) ?? 0,
@@ -1651,6 +1733,7 @@ export class ConsolePiRuntime {
1651
1733
  skills,
1652
1734
  extensions,
1653
1735
  packages,
1736
+ mcp,
1654
1737
  diagnostics,
1655
1738
  hasBash: true,
1656
1739
  });
@@ -1833,29 +1916,15 @@ export class ConsolePiRuntime {
1833
1916
  : undefined;
1834
1917
  assertNoWriteToolInList(OPERATOR_CHAT_ALLOWED_TOOLS);
1835
1918
  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
1919
  const { session } = await this.bindings.createSessionFromServices({
1840
1920
  services,
1841
1921
  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
- ],
1922
+ excludeTools: [...OPERATOR_CHAT_SESSION_EXCLUDE_TOOLS],
1856
1923
  ...(customTools.length > 0 ? { customTools } : {}),
1857
1924
  ...(resolvedModel ? { model: resolvedModel } : {}),
1858
1925
  });
1926
+ await bindSessionExtensions(session);
1927
+ pinOperatorChatTools(session);
1859
1928
  return {
1860
1929
  session,
1861
1930
  modelRuntime,
@@ -1950,6 +2019,32 @@ export class ConsolePiRuntime {
1950
2019
  if (options?.signal?.aborted) {
1951
2020
  return;
1952
2021
  }
2022
+ const emitContextUsage = () => {
2023
+ try {
2024
+ const contextUsage = session.getSessionStats?.()?.contextUsage;
2025
+ if (!contextUsage)
2026
+ return;
2027
+ if (contextUsage.contextWindow <= 0 &&
2028
+ contextUsage.percent === null &&
2029
+ contextUsage.tokens === null) {
2030
+ return;
2031
+ }
2032
+ onEvent({
2033
+ type: "usage",
2034
+ sessionId,
2035
+ usage: {
2036
+ inputTokens: 0,
2037
+ outputTokens: 0,
2038
+ totalTokens: 0,
2039
+ contextUsage,
2040
+ },
2041
+ });
2042
+ }
2043
+ catch {
2044
+ // Stats getter is best-effort; never break the event stream.
2045
+ }
2046
+ };
2047
+ emitContextUsage();
1953
2048
  unsub = session.subscribe((event) => {
1954
2049
  const mapped = mapSdkEvent(sessionId, event);
1955
2050
  if (mapped)
@@ -1962,31 +2057,13 @@ export class ConsolePiRuntime {
1962
2057
  const usage = extractUsageSample(event);
1963
2058
  if (usage)
1964
2059
  onEvent({ type: "usage", sessionId, usage });
2060
+ emitContextUsage();
1965
2061
  }
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
- }
2062
+ // Keep the context fill bar live through the turn (pi-web SessionInfoBar).
2063
+ if (event.type === "agent_start" ||
2064
+ event.type === "agent_settled" ||
2065
+ event.type === "tool_execution_end") {
2066
+ emitContextUsage();
1990
2067
  }
1991
2068
  });
1992
2069
  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())
@@ -51,7 +51,7 @@ function turnSummary(turn) {
51
51
  function isTerminalTurnState(state) {
52
52
  return state === "settled" || state === "aborted" || state === "failed";
53
53
  }
54
- const DEFAULT_ABORT_WAIT_MS = 8_000;
54
+ const DEFAULT_ABORT_WAIT_MS = 2_500;
55
55
  /** Parse Chat's `<epoch>:<seq>` SSE cursor. Legacy `<sessionId>:<seq>` has
56
56
  * the same wire shape; its prefix intentionally fails the current process
57
57
  * epoch comparison and triggers safe snapshot recovery (M5.1). */
@@ -1642,16 +1642,27 @@ async function handleChatFiles(res, deps, sessionId, query) {
1642
1642
  if (!(await requireChatSession(res, deps, sessionId)))
1643
1643
  return;
1644
1644
  const q = query.trim().toLowerCase();
1645
- const files = (await walkRepoFiles(deps.appData.repoRoot, {
1645
+ const walked = await walkRepoFiles(deps.appData.repoRoot, {
1646
1646
  skipSensitive: true,
1647
1647
  maxFiles: 5000,
1648
- }))
1649
- .map((entry) => entry.repoRelative)
1650
- .filter((file) => !isSensitivePath(file) && (!q || file.toLowerCase().includes(q)))
1651
- .sort((a, b) => a.localeCompare(b))
1648
+ includeDirs: true,
1649
+ });
1650
+ const entries = walked
1651
+ .filter((entry) => !isSensitivePath(entry.repoRelative) &&
1652
+ (!q || entry.repoRelative.toLowerCase().includes(q)))
1653
+ .sort((a, b) => {
1654
+ const aDir = a.repoRelative.endsWith("/");
1655
+ const bDir = b.repoRelative.endsWith("/");
1656
+ if (aDir !== bDir)
1657
+ return aDir ? -1 : 1;
1658
+ return a.repoRelative.localeCompare(b.repoRelative);
1659
+ })
1652
1660
  .slice(0, 50)
1653
- .map((path) => ({ path }));
1654
- sendJson(res, 200, { ok: true, files });
1661
+ .map((entry) => ({
1662
+ path: entry.repoRelative,
1663
+ isDir: entry.repoRelative.endsWith("/"),
1664
+ }));
1665
+ sendJson(res, 200, { ok: true, files: entries });
1655
1666
  }
1656
1667
  export async function handleChatCommands(res, deps, sessionId) {
1657
1668
  try {
@@ -39,9 +39,11 @@ const OPERATION_TITLES = {
39
39
  dagDoctor: "执行诊断",
40
40
  dagReport: "执行结果",
41
41
  dagRerunPlan: "恢复评估",
42
+ dagRerun: "从节点续跑",
42
43
  dagInterruptEligibility: "中止资格",
43
- standaloneTaskRerun: "恢复执行",
44
- workerTaskRetry: "重试任务",
44
+ standaloneTaskRerun: "完整重跑任务",
45
+ workerTaskRetry: "重试 Worker 任务",
46
+ workerPoolDoctor: "任务池诊断",
45
47
  promoteRun: "任务推进",
46
48
  status: "任务状态",
47
49
  };
@@ -96,10 +98,20 @@ const TOOL_SEMANTICS = {
96
98
  success: "中止资格已经确认",
97
99
  error: "当前运行不能协作中止",
98
100
  },
101
+ dagRerun: {
102
+ running: "正在从失败节点续跑",
103
+ success: "已经从失败节点继续执行",
104
+ error: "从失败节点续跑时遇到问题",
105
+ },
99
106
  standaloneTaskRerun: {
100
- running: "正在从失败位置恢复执行",
101
- success: "已经从失败位置继续执行",
102
- error: "从失败位置恢复执行时遇到问题",
107
+ running: "正在完整重跑整单任务",
108
+ success: "已经启动完整重跑",
109
+ error: "完整重跑任务时遇到问题",
110
+ },
111
+ workerPoolDoctor: {
112
+ running: "正在读取任务池状态",
113
+ success: "任务池状态已经读取",
114
+ error: "读取任务池状态时遇到问题",
103
115
  },
104
116
  promoteRun: {
105
117
  running: "正在推进任务状态",
@@ -92,36 +92,25 @@ function sortToolsStable(tools) {
92
92
  });
93
93
  }
94
94
  /**
95
- * Flush buffered consecutive tools into process items. Semantic operator
96
- * tools stay as individual activity events; plain tools group into a
97
- * `tool-group` ("并行调用 · N 项") when two or more land back to back.
95
+ * Flush buffered consecutive tools into process items (pi-web aligned:
96
+ * strict sequential flow, no synthetic grouping). Semantic operator tools
97
+ * stay as individual activity events; plain tools each render as their own
98
+ * card in stable server order (eventSeq first, createdAt fallback).
98
99
  * Thinking/text/activity boundaries break the buffer upstream.
99
100
  */
100
101
  function flushToolBuffer(processItems, buffer) {
101
102
  if (buffer.length === 0)
102
103
  return;
103
104
  const sorted = sortToolsStable(buffer);
104
- let plainGroup = [];
105
- const flushPlain = () => {
106
- if (plainGroup.length === 1) {
107
- processItems.push({ kind: "tool", tool: plainGroup[0] });
108
- }
109
- else if (plainGroup.length > 1) {
110
- processItems.push({ kind: "tool-group", tools: plainGroup });
111
- }
112
- plainGroup = [];
113
- };
114
105
  for (const tool of sorted) {
115
106
  const activity = semanticActivityForTool(tool);
116
107
  if (activity) {
117
- flushPlain();
118
108
  processItems.push({ kind: "activity", activity });
119
109
  }
120
110
  else {
121
- plainGroup.push(tool);
111
+ processItems.push({ kind: "tool", tool });
122
112
  }
123
113
  }
124
- flushPlain();
125
114
  }
126
115
  function emitProcessBlocks(blocks, group, processItems, usedToolIds) {
127
116
  let buffer = [];
@@ -227,9 +216,6 @@ export function buildTurnProcessGroups(input) {
227
216
  current = startGroup(`assistant-${message.id}`);
228
217
  current.assistants.push(message);
229
218
  }
230
- if (groups.length === 0 && input.toolCalls.length > 0) {
231
- groups.push(emptyGroup("orphan-tools"));
232
- }
233
219
  const assigned = new Set();
234
220
  const assistantToGroup = new Map();
235
221
  for (const group of groups) {
@@ -276,17 +262,12 @@ export function buildTurnProcessGroups(input) {
276
262
  const end = next?.user?.createdAt ?? Number.POSITIVE_INFINITY;
277
263
  return tool.createdAt >= start && tool.createdAt < end;
278
264
  });
265
+ // Time window only. Never dump unmatched tools onto the latest group:
266
+ // that is how a previous turn's bash cards jump under a new prompt.
279
267
  if (windowGroup) {
280
268
  windowGroup.tools.push(tool);
281
269
  assigned.add(tool.id);
282
270
  }
283
- else {
284
- const lastGroup = groups.at(-1);
285
- if (lastGroup) {
286
- lastGroup.tools.push(tool);
287
- assigned.add(tool.id);
288
- }
289
- }
290
271
  }
291
272
  for (const group of groups) {
292
273
  group.tools.sort((a, b) => {
@@ -303,6 +284,14 @@ export function buildTurnProcessGroups(input) {
303
284
  }
304
285
  return groups;
305
286
  }
287
+ /** Keep the most recent complete turns. Slice groups, never raw messages. */
288
+ export function selectRecentTurnGroups(groups, limit) {
289
+ if (limit <= 0)
290
+ return [];
291
+ if (groups.length <= limit)
292
+ return [...groups];
293
+ return groups.slice(-limit);
294
+ }
306
295
  export function processGroupSummary(tools, options) {
307
296
  const count = tools.length;
308
297
  const thinkingCount = options?.thinkingCount ?? 0;
@@ -324,7 +313,7 @@ export function processGroupSummary(tools, options) {
324
313
  parts.push("运行中");
325
314
  return { count, errorCount, runningCount, stepCount, label: parts.join(" · ") };
326
315
  }
327
- /** Summary over rendered process items (tool-group tools count individually). */
316
+ /** Summary over rendered process items. */
328
317
  export function summarizeProcessItems(items) {
329
318
  const tools = [];
330
319
  let thinkingCount = 0;
@@ -344,10 +333,8 @@ export function summarizeProcessItems(items) {
344
333
  else if (item.activity.kind === "progress")
345
334
  activityRunningCount += 1;
346
335
  }
347
- else if (item.kind === "tool")
348
- tools.push(item.tool);
349
336
  else
350
- tools.push(...item.tools);
337
+ tools.push(item.tool);
351
338
  }
352
339
  return processGroupSummary(tools, {
353
340
  thinkingCount,