@tea-agent/loop-agent 0.42.0-next.9 → 0.43.0-next.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 (157) hide show
  1. package/CHANGELOG.md +65 -59
  2. package/dist/application/dag/run-dag.js +3 -2
  3. package/dist/application/task-lifecycle/advance.js +9 -4
  4. package/dist/build-stamp.json +3 -3
  5. package/dist/cli/program.js +1 -1
  6. package/dist/commands/dag-rerun-task.js +2 -0
  7. package/dist/commands/task-source-prepare.js +3 -1
  8. package/dist/executors/dag-pi-executor.js +1514 -175
  9. package/dist/executors/pi-extension-resolver.js +14 -2
  10. package/dist/executors/shell-executor.js +131 -54
  11. package/dist/shared/dag-failure-category.js +6 -0
  12. package/dist/task/contract/apply.js +36 -2
  13. package/dist/task/source-prepare/parse-intent.js +7 -0
  14. package/dist/worker/console/chat/chat-event-store.js +4 -2
  15. package/dist/worker/console/chat/pi-runtime.js +45 -2
  16. package/dist/worker/console/chat/resource-loader.js +4 -1
  17. package/dist/worker/console/chat/routes.js +28 -12
  18. package/dist/worker/console/chat/sdd-data-alignment.js +13 -2
  19. package/dist/worker/console/chat/session-store.js +5 -1
  20. package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
  21. package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
  22. package/dist/worker/console/chat/subagents/index.js +5 -0
  23. package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
  24. package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
  25. package/dist/worker/console/chat/subagents/types.js +13 -0
  26. package/dist/worker/console/chat/tool-preview.js +10 -0
  27. package/dist/worker/console/chat/tools.js +11 -1
  28. package/dist/worker/console/chat/turn-process.js +1 -0
  29. package/dist/worker/console/interview/tools.js +1 -0
  30. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C6n9_m0P.js → abnfDiagram-N423BO3Z-B0Q0nClf.js} +1 -1
  31. package/dist/worker/console/static/assets/{arc-DQh-IfZ1.js → arc-DCPjC19G.js} +1 -1
  32. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-54NnrwUC.js → architectureDiagram-T3A2C74G-CP5n9jmG.js} +1 -1
  33. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-pivRALGK.js → blockDiagram-VBNYF7ZC-CmaWi_Wg.js} +1 -1
  34. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BR7OV2NJ.js → c4Diagram-5PPSVZJV-Ct5tcMfZ.js} +1 -1
  35. package/dist/worker/console/static/assets/channel-DAS07MdS.js +1 -0
  36. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B1Aq6BcK.js → chunk-2GRJ4B5K-C0osm1Zf.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CbdU0rjo.js → chunk-2Q5K7J3B-CDWviED7.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Dqi2GJeD.js → chunk-5RXB4S5H-mtSk1-j7.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BYn1Hu4R.js → chunk-5VM5RSS4-D5J9PHC7.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DRLjDL0k.js → chunk-6Q2QTUOP-Begg4WAa.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CY9Xx2jV.js → chunk-GF5L2VYU-sF1AEy7T.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BNCZs7_z.js → chunk-JWPE2WC7-BqbsWXZb.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DZ8AStLL.js → chunk-KBJHAD2P-BKJrPcJ6.js} +1 -1
  44. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DsxjYrzz.js → chunk-RYQCIY6F-CcwNMRho.js} +1 -1
  45. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Ddx1KC1l.js → chunk-XXDRQBXY-BEJi3iIs.js} +1 -1
  46. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cwk-5jiW.js +1 -0
  47. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cwk-5jiW.js +1 -0
  48. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-W9TveCnK.js → cose-bilkent-JH36ORCC-DRLgTvVu.js} +1 -1
  49. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-l9j_ztZH.js → cynefin-VYW2F7L2-BoAVG3cs.js} +1 -1
  50. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BMJMKi4G.js → cynefinDiagram-MW4NZA55-Bzol06hR.js} +1 -1
  51. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Bb4mG9pH.js → dagre-VZM6K2ZE-UmliJM77.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BMgL_Qv0.js → diagram-7IWD3JNH-BWLcJkFo.js} +1 -1
  53. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bvo2T4OQ.js → diagram-B4RE2ZJO-CgApOm-O.js} +1 -1
  54. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-_5kWRN9c.js → diagram-LBJQPF4R-Bh3RgTLs.js} +1 -1
  55. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DqdMrltM.js → diagram-Q27KOJAE-Dsh-D5nE.js} +1 -1
  56. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CDDYsNkp.js → diagram-UB23O5K3-Dkbbpcpb.js} +1 -1
  57. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-c4nfnZUV.js → ebnfDiagram-BXEA7PRR-3EBaA3t3.js} +1 -1
  58. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DnkUcNMs.js → erDiagram-JOGREHBK-Bq2Dc5ok.js} +1 -1
  59. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Dz0KGLZE.js → flowDiagram-UKHOOZJN-De7y55ha.js} +1 -1
  60. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cj1t1uka.js → ganttDiagram-PKOTCBZU-fLiDbQRh.js} +1 -1
  61. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-tp2FrBHd.js → gitGraphDiagram-DS77QQ5N-Du617mp1.js} +1 -1
  62. package/dist/worker/console/static/assets/index-C0O48S_P.js +449 -0
  63. package/dist/worker/console/static/assets/index-CzKf4U8P.css +1 -0
  64. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DQwJS8DH.js → infoDiagram-6WML65LV-BwmlF-XP.js} +1 -1
  65. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-eimCQWD4.js → ishikawaDiagram-WSZJBQD7-zsGtS59u.js} +1 -1
  66. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Dd9Gbwgv.js → journeyDiagram-NVQOT4AX-DTTPTe3f.js} +1 -1
  67. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Qve3cyft.js → kanban-definition-27J2QSJJ-BkXg9aP-.js} +1 -1
  68. package/dist/worker/console/static/assets/{linear-UXKSe36Z.js → linear-7U2ue5IE.js} +1 -1
  69. package/dist/worker/console/static/assets/{mermaid.core-CWhj4JXN.js → mermaid.core-BUuGHmWO.js} +5 -5
  70. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-yTcwf4SJ.js → mindmap-definition-FAOFIHXS-7q6fX0Sv.js} +1 -1
  71. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-_7qToaZP.js → pegDiagram-VL7TDLO6-CkQPJN55.js} +1 -1
  72. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CXmrKmDm.js → pieDiagram-7S7Q4E2Y-CCal_tfX.js} +1 -1
  73. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-hMmrJyaS.js → quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js} +1 -1
  74. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BXw8tCBe.js → railroadDiagram-AXF67PYL-CJmww7D-.js} +1 -1
  75. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BmjgnLZl.js → requirementDiagram-LRYGKXZP-D6ldWJEv.js} +1 -1
  76. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bj77SPpN.js → sankeyDiagram-W5VNT64P-VGh33I9e.js} +1 -1
  77. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-B2FDVysL.js → sequenceDiagram-SI44F4Z6-CCkDrIjU.js} +1 -1
  78. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-UChNY9ZZ.js → sizeCapture-X5ZJPWSS-NC8f5Otb.js} +1 -1
  79. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-wbAEkbd7.js → stateDiagram-OKZ733FA-_92ezZdF.js} +1 -1
  80. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyJg5gzW.js +1 -0
  81. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DCEkU8HR.js → swimlanes-SLNWSIFB-BdUCGtbP.js} +2 -2
  82. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B1cw_0Uy.js +8 -0
  83. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BGp7H06U.js → timeline-definition-Z64GVDOM-CdD1H8ia.js} +1 -1
  84. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DsCIQuWR.js → vennDiagram-T6HMQDX7-Dkdk3oFo.js} +1 -1
  85. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-cRZLP4Xe.js → wardleyDiagram-T6FBY63Y-CKu2uPPY.js} +1 -1
  86. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CP0xLR9Y.js → xychartDiagram-ELKLHX3M-ClqAnuG8.js} +1 -1
  87. package/dist/worker/console/static/index.html +2 -2
  88. package/dist/worker/console/static-src/chat-view-types.js +1 -1
  89. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +23 -0
  90. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +32 -0
  91. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
  92. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +2 -0
  93. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
  94. package/dist/worker/console/static-src/operator-chat/useChatStream.js +30 -14
  95. package/dist/worker/console/static-src/operator-chat/useChatThread.js +133 -5
  96. package/dist/worker/console/static-src/shell/workspace-route.js +4 -0
  97. package/dist/worker/observe/static/constants.js +22 -22
  98. package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
  99. package/dist/worker/observe/static/dag-history-labels.js +1 -0
  100. package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
  101. package/dist/worker/observe/static/dag-inspector-humanize.js +339 -0
  102. package/dist/worker/observe/static/dag-node-purpose.js +10 -10
  103. package/dist/worker/observe/static/index.html +4 -4
  104. package/dist/worker/observe/static/prompt-restart-candidates.js +4 -2
  105. package/dist/worker/observe/static/relations.js +5 -5
  106. package/dist/worker/observe/static/styles.css +171 -0
  107. package/dist/worker/observe/static/views/dag-graph.js +1 -1
  108. package/dist/worker/observe/static/views/dag-inspector.js +329 -174
  109. package/dist/worker/observe/static/views/dag.js +16 -12
  110. package/dist/worker/observe/static/views/session-timeline.js +5 -3
  111. package/dist/workflows/dag/backend-test-plan-protocol.js +104 -0
  112. package/dist/workflows/dag/dag-retry-schema.js +3 -0
  113. package/dist/workflows/dag/frontend-contract-facts.js +130 -0
  114. package/dist/workflows/dag/frontend-design-policy.js +101 -16
  115. package/dist/workflows/dag/frontend-implementation-contract.js +410 -41
  116. package/dist/workflows/dag/frontend-plan-render.js +13 -2
  117. package/dist/workflows/dag/frontend-prewrite-gate.js +1 -1
  118. package/dist/workflows/dag/frontend-recovery-controller.js +25 -1
  119. package/dist/workflows/dag/frontend-recovery-plan.js +4 -1
  120. package/dist/workflows/dag/frontend-recovery-run.js +142 -16
  121. package/dist/workflows/dag/frontend-risk.js +2 -0
  122. package/dist/workflows/dag/frontend-shadow-dual-write.js +17 -1
  123. package/dist/workflows/dag/frontend-shape.js +16 -6
  124. package/dist/workflows/dag/frontend-test-execution-evidence.js +48 -0
  125. package/dist/workflows/dag/frontend-typed-event-store.js +3 -0
  126. package/dist/workflows/dag/frontend-verification-trace.js +38 -4
  127. package/dist/workflows/dag/frontend-writer-admission.js +2 -2
  128. package/dist/workflows/dag/init-hybrid.js +57 -28
  129. package/dist/workflows/dag/node-execution.js +201 -85
  130. package/dist/workflows/dag/recovery-lease.js +106 -16
  131. package/dist/workflows/dag/rerun-feedback.js +1 -0
  132. package/dist/workflows/dag/rerun-plan.js +90 -4
  133. package/dist/workflows/dag/rerun-run.js +28 -6
  134. package/dist/workflows/dag/rerun-task.js +221 -18
  135. package/dist/workflows/dag/retry-policy.js +27 -10
  136. package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
  137. package/dist/workflows/dag/runner.js +220 -18
  138. package/dist/workflows/dag/structured-output-repair.js +4 -1
  139. package/dist/workflows/dag/types.js +4 -3
  140. package/dist/workflows/dag/validate.js +10 -8
  141. package/dist/workflows/dag/workspace-checkpoint.js +66 -0
  142. package/docs/operations/README.md +1 -0
  143. package/docs/templates/agent-dag.schema.json +2 -2
  144. package/docs/templates/backend-test-dag.json +6 -4
  145. package/docs/templates/frontend-implementation-contract.schema.json +34 -2
  146. package/harness.json +1 -1
  147. package/package.json +4 -5
  148. package/skills/frontend-plan/SKILL.md +14 -1
  149. package/skills/frontend-plan/references/decision-contract.md +101 -5
  150. package/skills/frontend-plan/references/design-decisions.md +32 -0
  151. package/dist/worker/console/static/assets/channel-DsZxrgqe.js +0 -1
  152. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +0 -1
  153. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +0 -1
  154. package/dist/worker/console/static/assets/index-D9Sc0f0y.js +0 -449
  155. package/dist/worker/console/static/assets/index-DDQc5a50.css +0 -1
  156. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +0 -1
  157. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.js +0 -8
@@ -62,9 +62,21 @@ function readSettingsPackages(settingsPath) {
62
62
  return undefined;
63
63
  }
64
64
  }
65
- /** Strip an `npm:` prefix from a settings package spec, e.g. `npm:pi-lens`. */
65
+ /**
66
+ * Strip an `npm:` prefix and a trailing `@<version>`/`@<dist-tag>` suffix
67
+ * from a settings package spec, e.g. `npm:pi-lens@3.8.72` → `pi-lens`,
68
+ * `npm:@scope/pkg@1.0.0` → `@scope/pkg`. The leading `@` of a scoped
69
+ * package name is preserved (the version separator is the LAST `@` at a
70
+ * positive index). Mirrors the console-side spec parsing semantics so a
71
+ * version-pinned install (e.g. via `pi install npm:pi-lens@3.8.72`) resolves
72
+ * identically in both surfaces.
73
+ */
66
74
  export function packageNameFromSpec(spec) {
67
- return spec.startsWith("npm:") ? spec.slice(4) : spec;
75
+ const base = spec.startsWith("npm:") ? spec.slice(4) : spec;
76
+ const at = base.lastIndexOf("@");
77
+ if (at > 0)
78
+ return base.slice(0, at);
79
+ return base;
68
80
  }
69
81
  function loadPackageInventories(repoRoot) {
70
82
  const agentDir = getPiAgentDir();
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
- import { createHash } from "node:crypto";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { withFrontendTestReporter, parseFrontendTestExecutionReport } from "../workflows/dag/frontend-test-execution-evidence.js";
3
4
  import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
4
5
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
5
6
  import path from "node:path";
@@ -15,7 +16,7 @@ import { buildRequirementCoverageGateShellCommand, expandShellPreset, buildVerdi
15
16
  import { materializeBackendTestAnalysisContract } from "../workflows/dag/backend-test-analysis-contract.js";
16
17
  import { extractBackendTestContractEnvelope } from "../workflows/dag/backend-test-contract-envelope.js";
17
18
  import { materializeFrontendImplementationContract } from "../workflows/dag/frontend-implementation-contract.js";
18
- import { assertFrontendSourceBindingFresh, buildContractFromCommittedFacts, deterministicSha256, frontendImplementationContractSchema, serializeCompactDeterministicJson, sha256Hex, writeFrontendImplementationContractArtifact, } from "../workflows/dag/frontend-implementation-contract.js";
19
+ import { assertFrontendSourceBindingFresh, buildContractFromCommittedFacts, deterministicSha256, frontendImplementationContractSchema, serializeCompactDeterministicJson, sha256Hex, writeFrontendImplementationContractArtifact, buildFrontendVerifyCommandDirectory, } from "../workflows/dag/frontend-implementation-contract.js";
19
20
  import { checkFrontendSourceFidelityLedger } from "../workflows/dag/frontend-source-fidelity-ledger.js";
20
21
  import { materializeFrontendTestResult, validateFrontendCaseEvidence, } from "../workflows/dag/frontend-test-result-contract.js";
21
22
  import { copyFrontendTestStandardScenarios } from "../workflows/dag/frontend-test-standard-scenarios.js";
@@ -1907,11 +1908,12 @@ async function executePipelineCommands(input, meta, overrideCommands) {
1907
1908
  return results;
1908
1909
  }
1909
1910
  /** A+B (AC-008): derive unresolved verification entrypoints from committed plan
1910
- * facts. A verification target is unresolved when its commandLabel does not
1911
- * match any frozen command label from the verify shell evidence bundle.
1912
- * Returns `{ ref, digest }` entries; a missing/unreadable plan ledger fails
1913
- * closed to [] so the router restarts the contract instead of inventing refs. */
1914
- async function deriveUnresolvedVerificationEntrypointRefs(runDir, frozenCommandLabels) {
1911
+ * facts. A verification target is unresolved when its commandId does not
1912
+ * match any frozen command directory entry derived from the verify shell
1913
+ * evidence bundle. Returns `{ ref, digest }` entries; a missing/unreadable
1914
+ * plan ledger fails closed to [] so the router restarts the contract instead
1915
+ * of inventing refs. */
1916
+ async function deriveUnresolvedVerificationEntrypointRefs(runDir, frozenCommandIds) {
1915
1917
  try {
1916
1918
  const { restorePlanPatchFromCommittedFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
1917
1919
  const records = await readTypedEventStoreFromJsonl(path.join(runDir, "frontend-plan-pi", "plan-typed-facts.jsonl"));
@@ -1920,15 +1922,13 @@ async function deriveUnresolvedVerificationEntrypointRefs(runDir, frozenCommandL
1920
1922
  ?.verificationTargets;
1921
1923
  if (!Array.isArray(targets))
1922
1924
  return [];
1923
- const frozen = new Set(frozenCommandLabels
1924
- .map((label) => label.trim().toLowerCase())
1925
- .filter(Boolean));
1925
+ const frozen = new Set(frozenCommandIds.filter(Boolean));
1926
1926
  const refs = [];
1927
1927
  for (const item of targets) {
1928
1928
  const target = item;
1929
- const commandLabel = typeof target.commandLabel === "string" ? target.commandLabel : "";
1930
- if (commandLabel &&
1931
- frozen.has(commandLabel.trim().toLowerCase())) {
1929
+ // v2 plan facts carry commandId as the stable directory key.
1930
+ const commandId = typeof target.commandId === "string" ? target.commandId : "";
1931
+ if (commandId && frozen.has(commandId)) {
1932
1932
  continue;
1933
1933
  }
1934
1934
  const file = typeof target.file === "string" ? target.file : "";
@@ -1951,6 +1951,23 @@ async function executeFrontendVerificationBundle(input, meta) {
1951
1951
  const bundle = shell.frontendVerificationBundle;
1952
1952
  const cwd = resolveShellCwd(input.cwd, shell.cwd);
1953
1953
  const results = [];
1954
+ const commandResultsByKey = new Map();
1955
+ const commandKey = (command) => `${cwd}\u0000${command.trim()}`;
1956
+ const reuseCommandResult = (result, command) => {
1957
+ const reusedAt = new Date().toISOString();
1958
+ return {
1959
+ ...result,
1960
+ command,
1961
+ durationMs: 0,
1962
+ wallDurationMs: 0,
1963
+ executionDurationMs: result.executionDurationMs ?? result.durationMs,
1964
+ startedAt: reusedAt,
1965
+ finishedAt: reusedAt,
1966
+ stdoutArtifactPath: undefined,
1967
+ stderrArtifactPath: undefined,
1968
+ reused: true,
1969
+ };
1970
+ };
1954
1971
  let beforeStatus;
1955
1972
  try {
1956
1973
  beforeStatus = await readGitStatusPorcelain(input.cwd);
@@ -1959,22 +1976,32 @@ async function executeFrontendVerificationBundle(input, meta) {
1959
1976
  beforeStatus = undefined;
1960
1977
  }
1961
1978
  const lintResults = [];
1979
+ const successfulLabels = new Map();
1980
+ const successfulCommandTexts = new Map();
1981
+ const executedTests = new Map();
1982
+ const commandExecutionEvidence = new Map();
1962
1983
  for (const command of bundle.lintCommands ?? []) {
1963
- const commandNumber = results.length + 1;
1964
- const result = await executeShellCommand({
1965
- command,
1966
- cwd,
1967
- timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
1968
- envAllowlist: shell.envAllowlist,
1969
- dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
1970
- reportActivity: input.reportActivity,
1971
- outputArtifacts: {
1972
- stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
1973
- stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
1974
- },
1975
- });
1976
- lintResults.push(result);
1984
+ const key = commandKey(command);
1985
+ const cached = commandResultsByKey.get(key);
1986
+ let result = cached ? reuseCommandResult(cached, command) : undefined;
1987
+ if (!result) {
1988
+ const commandNumber = results.length + 1;
1989
+ result = await executeShellCommand({
1990
+ command,
1991
+ cwd,
1992
+ timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
1993
+ envAllowlist: shell.envAllowlist,
1994
+ dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
1995
+ reportActivity: input.reportActivity,
1996
+ outputArtifacts: {
1997
+ stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
1998
+ stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
1999
+ },
2000
+ });
2001
+ commandResultsByKey.set(key, result);
2002
+ }
1977
2003
  results.push(result);
2004
+ lintResults.push(result);
1978
2005
  }
1979
2006
  let lintAssessment;
1980
2007
  if ((bundle.lintCommands?.length ?? 0) > 0 &&
@@ -2008,30 +2035,64 @@ async function executeFrontendVerificationBundle(input, meta) {
2008
2035
  labels: bundle.behaviorEvidence.commandLabels,
2009
2036
  },
2010
2037
  ];
2011
- const successfulLabels = new Map();
2038
+ const failedGroups = new Set();
2039
+ const groupFailures = [];
2012
2040
  for (const group of lintBlocked ? [] : groups) {
2013
2041
  for (const [index, command] of group.commands.entries()) {
2014
- const commandNumber = results.length + 1;
2015
- const result = await executeShellCommand({
2016
- command,
2017
- cwd,
2018
- timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
2019
- envAllowlist: shell.envAllowlist,
2020
- dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
2021
- reportActivity: input.reportActivity,
2022
- outputArtifacts: {
2023
- stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
2024
- stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
2025
- },
2026
- });
2042
+ if (failedGroups.has(group.name))
2043
+ break;
2044
+ const key = commandKey(command);
2045
+ const cached = commandResultsByKey.get(key);
2046
+ let result = cached ? reuseCommandResult(cached, command) : undefined;
2047
+ if (!result) {
2048
+ const commandNumber = results.length + 1;
2049
+ const reportPath = path.join(meta.runDir, input.task.id, "commands", `${commandNumber}-${randomUUID()}.vitest.json`);
2050
+ const observedCommand = await withFrontendTestReporter({ command, cwd, reportPath });
2051
+ if (observedCommand)
2052
+ await mkdir(path.dirname(reportPath), { recursive: true });
2053
+ result = await executeShellCommand({
2054
+ command: observedCommand ?? command,
2055
+ cwd,
2056
+ timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
2057
+ envAllowlist: shell.envAllowlist,
2058
+ dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
2059
+ reportActivity: input.reportActivity,
2060
+ outputArtifacts: {
2061
+ stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
2062
+ stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
2063
+ },
2064
+ });
2065
+ let reportError;
2066
+ if (observedCommand && result.ok) {
2067
+ try {
2068
+ commandExecutionEvidence.set(key, parseFrontendTestExecutionReport(JSON.parse(await readFile(reportPath, "utf8")), cwd));
2069
+ }
2070
+ catch (error) {
2071
+ reportError = `frontend test execution report unavailable: ${error instanceof Error ? error.message : String(error)}`;
2072
+ }
2073
+ }
2074
+ if (reportError) {
2075
+ result = { ...result, ok: false, failureCategory: "invalid-output", stderr: reportError };
2076
+ }
2077
+ commandResultsByKey.set(key, result);
2078
+ }
2027
2079
  results.push(result);
2028
2080
  if (result.ok && group.labels[index]) {
2081
+ const observations = commandExecutionEvidence.get(key);
2082
+ if (observations !== undefined)
2083
+ executedTests.set(group.labels[index], observations);
2029
2084
  const labels = successfulLabels.get(group.name) ?? [];
2030
2085
  labels.push(group.labels[index]);
2031
2086
  successfulLabels.set(group.name, labels);
2087
+ const texts = successfulCommandTexts.get(group.name) ?? [];
2088
+ texts.push(command);
2089
+ successfulCommandTexts.set(group.name, texts);
2032
2090
  }
2033
- if (!result.ok)
2091
+ if (!result.ok) {
2092
+ groupFailures.push(result);
2093
+ failedGroups.add(group.name);
2034
2094
  break;
2095
+ }
2035
2096
  }
2036
2097
  }
2037
2098
  if (beforeStatus !== undefined) {
@@ -2061,12 +2122,15 @@ async function executeFrontendVerificationBundle(input, meta) {
2061
2122
  exitCode: result.exitCode,
2062
2123
  failureCategory: result.failureCategory,
2063
2124
  command: result.command,
2125
+ reused: result.reused === true,
2064
2126
  }));
2127
+ // Lint baseline-debt is tolerated, so a lint-only failure must not become the
2128
+ // verification failure. A shared lint/static or lint/behavior command is
2129
+ // different: once reused by a non-lint lane, its failure belongs to that lane
2130
+ // and must remain the primary failure evidence.
2065
2131
  const firstFailure = lintBlocked
2066
2132
  ? lintResults.find((result) => !result.ok)
2067
- : results
2068
- .filter((result) => !lintResults.includes(result))
2069
- .find((result) => !result.ok);
2133
+ : groupFailures[0];
2070
2134
  const lintSyntheticFailure = lintBlocked && !firstFailure
2071
2135
  ? {
2072
2136
  failureCategory: "invalid-output",
@@ -2083,7 +2147,7 @@ async function executeFrontendVerificationBundle(input, meta) {
2083
2147
  mock: {
2084
2148
  nodeId: input.task.id,
2085
2149
  commandLabels: successfulLabels.get("mock") ?? [],
2086
- commandTexts: bundle.mockCommands.slice(0, successfulLabels.get("mock")?.length ?? 0),
2150
+ commandTexts: successfulCommandTexts.get("mock") ?? [],
2087
2151
  },
2088
2152
  static: {
2089
2153
  nodeId: input.task.id,
@@ -2092,6 +2156,7 @@ async function executeFrontendVerificationBundle(input, meta) {
2092
2156
  behavior: {
2093
2157
  nodeId: input.task.id,
2094
2158
  commandLabels: successfulLabels.get("behavior") ?? [],
2159
+ executedTests,
2095
2160
  },
2096
2161
  },
2097
2162
  });
@@ -2124,7 +2189,9 @@ async function executeFrontendVerificationBundle(input, meta) {
2124
2189
  nodeId: failedNodeId,
2125
2190
  rawFailureCategory: failureCategory,
2126
2191
  });
2127
- const failureOwner = classifiedOwner === "unknown" ? "implement-pi" : classifiedOwner;
2192
+ const failureOwner = classifiedOwner === "unknown" &&
2193
+ ["typecheck", "build", "lint", "component-test", "unit-test", "trace"].includes(failureClass)
2194
+ ? "implementation" : classifiedOwner;
2128
2195
  const restartPhase = classifyRestartPhase(failureOwner);
2129
2196
  // A+B (AC-008): verification-config routing. Produce structured
2130
2197
  // unresolved entrypoint refs from committed plan facts and the frozen
@@ -2135,12 +2202,18 @@ async function executeFrontendVerificationBundle(input, meta) {
2135
2202
  // never re-classifies the same signal twice.
2136
2203
  let verificationConfigRoute;
2137
2204
  if (failureOwner === "verification-config") {
2138
- const frozenCommandLabels = [
2139
- ...(bundle.staticEvidence.commandLabels ?? []),
2140
- ...(bundle.behaviorEvidence.commandLabels ?? []),
2141
- ...(bundle.mockEvidence?.commandLabels ?? []),
2142
- ];
2143
- const unresolvedEntrypointRefs = await deriveUnresolvedVerificationEntrypointRefs(meta.runDir, frozenCommandLabels);
2205
+ const verifyDirectory = buildFrontendVerifyCommandDirectory({
2206
+ staticLabels: bundle.staticEvidence.commandLabels ?? [],
2207
+ behaviorLabels: bundle.behaviorEvidence.commandLabels ?? [],
2208
+ mockLabels: bundle.mockEvidence?.commandLabels ?? [],
2209
+ lintLabels: bundle.lintEvidence?.commandLabels ?? [],
2210
+ staticCommandTexts: bundle.staticEvidence.commandTexts ?? [],
2211
+ behaviorCommandTexts: bundle.behaviorEvidence.commandTexts ?? [],
2212
+ mockCommandTexts: bundle.mockEvidence?.commandTexts ?? [],
2213
+ lintCommandTexts: bundle.lintEvidence?.commandTexts ?? [],
2214
+ });
2215
+ const frozenCommandLabels = verifyDirectory.map((entry) => entry.label);
2216
+ const unresolvedEntrypointRefs = await deriveUnresolvedVerificationEntrypointRefs(meta.runDir, verifyDirectory.map((entry) => entry.commandId));
2144
2217
  verificationConfigRoute = routeVerificationConfig({
2145
2218
  unresolvedEntrypointRefs: unresolvedEntrypointRefs.map((ref) => ref.ref),
2146
2219
  });
@@ -2531,7 +2604,7 @@ async function executeFrontendDesignPolicy(input, meta) {
2531
2604
  };
2532
2605
  }
2533
2606
  const uncoveredVTs = contract.verificationTargets
2534
- .filter((vt) => vt.type !== "static")
2607
+ .filter((vt) => vt.mode !== "static")
2535
2608
  .filter((vt) => ![...writeSet].some((pattern) => pathMatchesPattern(vt.file, pattern) || vt.file === pattern));
2536
2609
  if (uncoveredVTs.length > 0) {
2537
2610
  return {
@@ -2598,6 +2671,10 @@ async function executeFrontendDesignPolicy(input, meta) {
2598
2671
  [],
2599
2672
  allowedDependencies: config.allowedDependencies,
2600
2673
  writeSetPatterns: config.implementationWriteSet ?? [],
2674
+ // Fresh-existence evidence at gate time (Scout pathEvidence.fresh
2675
+ // semantics): a reuse-existing evidencePath must name a file that
2676
+ // exists NOW; greenfield paths are decision=new only.
2677
+ hasFreshPathEvidence: (candidate) => existsSync(path.resolve(input.cwd, candidate)),
2601
2678
  });
2602
2679
  // 8. Write canonical contract + policy result.
2603
2680
  const contractArtifact = await writeFrontendImplementationContractArtifact({
@@ -63,6 +63,12 @@ const RAW_TO_NORMALIZED = {
63
63
  // compaction / fresh context before retrying — an executor-side
64
64
  // environment condition, not a plan-quality defect.
65
65
  "context-overflow": "executor",
66
+ // stopReason=length output-capacity truncation: same executor-side
67
+ // capacity class as context-overflow. Legacy thinking-exhausted labels
68
+ // (pre-output-limit artifacts) map to the same class.
69
+ "output-limit": "executor",
70
+ "writer-thinking-exhausted": "executor",
71
+ "planner-thinking-exhausted": "executor",
66
72
  // Deterministic capability/policy refusal: missing verified launcher JS
67
73
  // entry, capability allowlist rejection, or missing bounded write tool
68
74
  // profile. These are environment/config defects the operator repairs
@@ -147,7 +147,7 @@ export async function applyTaskContract(input) {
147
147
  ref,
148
148
  changedFiles: [],
149
149
  idempotentReplay: true,
150
- txId: existing.txId,
150
+ ...(existing.txId ? { txId: existing.txId } : { noOp: true }),
151
151
  };
152
152
  }
153
153
  const lock = await acquireTaskContractLock({
@@ -175,7 +175,9 @@ export async function applyTaskContract(input) {
175
175
  ref,
176
176
  changedFiles: [],
177
177
  idempotentReplay: true,
178
- txId: existingUnderLock.txId,
178
+ ...(existingUnderLock.txId
179
+ ? { txId: existingUnderLock.txId }
180
+ : { noOp: true }),
179
181
  };
180
182
  }
181
183
  const state = await observeTaskContractState(input.repoRoot, input.taskId);
@@ -251,6 +253,38 @@ export async function applyTaskContract(input) {
251
253
  sourceHashes,
252
254
  taskConfigSha256,
253
255
  });
256
+ // Content-level no-op: when the freshly projected draft hashes
257
+ // identically to both the recorded contract and the on-disk observation,
258
+ // the journaled commit would only churn revision/updatedAt metadata and
259
+ // force downstream consumers (writeSet gate digest binds
260
+ // contractRevision+contractCanonicalHash) to re-issue approvals for
261
+ // zero content change. Return the current ref untouched instead. A
262
+ // drifted disk (observed !== ref) never reaches this short-circuit: the
263
+ // apply is then a repair projection and must run.
264
+ if (state.ref &&
265
+ canonicalHash === state.ref.canonicalHash &&
266
+ state.observedCanonicalHash === state.ref.canonicalHash) {
267
+ await assertLockHeld(lock);
268
+ await appendRequestLedgerEntry({
269
+ repoRoot: input.repoRoot,
270
+ taskId: input.taskId,
271
+ entry: {
272
+ schemaVersion: 1,
273
+ requestId: input.requestId,
274
+ requestPayloadSha256: input.requestPayloadSha256,
275
+ revision: state.ref.revision,
276
+ canonicalHash: state.ref.canonicalHash,
277
+ recordedAt: new Date().toISOString(),
278
+ resultSummary: { ok: true, operation: "apply" },
279
+ },
280
+ });
281
+ return {
282
+ ref: state.ref,
283
+ changedFiles: [],
284
+ idempotentReplay: false,
285
+ noOp: true,
286
+ };
287
+ }
254
288
  const nextRevision = currentRevision + 1;
255
289
  const nextRef = {
256
290
  schemaVersion: TASK_CONTRACT_REF_SCHEMA_VERSION,
@@ -13,6 +13,13 @@ const HEADING_ALIASES = {
13
13
  "需求范围",
14
14
  "已确认范围",
15
15
  "已确认需求",
16
+ // Common zh-CN frontend-PRD variants seen in real PRDs; without these a
17
+ // raw-PRD fallback parse (semantic intake unavailable) yields an empty
18
+ // scope and flips the import to incomplete (EMPTY scope deadlock).
19
+ "目标表面",
20
+ "交付范围",
21
+ "实现范围",
22
+ "交付内容",
16
23
  ]),
17
24
  nonGoals: new Set(["非目标", "out of scope", "non-goals", "non goals"]),
18
25
  acceptance: new Set([
@@ -22,14 +22,16 @@ import path from "node:path";
22
22
  import { ensureSecureDir, writeSecureJsonSync, } from "../../../infrastructure/console/app-data.js";
23
23
  import { isTerminalOperationState, } from "../../../infrastructure/console/operation-store.js";
24
24
  import { scrubSecrets } from "./explore-tools.js";
25
- const OPERATION_PREVIEW_MAX_CHARS = 1200;
25
+ /** Bounded preview stored on the event ring / SSE (not the live tool payload). */
26
+ export const OPERATION_PREVIEW_MAX_CHARS = 4000;
27
+ const OPERATION_PREVIEW_TRUNCATION_SUFFIX = "…[truncated]";
26
28
  function boundedRedactedText(value, maxChars = OPERATION_PREVIEW_MAX_CHARS) {
27
29
  if (typeof value !== "string" || !value.trim())
28
30
  return undefined;
29
31
  const { scrubbed } = scrubSecrets(value);
30
32
  return scrubbed.length <= maxChars
31
33
  ? scrubbed
32
- : `${scrubbed.slice(0, maxChars)}…[truncated]`;
34
+ : `${scrubbed.slice(0, maxChars)}${OPERATION_PREVIEW_TRUNCATION_SUFFIX}`;
33
35
  }
34
36
  function safeIdentity(value) {
35
37
  return boundedRedactedText(value, 240);
@@ -32,7 +32,8 @@ import { extractAssistantContentBlocks, extractAssistantText, isAssistantSdkMess
32
32
  import { parseProviderError, } from "./provider-error.js";
33
33
  import { projectCompactSnapshot, } from "./chat-event-store.js";
34
34
  import { extractUsageSample } from "./usage.js";
35
- import { OPERATOR_CHAT_ALLOWED_TOOLS, authorizeOperatorChatTool, assertNoWriteToolInList, } from "./tools.js";
35
+ import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_BUILTIN_CUSTOM_TOOLS, authorizeOperatorChatTool, assertNoWriteToolInList, } from "./tools.js";
36
+ import { buildAgentCustomTool, ExploreOrchestrator, } from "./subagents/index.js";
36
37
  import { createOperatorChatResourceLoader, } from "./resource-loader.js";
37
38
  import { applySkillAutoInvocationPreferences, isAutoInvocationEnabled, loadResourcePreferences, skillResourceId, } from "./resource-preferences-store.js";
38
39
  import { buildModelCallableToolSchemas } from "./tool-adapter.js";
@@ -115,6 +116,7 @@ export function OPERATOR_CHAT_ACTIVE_TOOL_NAMES() {
115
116
  ...OPERATOR_CHAT_ALLOWED_TOOLS,
116
117
  ...OPERATOR_CHAT_BUILTIN_EXPLORE_TOOLS,
117
118
  ...OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS,
119
+ ...OPERATOR_CHAT_BUILTIN_CUSTOM_TOOLS,
118
120
  ];
119
121
  }
120
122
  /**
@@ -1024,14 +1026,33 @@ export class ConsolePiRuntime {
1024
1026
  compactionControllers = new Map();
1025
1027
  siftStates = new Map();
1026
1028
  /** Session-less tool definition caches (no AgentSession state). */
1027
- /** Session-less tool definition caches (no AgentSession state). */
1028
1029
  operatorCustomToolsPromise;
1029
1030
  exploreCustomToolsPromise;
1031
+ exploreOrchestrator;
1030
1032
  bindings;
1031
1033
  constructor(options) {
1032
1034
  this.options = options;
1033
1035
  this.loader = createOperatorChatResourceLoader();
1034
1036
  this.bindings = options.bindings ?? createDefaultPiSdkBindings();
1037
+ this.exploreOrchestrator = new ExploreOrchestrator({
1038
+ cwd: this.options.cwd,
1039
+ bindings: this.bindings,
1040
+ loadSafeExploreCustomTools: () => this.loadExploreCustomTools(),
1041
+ // Fallback when the parent session's model is unknown at Agent-call
1042
+ // time (e.g. turn started before materialization finished): resolve
1043
+ // the default Chat model from the child services' modelRuntime.
1044
+ resolveModel: async (modelRuntime) => {
1045
+ const available = this.bindings.listAvailableModels({ modelRuntime });
1046
+ const descriptor = await resolveDefaultChatModel(this.options.cwd, available);
1047
+ if (!descriptor)
1048
+ return undefined;
1049
+ return this.bindings.resolveModel({
1050
+ modelRuntime,
1051
+ provider: descriptor.provider,
1052
+ modelId: descriptor.modelId,
1053
+ });
1054
+ },
1055
+ });
1035
1056
  }
1036
1057
  /** Absolute path for repo-scoped resource preferences (may be undefined in unit tests). */
1037
1058
  get resourcePreferencesPath() {
@@ -1526,12 +1547,31 @@ export class ConsolePiRuntime {
1526
1547
  const goalTools = this.options.goalRegistry
1527
1548
  ? await buildGoalTools({ sessionId, registry: this.options.goalRegistry })
1528
1549
  : [];
1550
+ const agentTool = await buildAgentCustomTool({
1551
+ parentSessionId: sessionId,
1552
+ orchestrator: this.exploreOrchestrator,
1553
+ resolveModel: () => {
1554
+ const model = this.sessionModels.get(sessionId);
1555
+ const modelRuntime = this.modelRuntimes.get(sessionId);
1556
+ if (!model || !modelRuntime)
1557
+ return undefined;
1558
+ return this.bindings.resolveModel({
1559
+ modelRuntime,
1560
+ provider: model.provider,
1561
+ modelId: model.modelId,
1562
+ });
1563
+ },
1564
+ }).catch((error) => {
1565
+ process.stderr.write(`[console] chat Agent tool build failed: ${error instanceof Error ? error.message : String(error)}\n`);
1566
+ return undefined;
1567
+ });
1529
1568
  return [
1530
1569
  ...operatorCustomTools,
1531
1570
  ...exploreCustomTools,
1532
1571
  ...(questionTool ? [questionTool] : []),
1533
1572
  ...(todoTool ? [todoTool] : []),
1534
1573
  ...goalTools,
1574
+ ...(agentTool ? [agentTool] : []),
1535
1575
  ];
1536
1576
  }
1537
1577
  loadOperatorCustomTools() {
@@ -2595,6 +2635,7 @@ export class ConsolePiRuntime {
2595
2635
  catch {
2596
2636
  // ignore
2597
2637
  }
2638
+ void this.exploreOrchestrator.abortByParent(sessionId);
2598
2639
  };
2599
2640
  try {
2600
2641
  if (options?.turnId && this.options.questionRegistry) {
@@ -2970,10 +3011,12 @@ export class ConsolePiRuntime {
2970
3011
  this.thinkingSelections.delete(sessionId);
2971
3012
  this.reloadInFlight.delete(sessionId);
2972
3013
  this.pendingSteering.delete(sessionId);
3014
+ void this.exploreOrchestrator.abortByParent(sessionId);
2973
3015
  }
2974
3016
  disposeAll() {
2975
3017
  for (const id of [...this.sessions.keys()])
2976
3018
  this.dispose(id);
3019
+ void this.exploreOrchestrator.abortAll();
2977
3020
  }
2978
3021
  }
2979
3022
  function mapAssistantMessageEvent(type, sessionId, message) {
@@ -15,7 +15,7 @@
15
15
  * The three-gate authorization (authorizeOperatorChatTool) is the actual
16
16
  * enforcement; this adapter describes what that gate admits.
17
17
  */
18
- import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, OPERATOR_CHAT_PI_BUILTIN_TOOLS, authorizeOperatorChatTool, filterOperatorChatTools, assertNoWriteToolInList, } from "./tools.js";
18
+ import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, OPERATOR_CHAT_PI_BUILTIN_TOOLS, OPERATOR_CHAT_BUILTIN_CUSTOM_TOOLS, authorizeOperatorChatTool, filterOperatorChatTools, assertNoWriteToolInList, } from "./tools.js";
19
19
  /** Built-in Pi tool names allowed in every Chat session (ADR 0011). */
20
20
  export const OPERATOR_CHAT_BUILTIN_EXPLORE_TOOL_IDS = Object.freeze([
21
21
  ...OPERATOR_CHAT_PI_BUILTIN_TOOLS,
@@ -42,6 +42,9 @@ export function createOperatorChatResourceLoader() {
42
42
  if (explore) {
43
43
  return { ok: true, toolId };
44
44
  }
45
+ if (OPERATOR_CHAT_BUILTIN_CUSTOM_TOOLS.includes(toolId)) {
46
+ return { ok: true, toolId };
47
+ }
45
48
  const decision = authorizeOperatorChatTool(toolId);
46
49
  if (decision.ok)
47
50
  active.add(decision.toolId);
@@ -517,6 +517,14 @@ export async function handleChatCapabilities(_req, res, deps) {
517
517
  deniedTools: OPERATOR_CHAT_DENIED_TOOLS,
518
518
  deniedOperatorActions: OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS,
519
519
  tools: toolSchemas,
520
+ builtinSubagents: [
521
+ {
522
+ toolId: "Agent",
523
+ agent: "Explore",
524
+ kind: "subagent",
525
+ description: "Start the built-in read-only Explore subagent for codebase search, symbol lookup, and call-chain tracing.",
526
+ },
527
+ ],
520
528
  // ADR 0011: full Pi tools including bash are default-active.
521
529
  hasBash: true,
522
530
  });
@@ -851,6 +859,7 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
851
859
  }
852
860
  }
853
861
  await deps.operationLinker.recoverSession(sessionId);
862
+ const messagePage = await deps.store.listMessages(sessionId, { limit: 50 });
854
863
  const composerDraft = await new ComposerDraftStore(deps.appData).get(sessionId);
855
864
  const snapshot = deps.events.snapshot(sessionId);
856
865
  const telemetry = deriveChatSessionTelemetry(snapshot);
@@ -862,7 +871,7 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
862
871
  eventEpoch: deps.events.epoch(sessionId),
863
872
  session: {
864
873
  ...projectBrowserChatSession(record),
865
- messages: projectStoredMessagesWithEventOrder(record.messages, snapshot),
874
+ messages: projectStoredMessagesWithEventOrder(messagePage.messages, snapshot),
866
875
  composerDraft,
867
876
  activeTurnId: activeTurn?.turnId,
868
877
  turnPhase: activeTurn
@@ -874,6 +883,10 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
874
883
  todos: projectCurrentTodos(snapshot) ?? [],
875
884
  goal: deps.goalRegistry?.get(sessionId) ?? null,
876
885
  },
886
+ history: {
887
+ hasMore: messagePage.hasMore,
888
+ ...(messagePage.nextCursor ? { nextCursor: messagePage.nextCursor } : {}),
889
+ },
877
890
  hasBash: true,
878
891
  sessionStats: telemetry.stats,
879
892
  usage: telemetry.usage,
@@ -1134,19 +1147,18 @@ export async function handleSteerDeferredTurn(req, res, deps, sessionId, clientR
1134
1147
  }
1135
1148
  }
1136
1149
  async function persistAcceptedSteering(deps, sessionId, turnId, userMessage, clientRequestId) {
1150
+ emitUserMessageEvent(deps, sessionId, turnId, userMessage, clientRequestId, true);
1151
+ await emitDeferredTurnUpdate(deps, sessionId, turnId, undefined, 0);
1152
+ }
1153
+ function emitUserMessageEvent(deps, sessionId, turnId, userMessage, clientRequestId, steering = false) {
1137
1154
  deps.events.append(sessionId, turnId, {
1138
1155
  kind: "user_message",
1139
1156
  data: {
1140
- clientRequestId,
1141
- steering: true,
1142
- message: {
1143
- id: userMessage.id,
1144
- text: userMessage.text,
1145
- at: userMessage.at,
1146
- },
1157
+ ...(clientRequestId ? { clientRequestId } : {}),
1158
+ ...(steering ? { steering: true } : {}),
1159
+ message: { id: userMessage.id, text: userMessage.text, at: userMessage.at },
1147
1160
  },
1148
1161
  });
1149
- await emitDeferredTurnUpdate(deps, sessionId, turnId, undefined, 0);
1150
1162
  }
1151
1163
  /** Lightweight busy-truth endpoint for the client reconcile loop (pi-web
1152
1164
  * get_state equivalent): exposes the active turn + the latest turn's durable
@@ -1690,7 +1702,8 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
1690
1702
  text,
1691
1703
  images,
1692
1704
  signal: ac.signal,
1693
- onUserMessagePersisted: (record) => {
1705
+ onUserMessagePersisted: (record, userMessage) => {
1706
+ emitUserMessageEvent(deps, sessionId, turnId, userMessage);
1694
1707
  const users = record.messages.filter((message) => message.role === "user");
1695
1708
  if (users.length !== 1)
1696
1709
  return;
@@ -2000,9 +2013,12 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
2000
2013
  sessionId,
2001
2014
  turnId: turn.turnId,
2002
2015
  text,
2016
+ clientRequestId,
2003
2017
  images,
2004
2018
  signal: controller.signal,
2005
2019
  onUserMessagePersisted: async (record, userMessage) => {
2020
+ if (!trustedPayload)
2021
+ emitUserMessageEvent(deps, sessionId, turn.turnId, userMessage, clientRequestId);
2006
2022
  await trustedPayload?.onUserMessagePersisted?.(turn.turnId, userMessage);
2007
2023
  const users = record.messages.filter((message) => message.role === "user");
2008
2024
  if (users.length !== 1)
@@ -2845,8 +2861,8 @@ async function handleSddBingo(req, res, deps, sessionId) {
2845
2861
  if (Object.keys(body).some((key) => key !== "skillName")) {
2846
2862
  throw new SddDataAlignmentError("SDD_INVALID_SKILL_NAME", "请求体只允许 skillName 字段", 400);
2847
2863
  }
2848
- await deps.sddDataAlignment.report(sessionId, body.skillName);
2849
- sendJson(res, 200, { ok: true });
2864
+ const result = await deps.sddDataAlignment.report(sessionId, body.skillName);
2865
+ sendJson(res, 200, { ok: true, skillName: result.skillName });
2850
2866
  }
2851
2867
  catch (error) {
2852
2868
  if (error instanceof SddDataAlignmentError) {