@tea-agent/loop-agent 0.43.0-next.1 → 0.43.0-next.11

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 (207) hide show
  1. package/CHANGELOG.md +144 -9
  2. package/dist/application/dag/run-dag.js +5 -0
  3. package/dist/application/evaluation/budget.js +19 -1
  4. package/dist/application/task-lifecycle/advance.js +17 -4
  5. package/dist/application/task-lifecycle/observe.js +43 -29
  6. package/dist/application/task-lifecycle/plan-transitions.js +5 -4
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/executors/dag-pi-executor.js +1237 -586
  9. package/dist/executors/pi-executor.js +22 -1
  10. package/dist/executors/pi-sdk-executor.js +140 -39
  11. package/dist/executors/shell-executor.js +8 -4
  12. package/dist/shared/frontend-execution-policy.js +23 -0
  13. package/dist/task/config-types.js +4 -0
  14. package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
  15. package/dist/task/source-prepare/ledger-review.js +6 -9
  16. package/dist/task/source-prepare/semantic-intake.js +16 -26
  17. package/dist/task/source-prepare/source-fidelity-pi.js +28 -9
  18. package/dist/task/source-references.js +48 -23
  19. package/dist/worker/console/chat/assistant-content.js +23 -2
  20. package/dist/worker/console/chat/browser-policy.js +113 -0
  21. package/dist/worker/console/chat/browser-routes.js +140 -0
  22. package/dist/worker/console/chat/explore-tools.js +13 -0
  23. package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +44 -0
  24. package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +202 -0
  25. package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +79 -0
  26. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +86 -0
  27. package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +38 -0
  28. package/dist/worker/console/chat/pi-runtime/inspection-types.js +28 -0
  29. package/dist/worker/console/chat/pi-runtime/inspection.js +439 -0
  30. package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +153 -0
  31. package/dist/worker/console/chat/pi-runtime/sdk-session.js +5 -0
  32. package/dist/worker/console/chat/pi-runtime.js +207 -1178
  33. package/dist/worker/console/chat/routes.js +208 -71
  34. package/dist/worker/console/chat/scm-routes.js +217 -0
  35. package/dist/worker/console/chat/scm-service.js +283 -0
  36. package/dist/worker/console/chat/scm-tools.js +111 -0
  37. package/dist/worker/console/chat/sdd-data-alignment.js +65 -9
  38. package/dist/worker/console/chat/session-catalog.js +32 -0
  39. package/dist/worker/console/chat/session-mode-view.js +49 -0
  40. package/dist/worker/console/chat/session-mode.js +219 -0
  41. package/dist/worker/console/chat/session-store.js +22 -6
  42. package/dist/worker/console/chat/shortcuts.js +6 -0
  43. package/dist/worker/console/chat/terminal-routes.js +216 -0
  44. package/dist/worker/console/chat/terminal-sessions.js +348 -0
  45. package/dist/worker/console/chat/terminal-tools.js +211 -0
  46. package/dist/worker/console/chat/tool-preview.js +11 -0
  47. package/dist/worker/console/chat/tools.js +2 -0
  48. package/dist/worker/console/server.js +2 -28
  49. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B0Q0nClf.js → abnfDiagram-N423BO3Z-CTSK0xbU.js} +1 -1
  50. package/dist/worker/console/static/assets/{arc-DCPjC19G.js → arc-_2MfctFP.js} +1 -1
  51. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP5n9jmG.js → architectureDiagram-T3A2C74G-DsDe2wTg.js} +1 -1
  52. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CmaWi_Wg.js → blockDiagram-VBNYF7ZC-Dtz_779U.js} +1 -1
  53. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct5tcMfZ.js → c4Diagram-5PPSVZJV-LaULi-gw.js} +1 -1
  54. package/dist/worker/console/static/assets/channel-CqS4yBWT.js +1 -0
  55. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C0osm1Zf.js → chunk-2GRJ4B5K-DuHBqqX8.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CDWviED7.js → chunk-2Q5K7J3B-BZgZs1dn.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-mtSk1-j7.js → chunk-5RXB4S5H-tzwmlIO_.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D5J9PHC7.js → chunk-5VM5RSS4-CeYrLkSn.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Begg4WAa.js → chunk-6Q2QTUOP-C92cgilm.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-sF1AEy7T.js → chunk-GF5L2VYU-XOXpGC0F.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BqbsWXZb.js → chunk-JWPE2WC7-CGHbJyCH.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BKJrPcJ6.js → chunk-KBJHAD2P-Bs2-LTzf.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CcwNMRho.js → chunk-RYQCIY6F-rN7up7E4.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BEJi3iIs.js → chunk-XXDRQBXY-B_fWjKpR.js} +1 -1
  65. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DVOxNOq0.js +1 -0
  66. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DVOxNOq0.js +1 -0
  67. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-DRLgTvVu.js → cose-bilkent-JH36ORCC-DE3SdJvB.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BoAVG3cs.js → cynefin-VYW2F7L2-B-4yuRAX.js} +1 -1
  69. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bzol06hR.js → cynefinDiagram-MW4NZA55-TiCPFuO8.js} +1 -1
  70. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UmliJM77.js → dagre-VZM6K2ZE-7crzLMzA.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BWLcJkFo.js → diagram-7IWD3JNH-Cvr-x6Fd.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CgApOm-O.js → diagram-B4RE2ZJO-BzcRHUjH.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Bh3RgTLs.js → diagram-LBJQPF4R-BjkAXilh.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Dsh-D5nE.js → diagram-Q27KOJAE-BkzyATK7.js} +1 -1
  75. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dkbbpcpb.js → diagram-UB23O5K3-rZuTarYP.js} +1 -1
  76. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-3EBaA3t3.js → ebnfDiagram-BXEA7PRR-CAYENaN1.js} +1 -1
  77. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bq2Dc5ok.js → erDiagram-JOGREHBK-DCEam0Uq.js} +1 -1
  78. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-De7y55ha.js → flowDiagram-UKHOOZJN-e_dH4wVO.js} +1 -1
  79. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-fLiDbQRh.js → ganttDiagram-PKOTCBZU-CvZa2zOK.js} +1 -1
  80. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Du617mp1.js → gitGraphDiagram-DS77QQ5N-CbT5TuKp.js} +1 -1
  81. package/dist/worker/console/static/assets/index-CUPeoE8W.js +468 -0
  82. package/dist/worker/console/static/assets/index-SAANIK8g.css +1 -0
  83. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BwmlF-XP.js → infoDiagram-6WML65LV-BWzHQa_Z.js} +1 -1
  84. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-zsGtS59u.js → ishikawaDiagram-WSZJBQD7-BTjlovtU.js} +1 -1
  85. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DTTPTe3f.js → journeyDiagram-NVQOT4AX-DkvQoCc1.js} +1 -1
  86. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BkXg9aP-.js → kanban-definition-27J2QSJJ-CHSzGi3W.js} +1 -1
  87. package/dist/worker/console/static/assets/{linear-7U2ue5IE.js → linear-DZMkTCWH.js} +1 -1
  88. package/dist/worker/console/static/assets/{mermaid.core-BUuGHmWO.js → mermaid.core-BIMhty7A.js} +5 -5
  89. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-7q6fX0Sv.js → mindmap-definition-FAOFIHXS-Dx-tyMl3.js} +1 -1
  90. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkQPJN55.js → pegDiagram-VL7TDLO6-C6G34rbY.js} +1 -1
  91. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CCal_tfX.js → pieDiagram-7S7Q4E2Y-CHK9XOfm.js} +1 -1
  92. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js → quadrantDiagram-CIZ2JOQS-CDARkI0w.js} +1 -1
  93. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CJmww7D-.js → railroadDiagram-AXF67PYL-DiuUe0ZQ.js} +1 -1
  94. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D6ldWJEv.js → requirementDiagram-LRYGKXZP-RE61UR56.js} +1 -1
  95. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-VGh33I9e.js → sankeyDiagram-W5VNT64P-KDNLHMEG.js} +1 -1
  96. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CCkDrIjU.js → sequenceDiagram-SI44F4Z6-CeVgXnD9.js} +1 -1
  97. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-NC8f5Otb.js → sizeCapture-X5ZJPWSS-CXwGyjjG.js} +1 -1
  98. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-_92ezZdF.js → stateDiagram-OKZ733FA-BjTfozab.js} +1 -1
  99. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BXoYsyr2.js +1 -0
  100. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BdUCGtbP.js → swimlanes-SLNWSIFB-BaXOB8GT.js} +2 -2
  101. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DfKliS4e.js +8 -0
  102. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CdD1H8ia.js → timeline-definition-Z64GVDOM-C3r2hqUU.js} +1 -1
  103. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Dkdk3oFo.js → vennDiagram-T6HMQDX7-MWKgl1S9.js} +1 -1
  104. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CKu2uPPY.js → wardleyDiagram-T6FBY63Y-C9iFcZPk.js} +1 -1
  105. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-ClqAnuG8.js → xychartDiagram-ELKLHX3M-DqnM24Jp.js} +1 -1
  106. package/dist/worker/console/static/index.html +2 -2
  107. package/dist/worker/console/static-src/operator-chat/active-turn-tail.js +10 -0
  108. package/dist/worker/console/static-src/operator-chat/chat-link.js +94 -0
  109. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +122 -10
  110. package/dist/worker/console/static-src/operator-chat/compaction-message.js +7 -0
  111. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +27 -15
  112. package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
  113. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
  114. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +10 -0
  115. package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
  116. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +44 -24
  117. package/dist/worker/console/static-src/operator-chat/useChatStream.js +18 -2
  118. package/dist/worker/console/static-src/operator-chat/useChatThread.js +41 -8
  119. package/dist/worker/console/static-src/operator-chat/useComposer.js +26 -11
  120. package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
  121. package/dist/worker/console/workspace-context.js +15 -1
  122. package/dist/worker/observe/node-transparency.js +81 -72
  123. package/dist/worker/observe/routes.js +20 -1
  124. package/dist/worker/observe/static/dag-inspector-humanize.js +3 -0
  125. package/dist/worker/observe/static/dom.js +20 -1
  126. package/dist/worker/observe/static/format-pool.d.ts +2 -0
  127. package/dist/worker/observe/static/format-pool.js +6 -0
  128. package/dist/worker/observe/static/format.js +7 -0
  129. package/dist/worker/observe/static/inspect-workspace.js +34 -7
  130. package/dist/worker/observe/static/inspector-submission.js +32 -0
  131. package/dist/worker/observe/static/kpi.js +1 -0
  132. package/dist/worker/observe/static/relations.js +2 -0
  133. package/dist/worker/observe/static/router.js +13 -0
  134. package/dist/worker/observe/static/run-processing.js +2 -0
  135. package/dist/worker/observe/static/shell-chrome.js +36 -3
  136. package/dist/worker/observe/static/state.js +35 -2
  137. package/dist/worker/observe/static/styles.css +263 -39
  138. package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
  139. package/dist/worker/observe/static/task-failure-labels.js +67 -0
  140. package/dist/worker/observe/static/task-history.js +12 -0
  141. package/dist/worker/observe/static/views/batch.js +6 -13
  142. package/dist/worker/observe/static/views/dag-graph.js +50 -3
  143. package/dist/worker/observe/static/views/dag-inspector.js +742 -268
  144. package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
  145. package/dist/worker/observe/static/views/dag.d.ts +6 -0
  146. package/dist/worker/observe/static/views/dag.js +49 -11
  147. package/dist/worker/observe/static/views/dags.js +2 -0
  148. package/dist/worker/observe/static/views/dashboard.js +21 -12
  149. package/dist/worker/observe/static/views/failures.js +21 -11
  150. package/dist/worker/observe/static/views/feature.js +11 -29
  151. package/dist/worker/observe/static/views/pool.js +37 -28
  152. package/dist/worker/observe/static/views/run.js +48 -5
  153. package/dist/worker/observe/static/views/session-timeline.js +189 -240
  154. package/dist/worker/observe/static/views/task.js +81 -62
  155. package/dist/workflows/dag/budget-enforcement.js +53 -3
  156. package/dist/workflows/dag/frontend-capacity.js +9 -0
  157. package/dist/workflows/dag/frontend-closeout.js +1 -1
  158. package/dist/workflows/dag/frontend-design-policy.js +2 -1
  159. package/dist/workflows/dag/frontend-durable-tools.js +193 -0
  160. package/dist/workflows/dag/frontend-execution-groups.js +24 -0
  161. package/dist/workflows/dag/frontend-implementation-contract.js +13 -0
  162. package/dist/workflows/dag/frontend-input-projection.js +76 -0
  163. package/dist/workflows/dag/frontend-plan-completeness.js +186 -0
  164. package/dist/workflows/dag/frontend-plan-recovery-policy.js +18 -0
  165. package/dist/workflows/dag/frontend-plan-render.js +10 -3
  166. package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
  167. package/dist/workflows/dag/frontend-recovery-controller.js +7 -7
  168. package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
  169. package/dist/workflows/dag/frontend-recovery-run.js +4 -0
  170. package/dist/workflows/dag/frontend-review-context.js +2 -1
  171. package/dist/workflows/dag/frontend-review-scopes.js +139 -0
  172. package/dist/workflows/dag/frontend-session-budget.js +249 -0
  173. package/dist/workflows/dag/frontend-shadow-dual-write.js +20 -2
  174. package/dist/workflows/dag/frontend-shape-facts.js +12 -2
  175. package/dist/workflows/dag/frontend-shape.js +30 -1
  176. package/dist/workflows/dag/frontend-test-execution-evidence.js +3 -2
  177. package/dist/workflows/dag/frontend-typed-event-store.js +11 -0
  178. package/dist/workflows/dag/frontend-verification-trace.js +1 -1
  179. package/dist/workflows/dag/init-hybrid.js +65 -167
  180. package/dist/workflows/dag/node-execution.js +38 -155
  181. package/dist/workflows/dag/prompt.js +4 -0
  182. package/dist/workflows/dag/rerun-plan.js +7 -1
  183. package/dist/workflows/dag/runner.js +26 -1
  184. package/dist/workflows/dag/types.js +6 -0
  185. package/docs/operations/README.md +1 -0
  186. package/docs/templates/frontend-design-contract.md +4 -4
  187. package/docs/templates/frontend-implementation-contract.schema.json +34 -2
  188. package/docs/templates/frontend-implementation-dag.json +14 -13
  189. package/package.json +6 -3
  190. package/skills/frontend-bounded-implement/SKILL.md +3 -3
  191. package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
  192. package/skills/frontend-contract/SKILL.md +2 -1
  193. package/skills/frontend-contract/references/contract-protocol.md +22 -4
  194. package/skills/frontend-design-review/SKILL.md +22 -16
  195. package/skills/frontend-design-review/references/review-checklist.md +7 -7
  196. package/skills/frontend-plan/SKILL.md +21 -2
  197. package/skills/frontend-plan/references/decision-contract.md +42 -23
  198. package/skills/frontend-review/SKILL.md +22 -29
  199. package/skills/frontend-review/references/review-findings.md +24 -43
  200. package/skills/frontend-scout/references/scout-evidence.md +4 -0
  201. package/dist/worker/console/static/assets/channel-DAS07MdS.js +0 -1
  202. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cwk-5jiW.js +0 -1
  203. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cwk-5jiW.js +0 -1
  204. package/dist/worker/console/static/assets/index-C0O48S_P.js +0 -449
  205. package/dist/worker/console/static/assets/index-CzKf4U8P.css +0 -1
  206. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyJg5gzW.js +0 -1
  207. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B1cw_0Uy.js +0 -8
@@ -1,3 +1,6 @@
1
+ import path from "node:path";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { observeFrontendSession } from "../../workflows/dag/frontend-session-budget.js";
1
4
  /**
2
5
  * 生产 Source Fidelity Pi 适配器(AC-PROD-001..006 / AC-001..006)
3
6
  *
@@ -22,7 +25,7 @@ import { resolveDagPiModelConfig } from "../../executors/dag-pi-executor.js";
22
25
  import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
23
26
  import { listPiSdkAvailableModels } from "../../executors/pi-sdk-executor.js";
24
27
  import { loadHarnessManifest } from "../../governance/harness.js";
25
- import { loadTaskConfig } from "../runtime.js";
28
+ import { getTaskPaths, loadTaskConfig } from "../runtime.js";
26
29
  import { buildSemanticIntakeModelQueue, extractJsonObject, } from "./semantic-intake.js";
27
30
  import { LEDGER_RECONCILIATION_DEFAULT_BUDGET_CHARS, LedgerReconciliationError, } from "./ledger-reconciliation.js";
28
31
  import { LedgerReviewError, } from "./ledger-review.js";
@@ -66,7 +69,7 @@ const reviewResponseSchema = z.object({
66
69
  disposition: z.enum(FRAGMENT_DISPOSITIONS).optional(),
67
70
  reason: z.string().min(1).optional(),
68
71
  })
69
- .optional(),
72
+ .nullish(),
70
73
  });
71
74
  /**
72
75
  * 模型队列构建镜像 semantic-intake.ts:MED→HIGH→LOW(harness executors.pi)+
@@ -112,7 +115,7 @@ function buildReconciliationPrompt(request) {
112
115
  `fragment path: ${request.fragment.path}`,
113
116
  `fragment headingPath: ${request.fragment.headingPath}`,
114
117
  `fragment lineRange: ${JSON.stringify(request.fragment.lineRange)}`,
115
- `budgetChars: ${request.budgetChars}`,
118
+ `packing hint: ${request.budgetChars} chars; the fragment below is complete and must not be truncated or summarized before deciding`,
116
119
  "fragment text:",
117
120
  "```markdown",
118
121
  request.fragment.text,
@@ -131,7 +134,7 @@ function buildReviewPrompt(request) {
131
134
  "- approved: true only when the exclusion/conflict handling is justified.",
132
135
  "- fragmentId: MUST equal the requested fragment id exactly (echo the `fragment id:` line verbatim); identity mismatch is rejected.",
133
136
  "- reason: recommended; explain the verdict.",
134
- "- revision: optional single bounded revision {disposition, reason}; never invent requirements or human resolutions.",
137
+ "- revision: omit or use null when no revision is proposed; otherwise one bounded revision {disposition, reason}. Never invent requirements or human resolutions.",
135
138
  JSON.stringify({
136
139
  fragmentId: "REQ-SRC-AB12CD34EF56",
137
140
  approved: true,
@@ -247,7 +250,11 @@ export async function runProductionLedgerReconciliation(request, options) {
247
250
  let lastFailure = "";
248
251
  for (const modelRef of queue) {
249
252
  try {
250
- const result = await execute({
253
+ const result = await observeFrontendSession({
254
+ phase: "source-reconciliation", userMessage: "Reconcile the source fragment into the required JSON only.", sourceDigest: createHash("sha256").update(JSON.stringify(request)).digest("hex"), scopeIds: [request.fragment.id], prompt, model: modelRef, taskId: options.taskId, sink: options.observationSink,
255
+ artifactPath: options.taskId ? path.join(getTaskPaths(options.repoRoot, options.taskId).taskDir, "artifacts", "session-budget", `${randomUUID()}.json`) : undefined,
256
+ }, observer => execute({
257
+ onAttemptObservation: observer,
251
258
  attachedFiles: [],
252
259
  modelConfig: resolveDagPiModelConfig(modelRef),
253
260
  prompt,
@@ -266,10 +273,14 @@ export async function runProductionLedgerReconciliation(request, options) {
266
273
  return [error instanceof Error ? error.message : String(error)];
267
274
  }
268
275
  },
269
- });
276
+ }));
270
277
  if (!result.ok) {
271
278
  lastFailure =
272
279
  `${result.failureCategory}: ${(result.stderr || result.assistantText).slice(0, 2000)}`;
280
+ if (result.stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory)) {
281
+ lastFailure = `SOURCE_FIDELITY_CAPACITY_EXHAUSTED: refine this complete source fragment without losing conditions; ${lastFailure}`;
282
+ break;
283
+ }
273
284
  continue;
274
285
  }
275
286
  try {
@@ -306,7 +317,11 @@ export async function runProductionLedgerReview(request, options) {
306
317
  let lastFailure = "";
307
318
  for (const modelRef of queue) {
308
319
  try {
309
- const result = await execute({
320
+ const result = await observeFrontendSession({
321
+ phase: "source-review", userMessage: "Review the fragment disposition into the required JSON only.", sourceDigest: createHash("sha256").update(JSON.stringify(request)).digest("hex"), scopeIds: [request.fragment.id], prompt, model: modelRef, taskId: options.taskId, sink: options.observationSink,
322
+ artifactPath: options.taskId ? path.join(getTaskPaths(options.repoRoot, options.taskId).taskDir, "artifacts", "session-budget", `${randomUUID()}.json`) : undefined,
323
+ }, observer => execute({
324
+ onAttemptObservation: observer,
310
325
  attachedFiles: [],
311
326
  modelConfig: resolveDagPiModelConfig(modelRef),
312
327
  prompt,
@@ -325,10 +340,14 @@ export async function runProductionLedgerReview(request, options) {
325
340
  return [error instanceof Error ? error.message : String(error)];
326
341
  }
327
342
  },
328
- });
343
+ }));
329
344
  if (!result.ok) {
330
345
  lastFailure =
331
346
  `${result.failureCategory}: ${(result.stderr || result.assistantText).slice(0, 2000)}`;
347
+ if (result.stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory)) {
348
+ lastFailure = `SOURCE_FIDELITY_CAPACITY_EXHAUSTED: refine this complete source fragment without losing conditions; ${lastFailure}`;
349
+ break;
350
+ }
332
351
  continue;
333
352
  }
334
353
  try {
@@ -380,5 +399,5 @@ export async function resolveProductionSourceFidelityPi(input) {
380
399
  }
381
400
  if (effectiveTaskKind !== SOURCE_FIDELITY_TASK_KIND)
382
401
  return undefined;
383
- return createProductionSourceFidelityPi({ repoRoot: input.repoRoot });
402
+ return createProductionSourceFidelityPi({ repoRoot: input.repoRoot, taskId: input.taskId });
384
403
  }
@@ -5,7 +5,7 @@
5
5
  * source/references/, separate from the derived source/需求.md contract.
6
6
  */
7
7
  import { createHash } from "node:crypto";
8
- import { access, copyFile, mkdir, readdir, readFile, writeFile, } from "node:fs/promises";
8
+ import { access, copyFile, lstat, mkdir, readdir, readFile, writeFile, } from "node:fs/promises";
9
9
  import path from "node:path";
10
10
  import { getTaskPaths, loadTaskConfig } from "./runtime.js";
11
11
  import { writeTaskConfig } from "../infrastructure/harness/task-store.js";
@@ -87,6 +87,7 @@ export async function materializeTaskReferenceDocs(input) {
87
87
  const referenceDir = path.join(paths.sourceDir, SOURCE_REFERENCE_DIRECTORY);
88
88
  await mkdir(referenceDir, { recursive: true });
89
89
  const materialized = [];
90
+ const manifest = await readSourceManifest(paths.sourceDir);
90
91
  for (const doc of docs) {
91
92
  const absoluteSource = path.isAbsolute(doc.path)
92
93
  ? doc.path
@@ -94,7 +95,10 @@ export async function materializeTaskReferenceDocs(input) {
94
95
  await assertReadableFile(absoluteSource, `referenceDocs entry "${doc.name ?? doc.path}"`);
95
96
  const extension = path.extname(absoluteSource) || ".md";
96
97
  const baseName = slugifyName(doc.name ?? path.basename(absoluteSource, extension));
97
- const targetName = `${baseName}${extension}`;
98
+ const imported = manifest?.documents.find((entry) => path.basename(entry.materializedPath, path.extname(entry.materializedPath)) === baseName);
99
+ const targetName = imported
100
+ ? path.basename(imported.materializedPath)
101
+ : `${baseName}${extension}`;
98
102
  const targetPath = path.join(referenceDir, targetName);
99
103
  try {
100
104
  await access(targetPath);
@@ -132,14 +136,23 @@ export async function importPrdDocument(input) {
132
136
  catch {
133
137
  // directory may be empty / just created
134
138
  }
135
- // Prefer stable name "requirement.md" when free; otherwise suffix.
136
- const preferredName = `${requestedName}${extension}`;
137
- const targetFileName = existingNames.has(preferredName)
138
- ? // If the same content is already there, reuse; else unique name.
139
- (await sameHashIfExists(path.join(referenceDir, preferredName), hash.sha256))
140
- ? preferredName
141
- : ensureUniqueReferenceName(existingNames, requestedName, extension)
142
- : preferredName;
139
+ // Search every immutable reference, including suffixed and renamed imports.
140
+ // Roles remain distinct: identical bytes must not erase a constraint/AC role.
141
+ const matching = existingManifest?.documents.find((doc) => doc.role === role && doc.sha256 === hash.sha256 && doc.bytes === hash.bytes);
142
+ if (matching) {
143
+ const candidate = path.resolve(paths.sourceDir, matching.materializedPath);
144
+ if (path.dirname(candidate) !== path.resolve(referenceDir)) {
145
+ throw new Error(`REFERENCE_PATH_UNSAFE: ${matching.materializedPath}`);
146
+ }
147
+ const stats = await lstat(candidate);
148
+ if (!stats.isFile() || stats.isSymbolicLink() ||
149
+ !(await sameHashIfExists(candidate, hash.sha256))) {
150
+ throw new Error(`REFERENCE_HASH_MISMATCH: ${matching.materializedPath}`);
151
+ }
152
+ }
153
+ const targetFileName = matching
154
+ ? path.basename(matching.materializedPath)
155
+ : ensureUniqueReferenceName(existingNames, requestedName, extension);
143
156
  const targetPath = path.join(referenceDir, targetFileName);
144
157
  if (!(await fileExists(targetPath))) {
145
158
  await writeFile(targetPath, hash.content);
@@ -149,7 +162,7 @@ export async function importPrdDocument(input) {
149
162
  ? toPosixRelative(input.repoRoot, absoluteSource)
150
163
  : input.filePath.split(path.sep).join("/");
151
164
  const materializedRelativePath = `${SOURCE_REFERENCE_DIRECTORY}/${targetFileName}`;
152
- const document = {
165
+ const document = matching ?? {
153
166
  role,
154
167
  name: requestedName,
155
168
  sourcePath: sourcePathForRecord,
@@ -176,20 +189,28 @@ export async function importPrdDocument(input) {
176
189
  : (existingManifest?.updatedAt ?? importedAt),
177
190
  documents,
178
191
  };
179
- await writeSourceManifest(paths.sourceDir, manifest);
192
+ if (documentSetChanged)
193
+ await writeSourceManifest(paths.sourceDir, manifest);
180
194
  const taskConfig = await loadTaskConfig(input.repoRoot, input.taskId);
181
195
  const referenceDocs = [...(taskConfig.referenceDocs ?? [])];
182
- const alreadyListed = referenceDocs.some((doc) => {
196
+ const listedIndex = referenceDocs.findIndex((doc) => {
183
197
  const absolute = path.isAbsolute(doc.path)
184
198
  ? doc.path
185
199
  : path.resolve(input.repoRoot, doc.path);
186
200
  return absolute === absoluteSource;
187
201
  });
188
- if (!alreadyListed) {
189
- referenceDocs.push({
190
- name: requestedName,
191
- path: sourcePathForRecord,
192
- });
202
+ // Keep original source paths as provenance, but point materialization at
203
+ // the chosen immutable filename so DAG generation cannot recreate duplicates.
204
+ const reference = {
205
+ name: path.basename(targetFileName, path.extname(targetFileName)),
206
+ path: sourcePathForRecord,
207
+ };
208
+ const configChanged = listedIndex < 0 || referenceDocs[listedIndex]?.name !== reference.name;
209
+ if (configChanged) {
210
+ if (listedIndex < 0)
211
+ referenceDocs.push(reference);
212
+ else
213
+ referenceDocs[listedIndex] = { ...referenceDocs[listedIndex], ...reference };
193
214
  await writeTaskConfig(input.repoRoot, input.taskId, {
194
215
  ...taskConfig,
195
216
  referenceDocs,
@@ -198,11 +219,15 @@ export async function importPrdDocument(input) {
198
219
  // Managed contracts must bump revision after import-prd mutates manifest/task config.
199
220
  // Lazy import avoids circular init with contract/observe.
200
221
  const { refreshManagedContractAfterImport } = await import("./contract/import-revision.js");
201
- await refreshManagedContractAfterImport({
202
- repoRoot: input.repoRoot,
203
- taskId: input.taskId,
204
- });
222
+ const changed = documentSetChanged || configChanged;
223
+ if (changed) {
224
+ await refreshManagedContractAfterImport({
225
+ repoRoot: input.repoRoot,
226
+ taskId: input.taskId,
227
+ });
228
+ }
205
229
  return {
230
+ changed,
206
231
  taskId: input.taskId,
207
232
  sourcePath: sourcePathForRecord,
208
233
  materializedAbsolutePath: targetPath,
@@ -210,7 +235,7 @@ export async function importPrdDocument(input) {
210
235
  sha256: hash.sha256,
211
236
  bytes: hash.bytes,
212
237
  manifestPath: getSourceManifestPath(paths.sourceDir),
213
- referenceDocs: alreadyListed ? referenceDocs : [...referenceDocs],
238
+ referenceDocs,
214
239
  };
215
240
  }
216
241
  async function sameHashIfExists(filePath, sha256) {
@@ -115,9 +115,30 @@ export function splitFinalAssistantBlocks(blocks, options = {}) {
115
115
  if (lastProcessIndex === -1) {
116
116
  return { answerBlocks: displayable, processBlocks: [] };
117
117
  }
118
+ const trailing = displayable.slice(lastProcessIndex + 1);
119
+ if (trailing.length > 0) {
120
+ return {
121
+ answerBlocks: trailing,
122
+ processBlocks: displayable.slice(0, lastProcessIndex + 1),
123
+ };
124
+ }
125
+ // The stage ends on a non-text block (a tool call after the prose, or a final
126
+ // thinking block). Returning an empty answer here hid the whole reply inside
127
+ // the collapsed process subtree; keep the last prose block as the visible
128
+ // answer instead (2026-09-08).
129
+ let lastTextIndex = -1;
130
+ for (let i = displayable.length - 1; i >= 0; i--) {
131
+ if (isFinalAnswerBlock(displayable[i])) {
132
+ lastTextIndex = i;
133
+ break;
134
+ }
135
+ }
136
+ if (lastTextIndex === -1) {
137
+ return { answerBlocks: [], processBlocks: displayable };
138
+ }
118
139
  return {
119
- answerBlocks: displayable.slice(lastProcessIndex + 1),
120
- processBlocks: displayable.slice(0, lastProcessIndex + 1),
140
+ answerBlocks: [displayable[lastTextIndex]],
141
+ processBlocks: displayable.filter((_block, index) => index !== lastTextIndex),
121
142
  };
122
143
  }
123
144
  export function joinAssistantText(blocks) {
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Pure URL policy for the Operator Chat browser panel: normalize user input
3
+ * into an http(s) URL. Dependency-free so it is unit-testable.
4
+ *
5
+ * Only the scheme is gated: javascript:, data:, file: and friends must never
6
+ * reach the iframe. Local addresses (localhost / 127.0.0.1 / ::1 / LAN
7
+ * hosts) are deliberately allowed — the panel is a development console and
8
+ * its users routinely browse local dev servers.
9
+ */
10
+ /**
11
+ * Extract the `frame-ancestors` source list of a Content-Security-Policy
12
+ * header, or undefined when the directive is absent (or empty).
13
+ */
14
+ export function extractFrameAncestors(csp) {
15
+ if (csp === null)
16
+ return undefined;
17
+ for (const directive of csp.split(";")) {
18
+ const parts = directive.trim().split(/\s+/);
19
+ if (parts[0] === "frame-ancestors") {
20
+ const sources = parts.slice(1).filter((source) => source !== "");
21
+ return sources.length === 0 ? undefined : sources;
22
+ }
23
+ }
24
+ return undefined;
25
+ }
26
+ /**
27
+ * Decide whether a site can render inside the panel iframe. The signals are
28
+ * exactly the ones the browser enforces when it refuses an iframe load:
29
+ * X-Frame-Options DENY/SAMEORIGIN, or a frame-ancestors directive that does
30
+ * not allow `*` ('self' means the SITE's own origin — never ours, so it also
31
+ * blocks the panel). A site we could not reach yields 'unknown' and the
32
+ * plain iframe stays.
33
+ */
34
+ export function embeddabilityOf(probe) {
35
+ if (probe.reachable !== true)
36
+ return "unknown";
37
+ const xfo = probe.xFrameOptions?.trim().toUpperCase();
38
+ if (xfo === "DENY" || xfo === "SAMEORIGIN")
39
+ return "blocked";
40
+ if (probe.frameAncestors !== undefined &&
41
+ !probe.frameAncestors.some((source) => source === "*")) {
42
+ return "blocked";
43
+ }
44
+ return "embeddable";
45
+ }
46
+ /**
47
+ * Schemes that must never reach the iframe even without `//` (javascript:,
48
+ * data:, file:, ...). Host:port lookalikes (example.com:8080) are NOT here —
49
+ * they parse as hosts below.
50
+ */
51
+ const FORBIDDEN_SCHEMES = new Set([
52
+ "javascript",
53
+ "data",
54
+ "file",
55
+ "about",
56
+ "vbscript",
57
+ "blob",
58
+ "mailto",
59
+ "tel",
60
+ "ftp",
61
+ "ftps",
62
+ "ws",
63
+ "wss",
64
+ "sftp",
65
+ "ssh",
66
+ "chrome",
67
+ "chrome-extension",
68
+ "moz-extension",
69
+ "edge",
70
+ "opera",
71
+ "resource",
72
+ "view-source",
73
+ ]);
74
+ /**
75
+ * Normalize one address-bar input against the navigation policy.
76
+ * @param input raw user text.
77
+ */
78
+ export function normalizeBrowserUrl(input) {
79
+ const trimmed = input.trim();
80
+ if (trimmed === "")
81
+ return { kind: "invalid" };
82
+ // Distinguish an explicit scheme from a bare host:port. "example.com:8080"
83
+ // would match a naive scheme regex (dots are legal in schemes), so a
84
+ // scheme prefix is only honored when it is http(s) or a known-forbidden
85
+ // scheme; anything else is treated as a host and gets https://.
86
+ const schemeMatch = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(trimmed);
87
+ let withScheme;
88
+ if (schemeMatch === null) {
89
+ withScheme = `https://${trimmed}`;
90
+ }
91
+ else {
92
+ const scheme = schemeMatch[1]?.toLowerCase() ?? "";
93
+ if (scheme === "http" || scheme === "https")
94
+ withScheme = trimmed;
95
+ else if (FORBIDDEN_SCHEMES.has(scheme))
96
+ return { kind: "blocked", reason: "scheme" };
97
+ else
98
+ withScheme = `https://${trimmed}`;
99
+ }
100
+ let url;
101
+ try {
102
+ url = new URL(withScheme);
103
+ }
104
+ catch {
105
+ return { kind: "invalid" };
106
+ }
107
+ // Protocol backstop: any URL that still parses to a non-http(s) scheme
108
+ // (ftp://, ws:// — which carry `//` and skip the list) is refused here.
109
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
110
+ return { kind: "blocked", reason: "scheme" };
111
+ }
112
+ return { kind: "ok", url: url.href };
113
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * HTTP routes for the Operator Chat browser panel: one read-only probe
3
+ * endpoint the panel uses to explain an iframe refusal (X-Frame-Options /
4
+ * CSP frame-ancestors are exactly the signals the browser enforces when it
5
+ * refuses to embed a site). The probe is display-only (headers back to the
6
+ * caller), restricted to http(s) URLs, with a hard timeout. Local addresses
7
+ * are allowed — the panel routinely browses local dev servers.
8
+ */
9
+ import { embeddabilityOf, extractFrameAncestors, } from "./browser-policy.js";
10
+ function workspaceIdOf(req) {
11
+ try {
12
+ return new URL(req.url ?? "/", "http://localhost").searchParams.get("workspaceId") ?? undefined;
13
+ }
14
+ catch {
15
+ return undefined;
16
+ }
17
+ }
18
+ function sendJson(res, status, body) {
19
+ res.writeHead(status, {
20
+ "content-type": "application/json; charset=utf-8",
21
+ "cache-control": "no-store",
22
+ });
23
+ res.end(JSON.stringify(body));
24
+ }
25
+ /** Route dispatcher; returns true when the request was handled. */
26
+ export async function handleBrowserRequest(req, res, deps, pathname) {
27
+ if (!pathname.startsWith("/api/operator/v1/chat/browser"))
28
+ return false;
29
+ const method = (req.method ?? "GET").toUpperCase();
30
+ const rest = pathname.slice("/api/operator/v1/chat/browser".length);
31
+ const parts = rest.split("/").filter(Boolean);
32
+ const sub = parts[0] ?? "";
33
+ try {
34
+ if (method === "GET" && sub === "probe" && parts.length === 1) {
35
+ // Fail-closed: the probe needs a workspaceId like every chat API.
36
+ if (deps.requireWorkspace) {
37
+ const ws = deps.requireWorkspace(req);
38
+ if (!ws.ok) {
39
+ sendJson(res, 400, {
40
+ ok: false,
41
+ error: { code: "WORKSPACE_REQUIRED", message: "workspaceId is required" },
42
+ });
43
+ return true;
44
+ }
45
+ }
46
+ else if (!workspaceIdOf(req)) {
47
+ sendJson(res, 400, {
48
+ ok: false,
49
+ error: { code: "WORKSPACE_REQUIRED", message: "workspaceId is required" },
50
+ });
51
+ return true;
52
+ }
53
+ const url = new URL(req.url ?? "/", "http://localhost");
54
+ const raw = url.searchParams.get("url") ?? "";
55
+ let parsed;
56
+ try {
57
+ parsed = new URL(raw);
58
+ }
59
+ catch {
60
+ sendJson(res, 400, {
61
+ ok: false,
62
+ error: { code: "INVALID_URL", message: "invalid url" },
63
+ });
64
+ return true;
65
+ }
66
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
67
+ sendJson(res, 400, {
68
+ ok: false,
69
+ error: { code: "BAD_SCHEME", message: "only http/https urls can be probed" },
70
+ });
71
+ return true;
72
+ }
73
+ const controller = new AbortController();
74
+ const timer = setTimeout(() => controller.abort(), 8000);
75
+ try {
76
+ let response = await fetch(parsed, {
77
+ method: "HEAD",
78
+ redirect: "follow",
79
+ signal: controller.signal,
80
+ });
81
+ // Some servers answer HEAD with 405/501; retry once as GET
82
+ // (the body is discarded — only the headers matter).
83
+ if (response.status === 405 || response.status === 501) {
84
+ response = await fetch(parsed, {
85
+ method: "GET",
86
+ redirect: "follow",
87
+ signal: controller.signal,
88
+ });
89
+ }
90
+ const csp = response.headers.get("content-security-policy");
91
+ const frameAncestors = extractFrameAncestors(csp);
92
+ const xFrameOptions = response.headers.get("x-frame-options");
93
+ const probe = {
94
+ reachable: true,
95
+ url: response.url,
96
+ status: response.status,
97
+ ...(xFrameOptions !== null ? { xFrameOptions } : {}),
98
+ ...(frameAncestors !== undefined ? { frameAncestors } : {}),
99
+ };
100
+ sendJson(res, 200, {
101
+ ok: true,
102
+ data: { probe, verdict: embeddabilityOf(probe) },
103
+ });
104
+ return true;
105
+ }
106
+ catch {
107
+ // DNS / TLS / connection / timeout: nothing to judge — the
108
+ // client keeps the plain iframe.
109
+ sendJson(res, 200, {
110
+ ok: true,
111
+ data: { probe: { reachable: false }, verdict: "unknown" },
112
+ });
113
+ return true;
114
+ }
115
+ finally {
116
+ clearTimeout(timer);
117
+ }
118
+ }
119
+ sendJson(res, 404, {
120
+ ok: false,
121
+ error: { code: "NOT_FOUND", message: `unknown browser route: ${method} ${pathname}` },
122
+ });
123
+ return true;
124
+ }
125
+ catch (error) {
126
+ if (!res.headersSent) {
127
+ sendJson(res, 500, {
128
+ ok: false,
129
+ error: {
130
+ code: "BROWSER_INTERNAL",
131
+ message: error instanceof Error ? error.message : String(error),
132
+ },
133
+ });
134
+ }
135
+ else {
136
+ res.end();
137
+ }
138
+ return true;
139
+ }
140
+ }
@@ -71,6 +71,10 @@ export const SECRET_SCRUB_PATTERNS = [
71
71
  name: "private-key-block",
72
72
  re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |DSA |)PRIVATE KEY-----/g,
73
73
  },
74
+ {
75
+ name: "aimax-auth-token",
76
+ re: /\bAIMAX_AUTH_TOKEN=["']?[^\s"']+/g,
77
+ },
74
78
  ];
75
79
  /** Throw a typed explore error (kept as a value object so callers can read .code). */
76
80
  export class ExploreDeniedError extends Error {
@@ -89,6 +93,10 @@ export class ExploreDeniedError extends Error {
89
93
  */
90
94
  export function scrubSecrets(input) {
91
95
  let out = input;
96
+ const liveToken = process.env.AIMAX_AUTH_TOKEN;
97
+ if (liveToken && liveToken.length >= 8 && out.includes(liveToken)) {
98
+ out = out.split(liveToken).join("[REDACTED:aimax-auth-token]");
99
+ }
92
100
  const redactions = [];
93
101
  for (const { name, re } of SECRET_SCRUB_PATTERNS) {
94
102
  re.lastIndex = 0;
@@ -98,6 +106,11 @@ export function scrubSecrets(input) {
98
106
  redactions.push({ name, count: matches.length });
99
107
  }
100
108
  }
109
+ if (liveToken && liveToken.length >= 8 && input.includes(liveToken)) {
110
+ const existing = redactions.find((entry) => entry.name === "aimax-auth-token");
111
+ if (!existing)
112
+ redactions.push({ name: "aimax-auth-token", count: 1 });
113
+ }
101
114
  return { scrubbed: out, redactions };
102
115
  }
103
116
  /** True if a repo-relative path matches a sensitive-file denylist pattern. */
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Session-bound ask_user_question custom tool.
3
+ * ConsolePiRuntime supplies the sessionId and UserQuestionRegistry.
4
+ */
5
+ const ASK_USER_QUESTION_DESCRIPTION = "Ask the user concise questions when a confirmation, choice, or missing detail is required before proceeding. " +
6
+ "The tool pauses until the user answers. Use it instead of writing questions as ordinary assistant text. " +
7
+ "Send one or more questions with stable ids; recommended options should be first and end with (Recommended).";
8
+ export async function buildAskUserQuestionTool(options) {
9
+ const [{ Type }, { defineTool }] = await Promise.all([
10
+ import("typebox"),
11
+ import("@earendil-works/pi-coding-agent"),
12
+ ]);
13
+ return defineTool({
14
+ name: "ask_user_question",
15
+ label: "Ask user",
16
+ description: ASK_USER_QUESTION_DESCRIPTION,
17
+ promptSnippet: ASK_USER_QUESTION_DESCRIPTION,
18
+ parameters: Type.Object({
19
+ questions: Type.Array(Type.Object({
20
+ id: Type.String({ description: "Stable question id echoed in the answer." }),
21
+ question: Type.String({ description: "Specific user-facing question." }),
22
+ header: Type.Optional(Type.String({ description: "Optional short heading." })),
23
+ detail: Type.Optional(Type.String({ description: "Optional Markdown context shown below the question." })),
24
+ options: Type.Optional(Type.Array(Type.Object({
25
+ label: Type.String({ description: "Short user-facing option label." }),
26
+ description: Type.Optional(Type.String({ description: "One sentence explaining impact or tradeoff." })),
27
+ }, { additionalProperties: false }))),
28
+ multi_select: Type.Optional(Type.Boolean({ description: "Allow more than one option." })),
29
+ }, { additionalProperties: false }), { minItems: 1, maxItems: 20 }),
30
+ }, { additionalProperties: false }),
31
+ async execute(toolCallId, params, signal) {
32
+ const answer = await options.registry.ask({
33
+ sessionId: options.sessionId,
34
+ toolCallId,
35
+ questions: params.questions,
36
+ signal,
37
+ });
38
+ return {
39
+ content: [{ type: "text", text: JSON.stringify(answer) }],
40
+ details: answer,
41
+ };
42
+ },
43
+ });
44
+ }