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

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 (199) hide show
  1. package/CHANGELOG.md +129 -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 +1214 -584
  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 +5 -2
  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 +143 -0
  21. package/dist/worker/console/chat/browser-routes.js +148 -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 +78 -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 +213 -1181
  33. package/dist/worker/console/chat/routes.js +212 -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 +48 -0
  40. package/dist/worker/console/chat/session-mode.js +218 -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-D_fNbWc_.js} +1 -1
  50. package/dist/worker/console/static/assets/{arc-DCPjC19G.js → arc-_HuqLW6m.js} +1 -1
  51. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP5n9jmG.js → architectureDiagram-T3A2C74G-Djy2qY1m.js} +1 -1
  52. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CmaWi_Wg.js → blockDiagram-VBNYF7ZC-bB-1TSyY.js} +1 -1
  53. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct5tcMfZ.js → c4Diagram-5PPSVZJV-C5Hmghrz.js} +1 -1
  54. package/dist/worker/console/static/assets/channel-C0A6UEIa.js +1 -0
  55. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C0osm1Zf.js → chunk-2GRJ4B5K-Duyl39oy.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CDWviED7.js → chunk-2Q5K7J3B-x6mdnSyf.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-mtSk1-j7.js → chunk-5RXB4S5H-WlBuFj7W.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D5J9PHC7.js → chunk-5VM5RSS4-CTF429HX.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Begg4WAa.js → chunk-6Q2QTUOP-Dwd6r-SF.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-sF1AEy7T.js → chunk-GF5L2VYU-C-Br82D1.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BqbsWXZb.js → chunk-JWPE2WC7-CuGp0DeE.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BKJrPcJ6.js → chunk-KBJHAD2P-CJhFX6Lh.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CcwNMRho.js → chunk-RYQCIY6F-DpGOa_jT.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BEJi3iIs.js → chunk-XXDRQBXY-DXJaTynd.js} +1 -1
  65. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DzguNppy.js +1 -0
  66. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DzguNppy.js +1 -0
  67. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-DRLgTvVu.js → cose-bilkent-JH36ORCC-CRBZWjE4.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BoAVG3cs.js → cynefin-VYW2F7L2-BdXcxtlb.js} +1 -1
  69. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bzol06hR.js → cynefinDiagram-MW4NZA55-07UKsOts.js} +1 -1
  70. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UmliJM77.js → dagre-VZM6K2ZE-D38p9__S.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BWLcJkFo.js → diagram-7IWD3JNH-nSiBNNjH.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CgApOm-O.js → diagram-B4RE2ZJO-BFOTot5D.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Bh3RgTLs.js → diagram-LBJQPF4R-DypKaZ6k.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Dsh-D5nE.js → diagram-Q27KOJAE-B6yoOQeW.js} +1 -1
  75. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dkbbpcpb.js → diagram-UB23O5K3-BHYS6viX.js} +1 -1
  76. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-3EBaA3t3.js → ebnfDiagram-BXEA7PRR-D2VD5Snj.js} +1 -1
  77. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bq2Dc5ok.js → erDiagram-JOGREHBK-E98mWxN1.js} +1 -1
  78. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-De7y55ha.js → flowDiagram-UKHOOZJN-CT1zxhqP.js} +1 -1
  79. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-fLiDbQRh.js → ganttDiagram-PKOTCBZU-6GcngNcy.js} +1 -1
  80. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Du617mp1.js → gitGraphDiagram-DS77QQ5N-DCL6NvKe.js} +1 -1
  81. package/dist/worker/console/static/assets/index-24jp3aBh.js +468 -0
  82. package/dist/worker/console/static/assets/index-CY6y6I8M.css +1 -0
  83. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BwmlF-XP.js → infoDiagram-6WML65LV-C0627e-9.js} +1 -1
  84. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-zsGtS59u.js → ishikawaDiagram-WSZJBQD7-C_u3P6Ik.js} +1 -1
  85. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DTTPTe3f.js → journeyDiagram-NVQOT4AX-DPDPv9HL.js} +1 -1
  86. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BkXg9aP-.js → kanban-definition-27J2QSJJ-D3OHdWiO.js} +1 -1
  87. package/dist/worker/console/static/assets/{linear-7U2ue5IE.js → linear-DI88zD7n.js} +1 -1
  88. package/dist/worker/console/static/assets/{mermaid.core-BUuGHmWO.js → mermaid.core-p2l8IrWY.js} +5 -5
  89. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-7q6fX0Sv.js → mindmap-definition-FAOFIHXS-B1bAX-oS.js} +1 -1
  90. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkQPJN55.js → pegDiagram-VL7TDLO6-DwOnWxdD.js} +1 -1
  91. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CCal_tfX.js → pieDiagram-7S7Q4E2Y-xRkTXnJC.js} +1 -1
  92. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js → quadrantDiagram-CIZ2JOQS-_9UC5Zio.js} +1 -1
  93. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CJmww7D-.js → railroadDiagram-AXF67PYL-CgEUirWJ.js} +1 -1
  94. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D6ldWJEv.js → requirementDiagram-LRYGKXZP-Czbpwiyz.js} +1 -1
  95. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-VGh33I9e.js → sankeyDiagram-W5VNT64P-_j7_KiqY.js} +1 -1
  96. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CCkDrIjU.js → sequenceDiagram-SI44F4Z6-BNJa6Hyi.js} +1 -1
  97. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-NC8f5Otb.js → sizeCapture-X5ZJPWSS-DedpKtOc.js} +1 -1
  98. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-_92ezZdF.js → stateDiagram-OKZ733FA-BBvthr7c.js} +1 -1
  99. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BABVQnfM.js +1 -0
  100. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BdUCGtbP.js → swimlanes-SLNWSIFB-Ds7umgEM.js} +2 -2
  101. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D0qsLaxR.js +8 -0
  102. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CdD1H8ia.js → timeline-definition-Z64GVDOM-CKa-p1nF.js} +1 -1
  103. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Dkdk3oFo.js → vennDiagram-T6HMQDX7-LoUNR56o.js} +1 -1
  104. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CKu2uPPY.js → wardleyDiagram-T6FBY63Y-Bqbl6ym7.js} +1 -1
  105. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-ClqAnuG8.js → xychartDiagram-ELKLHX3M-B8dUg4lf.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-design-policy.js +2 -1
  158. package/dist/workflows/dag/frontend-durable-tools.js +193 -0
  159. package/dist/workflows/dag/frontend-execution-groups.js +24 -0
  160. package/dist/workflows/dag/frontend-implementation-contract.js +13 -0
  161. package/dist/workflows/dag/frontend-input-projection.js +76 -0
  162. package/dist/workflows/dag/frontend-plan-completeness.js +186 -0
  163. package/dist/workflows/dag/frontend-plan-recovery-policy.js +18 -0
  164. package/dist/workflows/dag/frontend-plan-render.js +10 -3
  165. package/dist/workflows/dag/frontend-recovery-controller.js +7 -7
  166. package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
  167. package/dist/workflows/dag/frontend-recovery-run.js +4 -0
  168. package/dist/workflows/dag/frontend-review-scopes.js +139 -0
  169. package/dist/workflows/dag/frontend-session-budget.js +249 -0
  170. package/dist/workflows/dag/frontend-shadow-dual-write.js +20 -2
  171. package/dist/workflows/dag/frontend-shape-facts.js +12 -2
  172. package/dist/workflows/dag/frontend-shape.js +30 -1
  173. package/dist/workflows/dag/frontend-test-execution-evidence.js +3 -2
  174. package/dist/workflows/dag/frontend-typed-event-store.js +11 -0
  175. package/dist/workflows/dag/init-hybrid.js +16 -10
  176. package/dist/workflows/dag/node-execution.js +38 -155
  177. package/dist/workflows/dag/prompt.js +4 -0
  178. package/dist/workflows/dag/rerun-plan.js +7 -1
  179. package/dist/workflows/dag/runner.js +26 -1
  180. package/dist/workflows/dag/types.js +6 -0
  181. package/docs/operations/README.md +1 -0
  182. package/docs/templates/frontend-design-contract.md +4 -4
  183. package/docs/templates/frontend-implementation-contract.schema.json +34 -2
  184. package/docs/templates/frontend-implementation-dag.json +5 -5
  185. package/package.json +6 -3
  186. package/skills/frontend-contract/SKILL.md +2 -1
  187. package/skills/frontend-contract/references/contract-protocol.md +19 -3
  188. package/skills/frontend-design-review/SKILL.md +12 -11
  189. package/skills/frontend-plan/SKILL.md +9 -2
  190. package/skills/frontend-plan/references/decision-contract.md +18 -5
  191. package/skills/frontend-review/SKILL.md +10 -11
  192. package/skills/frontend-scout/references/scout-evidence.md +4 -0
  193. package/dist/worker/console/static/assets/channel-DAS07MdS.js +0 -1
  194. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cwk-5jiW.js +0 -1
  195. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cwk-5jiW.js +0 -1
  196. package/dist/worker/console/static/assets/index-C0O48S_P.js +0 -449
  197. package/dist/worker/console/static/assets/index-CzKf4U8P.css +0 -1
  198. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyJg5gzW.js +0 -1
  199. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B1cw_0Uy.js +0 -8
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Operator Chat — source control HTTP API (Console UI side).
3
+ *
4
+ * Endpoints (all under /api/operator/v1/chat):
5
+ * GET /scm/status branch + changes + counts
6
+ * GET /scm/diff?path=&staged= unified diff (one path or --stat)
7
+ * POST /scm/stage { paths: string[] } | { all: true }
8
+ * POST /scm/unstage { paths: string[] }
9
+ * POST /scm/discard { paths: string[] } [dangerous]
10
+ * POST /scm/commit { message }
11
+ * GET /scm/log?limit= recent commits
12
+ * GET /scm/branches branch list + current
13
+ * POST /scm/branches { name, create? } switch or create
14
+ * POST /scm/push push to upstream
15
+ * POST /scm/pull pull from upstream
16
+ *
17
+ * Reads are open (same-origin console); every mutation passes gateMutation.
18
+ * The ScmService is workspace-scoped (repoRoot fixed at construction).
19
+ */
20
+ function sendJson(res, status, body) {
21
+ res.writeHead(status, {
22
+ "content-type": "application/json; charset=utf-8",
23
+ "cache-control": "no-store",
24
+ });
25
+ res.end(JSON.stringify(body));
26
+ }
27
+ function mapScmError(res, error) {
28
+ const status = error.code === "NO_PATHS" || error.code === "EMPTY_MESSAGE" || error.code === "INVALID_NAME" || error.code === "INVALID_HASH"
29
+ ? 400
30
+ : error.code === "GIT_TIMEOUT"
31
+ ? 504
32
+ : 500;
33
+ sendJson(res, status, {
34
+ ok: false,
35
+ error: { code: error.code, message: error.message, ...(error.stderr ? { stderr: error.stderr } : {}) },
36
+ });
37
+ }
38
+ async function readJsonBody(req, maxBytes = 256 * 1024) {
39
+ const chunks = [];
40
+ let total = 0;
41
+ for await (const chunk of req) {
42
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
43
+ total += buf.length;
44
+ if (total > maxBytes)
45
+ throw new Error("request body too large");
46
+ chunks.push(buf);
47
+ }
48
+ if (chunks.length === 0)
49
+ return {};
50
+ const raw = Buffer.concat(chunks).toString("utf8");
51
+ if (!raw.trim())
52
+ return {};
53
+ return JSON.parse(raw);
54
+ }
55
+ function parsePaths(body) {
56
+ if (!Array.isArray(body.paths))
57
+ return [];
58
+ return body.paths
59
+ .filter((p) => typeof p === "string" && p.length > 0 && !p.includes("\0"))
60
+ .slice(0, 500);
61
+ }
62
+ /** Route dispatcher; returns true when the request was handled. */
63
+ export async function handleScmRequest(req, res, deps, pathname) {
64
+ if (!pathname.startsWith("/api/operator/v1/chat/scm"))
65
+ return false;
66
+ const method = (req.method ?? "GET").toUpperCase();
67
+ const rest = pathname.slice("/api/operator/v1/chat/scm".length);
68
+ const parts = rest.split("/").filter(Boolean);
69
+ const sub = parts[0] ?? "";
70
+ const url = new URL(req.url ?? "/", "http://localhost");
71
+ const gateFailed = (gate) => {
72
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
73
+ return true;
74
+ };
75
+ try {
76
+ if (method === "GET" && sub === "status" && parts.length === 1) {
77
+ const r = await deps.scm.status();
78
+ if (!r.ok)
79
+ return mapScmError(res, r), true;
80
+ sendJson(res, 200, { ok: true, data: r });
81
+ return true;
82
+ }
83
+ if (method === "GET" && sub === "diff" && parts.length === 1) {
84
+ const path = url.searchParams.get("path") ?? undefined;
85
+ const staged = url.searchParams.get("staged") === "1";
86
+ const r = await deps.scm.diff({ path, staged });
87
+ if (!r.ok)
88
+ return mapScmError(res, r), true;
89
+ sendJson(res, 200, { ok: true, data: r });
90
+ return true;
91
+ }
92
+ if (method === "GET" && sub === "commit-diff" && parts.length === 1) {
93
+ const hash = url.searchParams.get("hash") ?? "";
94
+ if (!hash) {
95
+ sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: "hash is required" } });
96
+ return true;
97
+ }
98
+ const r = await deps.scm.commitDiff(hash);
99
+ if (!r.ok)
100
+ return mapScmError(res, r), true;
101
+ sendJson(res, 200, { ok: true, data: r });
102
+ return true;
103
+ }
104
+ if (method === "GET" && sub === "untracked-diff" && parts.length === 1) {
105
+ const path = url.searchParams.get("path") ?? "";
106
+ if (!path) {
107
+ sendJson(res, 400, { ok: false, error: { code: "NO_PATHS", message: "path is required" } });
108
+ return true;
109
+ }
110
+ const r = await deps.scm.untrackedDiff(path);
111
+ if (!r.ok)
112
+ return mapScmError(res, r), true;
113
+ sendJson(res, 200, { ok: true, data: r });
114
+ return true;
115
+ }
116
+ if (method === "GET" && sub === "log" && parts.length === 1) {
117
+ const limit = Number(url.searchParams.get("limit") ?? 30);
118
+ const r = await deps.scm.log(Number.isFinite(limit) ? Math.min(Math.max(limit, 1), 200) : 30);
119
+ if (!r.ok)
120
+ return mapScmError(res, r), true;
121
+ sendJson(res, 200, { ok: true, data: r });
122
+ return true;
123
+ }
124
+ if (method === "GET" && sub === "branches" && parts.length === 1) {
125
+ const r = await deps.scm.branchList();
126
+ if (!r.ok)
127
+ return mapScmError(res, r), true;
128
+ sendJson(res, 200, { ok: true, data: r });
129
+ return true;
130
+ }
131
+ // Everything below mutates → gate first.
132
+ const gate = deps.gateMutation(req);
133
+ if (!gate.ok)
134
+ return gateFailed(gate);
135
+ if (method === "POST" && sub === "stage" && parts.length === 1) {
136
+ const body = await readJsonBody(req);
137
+ const r = body.all === true
138
+ ? await deps.scm.stage([], true)
139
+ : await deps.scm.stage(parsePaths(body));
140
+ if (!r.ok)
141
+ return mapScmError(res, r), true;
142
+ sendJson(res, 200, { ok: true });
143
+ return true;
144
+ }
145
+ if (method === "POST" && sub === "unstage" && parts.length === 1) {
146
+ const body = await readJsonBody(req);
147
+ const r = await deps.scm.unstage(parsePaths(body));
148
+ if (!r.ok)
149
+ return mapScmError(res, r), true;
150
+ sendJson(res, 200, { ok: true });
151
+ return true;
152
+ }
153
+ if (method === "POST" && sub === "discard" && parts.length === 1) {
154
+ const body = await readJsonBody(req);
155
+ const r = await deps.scm.discard(parsePaths(body));
156
+ if (!r.ok)
157
+ return mapScmError(res, r), true;
158
+ sendJson(res, 200, { ok: true });
159
+ return true;
160
+ }
161
+ if (method === "POST" && sub === "commit" && parts.length === 1) {
162
+ const body = await readJsonBody(req);
163
+ const message = typeof body.message === "string" ? body.message : "";
164
+ const r = await deps.scm.commit(message);
165
+ if (!r.ok)
166
+ return mapScmError(res, r), true;
167
+ sendJson(res, 200, { ok: true, data: r });
168
+ return true;
169
+ }
170
+ if (method === "POST" && sub === "branches" && parts.length === 1) {
171
+ const body = await readJsonBody(req);
172
+ const name = typeof body.name === "string" ? body.name : "";
173
+ if (!name) {
174
+ sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: "name is required" } });
175
+ return true;
176
+ }
177
+ const r = body.create === true
178
+ ? await deps.scm.branchCreate(name)
179
+ : await deps.scm.branchSwitch(name);
180
+ if (!r.ok)
181
+ return mapScmError(res, r), true;
182
+ sendJson(res, 200, { ok: true });
183
+ return true;
184
+ }
185
+ if (method === "POST" && sub === "push" && parts.length === 1) {
186
+ const r = await deps.scm.push();
187
+ if (!r.ok)
188
+ return mapScmError(res, r), true;
189
+ sendJson(res, 200, { ok: true, data: r });
190
+ return true;
191
+ }
192
+ if (method === "POST" && sub === "pull" && parts.length === 1) {
193
+ const r = await deps.scm.pull();
194
+ if (!r.ok)
195
+ return mapScmError(res, r), true;
196
+ sendJson(res, 200, { ok: true, data: r });
197
+ return true;
198
+ }
199
+ sendJson(res, 404, { ok: false, error: { code: "NOT_FOUND", message: `unknown scm route: ${method} ${pathname}` } });
200
+ return true;
201
+ }
202
+ catch (error) {
203
+ if (!res.headersSent) {
204
+ sendJson(res, 500, {
205
+ ok: false,
206
+ error: {
207
+ code: "SCM_INTERNAL",
208
+ message: error instanceof Error ? error.message : String(error),
209
+ },
210
+ });
211
+ }
212
+ else {
213
+ res.end();
214
+ }
215
+ return true;
216
+ }
217
+ }
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Operator Chat — source control service (DSH-style SCM).
3
+ *
4
+ * Workspace-scoped git operations shared by the Console UI panel and the
5
+ * model-facing tools:
6
+ * - status: porcelain + branch + ahead/behind, parsed into typed changes
7
+ * - diff: working tree / staged, per-path or whole tree (bounded)
8
+ * - stage / unstage / discard (path-scoped)
9
+ * - commit (message required), log (bounded), branch list/switch/create
10
+ * - push / pull (remote ops, explicit user/model intent only)
11
+ *
12
+ * All git invocations are fixed-argv execFile (no shell), bounded output and
13
+ * timeout, scoped to the workspace repoRoot. Failures return typed errors
14
+ * instead of throwing raw.
15
+ */
16
+ import { execFile } from "node:child_process";
17
+ import { promisify } from "node:util";
18
+ const execFileAsync = promisify(execFile);
19
+ const GIT_TIMEOUT_MS = 20_000;
20
+ const MAX_OUTPUT_CHARS = 200_000;
21
+ function gitEnv() {
22
+ return {
23
+ ...process.env,
24
+ // Deterministic porcelain output regardless of user locale config.
25
+ GIT_PAGER: "cat",
26
+ LC_ALL: "C",
27
+ };
28
+ }
29
+ export class ScmService {
30
+ repoRoot;
31
+ constructor(repoRoot) {
32
+ this.repoRoot = repoRoot;
33
+ }
34
+ async git(args, opts) {
35
+ try {
36
+ const { stdout } = await execFileAsync("git", args, {
37
+ cwd: this.repoRoot,
38
+ encoding: "utf8",
39
+ maxBuffer: 4 * 1024 * 1024,
40
+ timeout: opts?.timeoutMs ?? GIT_TIMEOUT_MS,
41
+ windowsHide: true,
42
+ env: gitEnv(),
43
+ });
44
+ const cap = opts?.maxOutputChars ?? MAX_OUTPUT_CHARS;
45
+ return { ok: true, stdout: stdout.length > cap ? stdout.slice(0, cap) : stdout };
46
+ }
47
+ catch (e) {
48
+ const err = e;
49
+ return {
50
+ ok: false,
51
+ code: err.killed ? "GIT_TIMEOUT" : "GIT_FAILED",
52
+ message: err.killed
53
+ ? `git ${args[0]} timed out`
54
+ : err.message ?? String(e),
55
+ stderr: typeof err.stderr === "string" ? err.stderr.slice(0, 2000) : undefined,
56
+ // `git diff --no-index` exits 1 when a diff exists; callers that
57
+ // expect that carry the partial stdout through.
58
+ stdout: typeof err.stdout === "string" ? err.stdout : undefined,
59
+ };
60
+ }
61
+ }
62
+ /** `git rev-parse --is-inside-work-tree` — cheap repo probe. */
63
+ async isRepo() {
64
+ const r = await this.git(["rev-parse", "--is-inside-work-tree"], { timeoutMs: 5_000 });
65
+ return r.ok && r.stdout.trim() === "true";
66
+ }
67
+ async status() {
68
+ // -uall: expand untracked directories to individual files (VS Code style);
69
+ // renames surface as R rows (with fromPath) only when git's rename
70
+ // detection is active, which is the default for status since git 2.9.
71
+ const porcelain = await this.git([
72
+ "status", "--porcelain=v1", "-z", "--branch", "-uall",
73
+ ]);
74
+ if (!porcelain.ok)
75
+ return porcelain;
76
+ const changes = [];
77
+ // porcelain v1 -z: NUL-separated fields. Header is
78
+ // "## branch...upstream [ahead N, behind M]"; status rows "XY path";
79
+ // rename/copy rows "XY newpath" followed by a bare "oldpath" field.
80
+ const fields = porcelain.stdout.split("\0").filter((s) => s !== "");
81
+ let branch = "";
82
+ let upstream;
83
+ let ahead;
84
+ let behind;
85
+ for (const field of fields) {
86
+ if (field.startsWith("## ")) {
87
+ const header = field.slice(3);
88
+ const track = header.split(/\s+/)[0] ?? "";
89
+ const dot = track.indexOf("...");
90
+ if (dot >= 0) {
91
+ branch = track.slice(0, dot);
92
+ upstream = track.slice(dot + 3);
93
+ }
94
+ else {
95
+ branch = track;
96
+ }
97
+ const aheadM = /\[ahead (\d+)/.exec(header);
98
+ const behindM = /behind (\d+)\]/.exec(header);
99
+ if (aheadM)
100
+ ahead = Number(aheadM[1]);
101
+ if (behindM)
102
+ behind = Number(behindM[1]);
103
+ continue;
104
+ }
105
+ if (field.startsWith("#"))
106
+ continue;
107
+ // Bare path field: from-path of the previous rename/copy row.
108
+ const prev = changes[changes.length - 1];
109
+ if (prev && prev.xy.match(/[RC]/) && !prev.fromPath && !/^[ MADRCU?!]{1,2} /.test(field)) {
110
+ prev.fromPath = field;
111
+ continue;
112
+ }
113
+ const xy = field.slice(0, 2);
114
+ const path = field.slice(3);
115
+ changes.push({
116
+ path,
117
+ xy,
118
+ staged: xy[0] !== " " && xy[0] !== "?" && xy[0] !== "!",
119
+ unstaged: xy[1] !== " " && xy[1] !== "?",
120
+ untracked: xy === "??",
121
+ });
122
+ }
123
+ const head = await this.git(["rev-parse", "--short", "HEAD"], { timeoutMs: 5_000 });
124
+ return {
125
+ ok: true,
126
+ repoRoot: this.repoRoot,
127
+ branch,
128
+ head: head.ok ? head.stdout.trim() : "",
129
+ upstream,
130
+ ahead,
131
+ behind,
132
+ changes,
133
+ stagedCount: changes.filter((c) => c.staged).length,
134
+ unstagedCount: changes.filter((c) => c.unstaged).length,
135
+ untrackedCount: changes.filter((c) => c.untracked).length,
136
+ };
137
+ }
138
+ /** Unified diff for one path (or whole tree when path omitted). */
139
+ async diff(options) {
140
+ const args = ["diff"];
141
+ if (options.staged)
142
+ args.push("--cached");
143
+ args.push(`-U${options.contextLines ?? 3}`);
144
+ if (options.path) {
145
+ args.push("--", options.path);
146
+ }
147
+ else {
148
+ args.push("--stat");
149
+ }
150
+ const r = await this.git(args);
151
+ if (!r.ok)
152
+ return r;
153
+ return { ok: true, diff: r.stdout };
154
+ }
155
+ /** Full patch of one commit (`git show` without the commit header). */
156
+ async commitDiff(hash) {
157
+ if (!/^[0-9a-f]{4,40}$/i.test(hash)) {
158
+ return { ok: false, code: "INVALID_HASH", message: "invalid commit hash" };
159
+ }
160
+ const r = await this.git(["show", "--format=", "--no-color", hash]);
161
+ if (!r.ok)
162
+ return r;
163
+ return { ok: true, diff: r.stdout };
164
+ }
165
+ /**
166
+ * Untracked-file content as a full-addition diff (`git diff --no-index
167
+ * /dev/null <path>`): git diff never lists untracked files, and this
168
+ * renders them like any other change in the diff view.
169
+ */
170
+ async untrackedDiff(path) {
171
+ if (path.includes("\0") || path.includes("..")) {
172
+ return { ok: false, code: "NO_PATHS", message: "invalid path" };
173
+ }
174
+ const r = await this.git(["diff", "--no-index", "--no-color", "-U3", "--", "/dev/null", path]);
175
+ // `git diff --no-index` exits 1 when there IS a diff — carry stdout.
176
+ if (!r.ok && r.code === "GIT_FAILED")
177
+ return { ok: true, diff: r.stdout ?? "" };
178
+ if (!r.ok)
179
+ return r;
180
+ return { ok: true, diff: r.stdout };
181
+ }
182
+ /** Stage paths (`git add --` / `git add -A --` for "."). */
183
+ async stage(paths, all = false) {
184
+ if (!all && paths.length === 0) {
185
+ return { ok: false, code: "NO_PATHS", message: "stage requires paths or all=true" };
186
+ }
187
+ const args = ["add"];
188
+ if (all) {
189
+ args.push("-A");
190
+ }
191
+ else {
192
+ args.push("--");
193
+ args.push(...paths);
194
+ }
195
+ return this.git(args);
196
+ }
197
+ /** Unstage paths (`git reset HEAD --`). */
198
+ async unstage(paths) {
199
+ if (paths.length === 0) {
200
+ return { ok: false, code: "NO_PATHS", message: "unstage requires paths" };
201
+ }
202
+ return this.git(["reset", "HEAD", "--", ...paths]);
203
+ }
204
+ /** Discard worktree changes for paths (`git checkout --`). DANGEROUS. */
205
+ async discard(paths) {
206
+ if (paths.length === 0) {
207
+ return { ok: false, code: "NO_PATHS", message: "discard requires paths" };
208
+ }
209
+ return this.git(["checkout", "--", ...paths]);
210
+ }
211
+ async commit(message) {
212
+ const trimmed = message.trim();
213
+ if (!trimmed) {
214
+ return { ok: false, code: "EMPTY_MESSAGE", message: "commit message required" };
215
+ }
216
+ const r = await this.git(["commit", "-m", trimmed]);
217
+ if (!r.ok)
218
+ return r;
219
+ const sha = await this.git(["rev-parse", "--short", "HEAD"], { timeoutMs: 5_000 });
220
+ return { ok: true, commit: sha.ok ? sha.stdout.trim() : "" };
221
+ }
222
+ async log(limit = 30) {
223
+ const r = await this.git([
224
+ "log",
225
+ `--max-count=${limit}`,
226
+ "--pretty=format:%H\u0001%h\u0001%s\u0001%an\u0001%aI\u0001%D",
227
+ ]);
228
+ if (!r.ok)
229
+ return r;
230
+ const commits = [];
231
+ for (const line of r.stdout.split("\n")) {
232
+ if (!line.trim())
233
+ continue;
234
+ const [sha, short, subject, author, date, refs] = line.split("\u0001");
235
+ if (!sha)
236
+ continue;
237
+ commits.push({
238
+ sha: short ?? sha,
239
+ subject: subject ?? "",
240
+ author: author ?? "",
241
+ date: date ?? "",
242
+ refs: refs?.trim() || undefined,
243
+ });
244
+ }
245
+ return { ok: true, commits };
246
+ }
247
+ async branchList() {
248
+ const r = await this.git(["branch", "--list", "--format=%(HEAD)%02%(refname:short)"]);
249
+ if (!r.ok)
250
+ return r;
251
+ const branches = [];
252
+ for (const line of r.stdout.split("\n")) {
253
+ if (!line.trim())
254
+ continue;
255
+ const [headMark, name] = line.split("\x02");
256
+ if (!name)
257
+ continue;
258
+ branches.push({ name: name.trim(), current: headMark?.trim() === "*" });
259
+ }
260
+ const current = branches.find((b) => b.current)?.name ?? "";
261
+ return { ok: true, current, branches };
262
+ }
263
+ async branchCreate(name) {
264
+ if (!/^[\w.\-/]+$/.test(name)) {
265
+ return { ok: false, code: "INVALID_NAME", message: `invalid branch name: ${name}` };
266
+ }
267
+ return this.git(["checkout", "-b", name]);
268
+ }
269
+ async branchSwitch(name) {
270
+ if (!/^[\w.\-/]+$/.test(name)) {
271
+ return { ok: false, code: "INVALID_NAME", message: `invalid branch name: ${name}` };
272
+ }
273
+ return this.git(["checkout", name]);
274
+ }
275
+ async push() {
276
+ const r = await this.git(["push"], { timeoutMs: 120_000 });
277
+ return r.ok ? { ok: true, output: r.stdout } : r;
278
+ }
279
+ async pull() {
280
+ const r = await this.git(["pull", "--no-edit"], { timeoutMs: 120_000 });
281
+ return r.ok ? { ok: true, output: r.stdout } : r;
282
+ }
283
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Operator Chat — model-facing source control tool (DSH-style SCM).
3
+ *
4
+ * Registers `scm` (single tool, multi-action) so the model can inspect and
5
+ * drive git in the workspace repo: status / diff / stage / unstage / commit /
6
+ * log / branch / push / pull. Read actions are cheap and safe; write actions
7
+ * (commit/push) are the model's explicit intent, mirroring DSH's SCM tool.
8
+ *
9
+ * All operations go through ScmService (fixed-argv git, no shell, bounded),
10
+ * scoped to the workspace repoRoot.
11
+ */
12
+ const TOOL_DESCRIPTION = "Inspect and drive git source control in the workspace repo. " +
13
+ "Actions: 'status' (branch + working-tree changes), 'diff' (one path or whole tree; staged=1 for the index), " +
14
+ "'stage'/'unstage' (paths array; all=true stages everything), 'discard' (DESTRUCTIVE: revert worktree changes of paths), " +
15
+ "'commit' (message required), 'log' (recent commits), 'branch' (list; name+create to create/switch), 'push', 'pull'. " +
16
+ "Use status/diff before proposing commits; prefer committing focused changesets. push/pull contact the remote — call them only when the user asks.";
17
+ export async function buildScmTools(deps) {
18
+ const [{ Type }, { defineTool }] = await Promise.all([
19
+ import("typebox"),
20
+ import("@earendil-works/pi-coding-agent"),
21
+ ]);
22
+ function result(body) {
23
+ return { content: [{ type: "text", text: JSON.stringify(body) }], details: body };
24
+ }
25
+ function errText(e) {
26
+ return JSON.stringify({ ok: false, code: e.code, message: e.message, ...(e.stderr ? { stderr: e.stderr } : {}) });
27
+ }
28
+ const tool = defineTool({
29
+ name: "scm",
30
+ label: "SCM",
31
+ description: TOOL_DESCRIPTION,
32
+ promptSnippet: "scm: git source control (status/diff/stage/unstage/discard/commit/log/branch/push/pull) for the workspace repo.",
33
+ parameters: Type.Object({
34
+ action: Type.Union([
35
+ Type.Literal("status"),
36
+ Type.Literal("diff"),
37
+ Type.Literal("stage"),
38
+ Type.Literal("unstage"),
39
+ Type.Literal("discard"),
40
+ Type.Literal("commit"),
41
+ Type.Literal("log"),
42
+ Type.Literal("branch"),
43
+ Type.Literal("push"),
44
+ Type.Literal("pull"),
45
+ ], { description: "Which git operation to run." }),
46
+ path: Type.Optional(Type.String({ description: "For diff: repo-relative path (default whole tree --stat)." })),
47
+ staged: Type.Optional(Type.Boolean({ description: "For diff: diff the staging area (index) instead of the worktree." })),
48
+ paths: Type.Optional(Type.Array(Type.String(), { description: "For stage/unstage/discard: repo-relative paths." })),
49
+ all: Type.Optional(Type.Boolean({ description: "For stage: stage all changes (git add -A)." })),
50
+ message: Type.Optional(Type.String({ description: "For commit: commit message (required)." })),
51
+ limit: Type.Optional(Type.Number({ description: "For log: max commits (default 30)." })),
52
+ name: Type.Optional(Type.String({ description: "For branch: branch name to create or switch to." })),
53
+ create: Type.Optional(Type.Boolean({ description: "For branch: create the branch instead of switching." })),
54
+ }, { additionalProperties: false }),
55
+ async execute(_toolCallId, params) {
56
+ const paths = Array.isArray(params.paths) ? params.paths.slice(0, 500) : [];
57
+ switch (params.action) {
58
+ case "status":
59
+ return result(await deps.scm.status());
60
+ case "diff": {
61
+ const r = await deps.scm.diff({
62
+ ...(params.path ? { path: params.path } : {}),
63
+ ...(params.staged ? { staged: true } : {}),
64
+ });
65
+ return r.ok ? result(r) : result({ error: errText(r) });
66
+ }
67
+ case "stage": {
68
+ const r = params.all === true ? await deps.scm.stage([], true) : await deps.scm.stage(paths);
69
+ return r.ok ? result({ ok: true, staged: params.all === true ? "all" : paths }) : result({ error: errText(r) });
70
+ }
71
+ case "unstage": {
72
+ const r = await deps.scm.unstage(paths);
73
+ return r.ok ? result({ ok: true, unstaged: paths }) : result({ error: errText(r) });
74
+ }
75
+ case "discard": {
76
+ const r = await deps.scm.discard(paths);
77
+ return r.ok ? result({ ok: true, discarded: paths }) : result({ error: errText(r) });
78
+ }
79
+ case "commit": {
80
+ const r = await deps.scm.commit(params.message ?? "");
81
+ return r.ok ? result(r) : result({ error: errText(r) });
82
+ }
83
+ case "log": {
84
+ const r = await deps.scm.log(params.limit ?? 30);
85
+ return r.ok ? result(r) : result({ error: errText(r) });
86
+ }
87
+ case "branch": {
88
+ if (params.name) {
89
+ const r = params.create === true
90
+ ? await deps.scm.branchCreate(params.name)
91
+ : await deps.scm.branchSwitch(params.name);
92
+ return r.ok ? result({ ok: true, ...(params.create === true ? { created: params.name } : { switched: params.name }) }) : result({ error: errText(r) });
93
+ }
94
+ const r = await deps.scm.branchList();
95
+ return r.ok ? result(r) : result({ error: errText(r) });
96
+ }
97
+ case "push": {
98
+ const r = await deps.scm.push();
99
+ return r.ok ? result(r) : result({ error: errText(r) });
100
+ }
101
+ case "pull": {
102
+ const r = await deps.scm.pull();
103
+ return r.ok ? result(r) : result({ error: errText(r) });
104
+ }
105
+ default:
106
+ return result({ error: `unknown action: ${params.action}` });
107
+ }
108
+ },
109
+ });
110
+ return [tool];
111
+ }