@tea-agent/loop-agent 0.43.0-next.3 → 0.43.0-next.5

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 (109) hide show
  1. package/CHANGELOG.md +81 -9
  2. package/dist/application/task-lifecycle/advance.js +17 -4
  3. package/dist/application/task-lifecycle/observe.js +43 -29
  4. package/dist/application/task-lifecycle/plan-transitions.js +5 -4
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/executors/dag-pi-executor.js +244 -33
  7. package/dist/executors/pi-executor.js +2 -0
  8. package/dist/executors/pi-sdk-executor.js +75 -39
  9. package/dist/task/source-prepare/source-fidelity-pi.js +2 -2
  10. package/dist/task/source-references.js +48 -23
  11. package/dist/worker/console/chat/assistant-content.js +23 -2
  12. package/dist/worker/console/chat/explore-tools.js +13 -0
  13. package/dist/worker/console/chat/pi-runtime.js +131 -95
  14. package/dist/worker/console/chat/routes.js +175 -54
  15. package/dist/worker/console/chat/scm-routes.js +193 -0
  16. package/dist/worker/console/chat/scm-service.js +253 -0
  17. package/dist/worker/console/chat/scm-tools.js +111 -0
  18. package/dist/worker/console/chat/sdd-data-alignment.js +65 -9
  19. package/dist/worker/console/chat/session-catalog.js +1 -0
  20. package/dist/worker/console/chat/session-mode-view.js +48 -0
  21. package/dist/worker/console/chat/session-mode.js +218 -0
  22. package/dist/worker/console/chat/session-store.js +22 -6
  23. package/dist/worker/console/chat/shortcuts.js +6 -0
  24. package/dist/worker/console/chat/terminal-routes.js +216 -0
  25. package/dist/worker/console/chat/terminal-sessions.js +284 -0
  26. package/dist/worker/console/chat/terminal-tools.js +199 -0
  27. package/dist/worker/console/chat/tool-preview.js +11 -0
  28. package/dist/worker/console/chat/tools.js +2 -0
  29. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-hUmM_GRf.js → abnfDiagram-N423BO3Z-BuyrlONK.js} +1 -1
  30. package/dist/worker/console/static/assets/{arc-Bt-xuieW.js → arc-BvCl_wpi.js} +1 -1
  31. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DTqkL4DJ.js → architectureDiagram-T3A2C74G-BkBpBaRq.js} +1 -1
  32. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Cy0MNnzV.js → blockDiagram-VBNYF7ZC-yve4Mcb_.js} +1 -1
  33. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DDdlT0Pd.js → c4Diagram-5PPSVZJV-9yLGboBx.js} +1 -1
  34. package/dist/worker/console/static/assets/channel-BRahIhjw.js +1 -0
  35. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-3JawwNZ6.js → chunk-2GRJ4B5K-BKP69b8r.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DoTp7Cri.js → chunk-2Q5K7J3B-NWFRnXju.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CcirhZaS.js → chunk-5RXB4S5H-BiweqLEc.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-VrG21gnP.js → chunk-5VM5RSS4-CP9GHmRY.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DDOgIS70.js → chunk-6Q2QTUOP-BiGDkBtj.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CBjmgvJ_.js → chunk-GF5L2VYU-DkJ4ZjQS.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CCQSht8M.js → chunk-JWPE2WC7-B5jjn7zq.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BpGMkUUV.js → chunk-KBJHAD2P-C6KoK9O0.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DNDT41QL.js → chunk-RYQCIY6F-BvNIfyn-.js} +1 -1
  44. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-euXnDFbn.js → chunk-XXDRQBXY-BZ33iX4W.js} +1 -1
  45. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-GOC80mhq.js +1 -0
  46. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-GOC80mhq.js +1 -0
  47. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BJ6NAeql.js → cose-bilkent-JH36ORCC-CP8eIuZd.js} +1 -1
  48. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-F5JKIvIq.js → cynefin-VYW2F7L2-DZECjpOe.js} +1 -1
  49. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Cwmkl_Gi.js → cynefinDiagram-MW4NZA55-CvVJoFqf.js} +1 -1
  50. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BmCZ8815.js → dagre-VZM6K2ZE-D_JTqdAn.js} +1 -1
  51. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zWA3hUnX.js → diagram-7IWD3JNH-Db7AQ0z0.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BzA-888F.js → diagram-B4RE2ZJO-AZPVo5CZ.js} +1 -1
  53. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-tPNheoch.js → diagram-LBJQPF4R-BgXqI3E6.js} +1 -1
  54. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Cdi7Fyqp.js → diagram-Q27KOJAE-BYjFn3Ba.js} +1 -1
  55. package/dist/worker/console/static/assets/{diagram-UB23O5K3-DFK9VBf1.js → diagram-UB23O5K3-B4rJgGGO.js} +1 -1
  56. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-xCUr1kpg.js → ebnfDiagram-BXEA7PRR-C6XWZhpw.js} +1 -1
  57. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BANQHY_A.js → erDiagram-JOGREHBK-1YdQRIQW.js} +1 -1
  58. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-VHJYEMkP.js → flowDiagram-UKHOOZJN-l3a0AwN0.js} +1 -1
  59. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiyZQ0C6.js → ganttDiagram-PKOTCBZU-CQL96F2Z.js} +1 -1
  60. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CZtIeBBa.js → gitGraphDiagram-DS77QQ5N-BxCvafRb.js} +1 -1
  61. package/dist/worker/console/static/assets/index-4wCGsXuU.js +465 -0
  62. package/dist/worker/console/static/assets/index-CiF9WBoR.css +1 -0
  63. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BuSspC0X.js → infoDiagram-6WML65LV-BUipIeDO.js} +1 -1
  64. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DRJZyPFy.js → ishikawaDiagram-WSZJBQD7-CRfomVMX.js} +1 -1
  65. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DIfXIVuE.js → journeyDiagram-NVQOT4AX-_UFvyviX.js} +1 -1
  66. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CT10_7Lt.js → kanban-definition-27J2QSJJ-CKIRoqKv.js} +1 -1
  67. package/dist/worker/console/static/assets/{linear-ufemj74H.js → linear-CjttJDCM.js} +1 -1
  68. package/dist/worker/console/static/assets/{mermaid.core-DEQe8-_m.js → mermaid.core-DPvHX155.js} +5 -5
  69. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BRLUrbrC.js → mindmap-definition-FAOFIHXS-Qmj-qwBe.js} +1 -1
  70. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BgibKtiF.js → pegDiagram-VL7TDLO6-Auw6DRgq.js} +1 -1
  71. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-D3eyfdV9.js → pieDiagram-7S7Q4E2Y-CqMrfD4c.js} +1 -1
  72. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DXeTk5Px.js → quadrantDiagram-CIZ2JOQS-BAddeggH.js} +1 -1
  73. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BgXdFEe7.js → railroadDiagram-AXF67PYL-DG1JuOu2.js} +1 -1
  74. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CseC2A7R.js → requirementDiagram-LRYGKXZP-DLYGMKlC.js} +1 -1
  75. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-D80PFzfJ.js → sankeyDiagram-W5VNT64P-g68SbLM9.js} +1 -1
  76. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BWzHW7LS.js → sequenceDiagram-SI44F4Z6-CmszLKv8.js} +1 -1
  77. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-xWQYTLAn.js → sizeCapture-X5ZJPWSS-D2tEKUCj.js} +1 -1
  78. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CLxtpZeL.js → stateDiagram-OKZ733FA-Syc5E_Jf.js} +1 -1
  79. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CwWwuPI-.js +1 -0
  80. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DwugtmWV.js → swimlanes-SLNWSIFB-Ibn0IRCa.js} +2 -2
  81. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-A7fqemr6.js +8 -0
  82. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DHEOwHIe.js → timeline-definition-Z64GVDOM-2ZW9W3iS.js} +1 -1
  83. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-pmbZKlBK.js → vennDiagram-T6HMQDX7-v50HsKUh.js} +1 -1
  84. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CivHoM5q.js → wardleyDiagram-T6FBY63Y-DqCXEoDH.js} +1 -1
  85. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BfHIE7o6.js → xychartDiagram-ELKLHX3M-vIP8Vo9d.js} +1 -1
  86. package/dist/worker/console/static/index.html +2 -2
  87. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +122 -10
  88. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +27 -15
  89. package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
  90. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
  91. package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
  92. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +20 -4
  93. package/dist/worker/console/static-src/operator-chat/useChatStream.js +18 -2
  94. package/dist/worker/console/static-src/operator-chat/useChatThread.js +39 -4
  95. package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -7
  96. package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
  97. package/dist/worker/console/workspace-context.js +15 -1
  98. package/dist/workflows/dag/frontend-capacity.js +9 -0
  99. package/dist/workflows/dag/frontend-design-policy.js +2 -1
  100. package/dist/workflows/dag/frontend-shape-facts.js +12 -2
  101. package/package.json +6 -3
  102. package/skills/frontend-plan/SKILL.md +7 -0
  103. package/dist/worker/console/static/assets/channel-BpzRnZmU.js +0 -1
  104. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlMVqCc.js +0 -1
  105. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlMVqCc.js +0 -1
  106. package/dist/worker/console/static/assets/index-BlN6ytLr.css +0 -1
  107. package/dist/worker/console/static/assets/index-CA-jfWlE.js +0 -449
  108. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CiVoq4G-.js +0 -1
  109. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BkdgdpfR.js +0 -8
@@ -0,0 +1,253 @@
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
+ };
57
+ }
58
+ }
59
+ /** `git rev-parse --is-inside-work-tree` — cheap repo probe. */
60
+ async isRepo() {
61
+ const r = await this.git(["rev-parse", "--is-inside-work-tree"], { timeoutMs: 5_000 });
62
+ return r.ok && r.stdout.trim() === "true";
63
+ }
64
+ async status() {
65
+ // -uall: expand untracked directories to individual files (VS Code style);
66
+ // renames surface as R rows (with fromPath) only when git's rename
67
+ // detection is active, which is the default for status since git 2.9.
68
+ const porcelain = await this.git([
69
+ "status", "--porcelain=v1", "-z", "--branch", "-uall",
70
+ ]);
71
+ if (!porcelain.ok)
72
+ return porcelain;
73
+ const changes = [];
74
+ // porcelain v1 -z: NUL-separated fields. Header is
75
+ // "## branch...upstream [ahead N, behind M]"; status rows "XY path";
76
+ // rename/copy rows "XY newpath" followed by a bare "oldpath" field.
77
+ const fields = porcelain.stdout.split("\0").filter((s) => s !== "");
78
+ let branch = "";
79
+ let upstream;
80
+ let ahead;
81
+ let behind;
82
+ for (const field of fields) {
83
+ if (field.startsWith("## ")) {
84
+ const header = field.slice(3);
85
+ const track = header.split(/\s+/)[0] ?? "";
86
+ const dot = track.indexOf("...");
87
+ if (dot >= 0) {
88
+ branch = track.slice(0, dot);
89
+ upstream = track.slice(dot + 3);
90
+ }
91
+ else {
92
+ branch = track;
93
+ }
94
+ const aheadM = /\[ahead (\d+)/.exec(header);
95
+ const behindM = /behind (\d+)\]/.exec(header);
96
+ if (aheadM)
97
+ ahead = Number(aheadM[1]);
98
+ if (behindM)
99
+ behind = Number(behindM[1]);
100
+ continue;
101
+ }
102
+ if (field.startsWith("#"))
103
+ continue;
104
+ // Bare path field: from-path of the previous rename/copy row.
105
+ const prev = changes[changes.length - 1];
106
+ if (prev && prev.xy.match(/[RC]/) && !prev.fromPath && !/^[ MADRCU?!]{1,2} /.test(field)) {
107
+ prev.fromPath = field;
108
+ continue;
109
+ }
110
+ const xy = field.slice(0, 2);
111
+ const path = field.slice(3);
112
+ changes.push({
113
+ path,
114
+ xy,
115
+ staged: xy[0] !== " " && xy[0] !== "?" && xy[0] !== "!",
116
+ unstaged: xy[1] !== " " && xy[1] !== "?",
117
+ untracked: xy === "??",
118
+ });
119
+ }
120
+ const head = await this.git(["rev-parse", "--short", "HEAD"], { timeoutMs: 5_000 });
121
+ return {
122
+ ok: true,
123
+ repoRoot: this.repoRoot,
124
+ branch,
125
+ head: head.ok ? head.stdout.trim() : "",
126
+ upstream,
127
+ ahead,
128
+ behind,
129
+ changes,
130
+ stagedCount: changes.filter((c) => c.staged).length,
131
+ unstagedCount: changes.filter((c) => c.unstaged).length,
132
+ untrackedCount: changes.filter((c) => c.untracked).length,
133
+ };
134
+ }
135
+ /** Unified diff for one path (or whole tree when path omitted). */
136
+ async diff(options) {
137
+ const args = ["diff"];
138
+ if (options.staged)
139
+ args.push("--cached");
140
+ args.push(`-U${options.contextLines ?? 3}`);
141
+ if (options.path) {
142
+ args.push("--", options.path);
143
+ }
144
+ else {
145
+ args.push("--stat");
146
+ }
147
+ const r = await this.git(args);
148
+ if (!r.ok)
149
+ return r;
150
+ return { ok: true, diff: r.stdout };
151
+ }
152
+ /** Stage paths (`git add --` / `git add -A --` for "."). */
153
+ async stage(paths, all = false) {
154
+ if (!all && paths.length === 0) {
155
+ return { ok: false, code: "NO_PATHS", message: "stage requires paths or all=true" };
156
+ }
157
+ const args = ["add"];
158
+ if (all) {
159
+ args.push("-A");
160
+ }
161
+ else {
162
+ args.push("--");
163
+ args.push(...paths);
164
+ }
165
+ return this.git(args);
166
+ }
167
+ /** Unstage paths (`git reset HEAD --`). */
168
+ async unstage(paths) {
169
+ if (paths.length === 0) {
170
+ return { ok: false, code: "NO_PATHS", message: "unstage requires paths" };
171
+ }
172
+ return this.git(["reset", "HEAD", "--", ...paths]);
173
+ }
174
+ /** Discard worktree changes for paths (`git checkout --`). DANGEROUS. */
175
+ async discard(paths) {
176
+ if (paths.length === 0) {
177
+ return { ok: false, code: "NO_PATHS", message: "discard requires paths" };
178
+ }
179
+ return this.git(["checkout", "--", ...paths]);
180
+ }
181
+ async commit(message) {
182
+ const trimmed = message.trim();
183
+ if (!trimmed) {
184
+ return { ok: false, code: "EMPTY_MESSAGE", message: "commit message required" };
185
+ }
186
+ const r = await this.git(["commit", "-m", trimmed]);
187
+ if (!r.ok)
188
+ return r;
189
+ const sha = await this.git(["rev-parse", "--short", "HEAD"], { timeoutMs: 5_000 });
190
+ return { ok: true, commit: sha.ok ? sha.stdout.trim() : "" };
191
+ }
192
+ async log(limit = 30) {
193
+ const r = await this.git([
194
+ "log",
195
+ `--max-count=${limit}`,
196
+ "--pretty=format:%H\u0001%h\u0001%s\u0001%an\u0001%aI\u0001%D",
197
+ ]);
198
+ if (!r.ok)
199
+ return r;
200
+ const commits = [];
201
+ for (const line of r.stdout.split("\n")) {
202
+ if (!line.trim())
203
+ continue;
204
+ const [sha, short, subject, author, date, refs] = line.split("\u0001");
205
+ if (!sha)
206
+ continue;
207
+ commits.push({
208
+ sha: short ?? sha,
209
+ subject: subject ?? "",
210
+ author: author ?? "",
211
+ date: date ?? "",
212
+ refs: refs?.trim() || undefined,
213
+ });
214
+ }
215
+ return { ok: true, commits };
216
+ }
217
+ async branchList() {
218
+ const r = await this.git(["branch", "--list", "--format=%(HEAD)%02%(refname:short)"]);
219
+ if (!r.ok)
220
+ return r;
221
+ const branches = [];
222
+ for (const line of r.stdout.split("\n")) {
223
+ if (!line.trim())
224
+ continue;
225
+ const [headMark, name] = line.split("\x02");
226
+ if (!name)
227
+ continue;
228
+ branches.push({ name: name.trim(), current: headMark?.trim() === "*" });
229
+ }
230
+ const current = branches.find((b) => b.current)?.name ?? "";
231
+ return { ok: true, current, branches };
232
+ }
233
+ async branchCreate(name) {
234
+ if (!/^[\w.\-/]+$/.test(name)) {
235
+ return { ok: false, code: "INVALID_NAME", message: `invalid branch name: ${name}` };
236
+ }
237
+ return this.git(["checkout", "-b", name]);
238
+ }
239
+ async branchSwitch(name) {
240
+ if (!/^[\w.\-/]+$/.test(name)) {
241
+ return { ok: false, code: "INVALID_NAME", message: `invalid branch name: ${name}` };
242
+ }
243
+ return this.git(["checkout", name]);
244
+ }
245
+ async push() {
246
+ const r = await this.git(["push"], { timeoutMs: 120_000 });
247
+ return r.ok ? { ok: true, output: r.stdout } : r;
248
+ }
249
+ async pull() {
250
+ const r = await this.git(["pull", "--no-edit"], { timeoutMs: 120_000 });
251
+ return r.ok ? { ok: true, output: r.stdout } : r;
252
+ }
253
+ }
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- import { randomBytes } from "node:crypto";
1
+ import { createDecipheriv, randomBytes } from "node:crypto";
2
2
  import { readFile, stat } from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
@@ -10,8 +10,49 @@ export const SDD_SKILL_NAMES = [
10
10
  /** Backward-compatible name for callers that need a stable representative value. */
11
11
  export const SDD_DEFAULT_SKILL_NAME = "SDD-implementation-test-review";
12
12
  export const SDD_TELEMETRY_URL = "https://wizard-ai.paic.com.cn/code_pilot/api/skill-usage-log";
13
- export const SDD_TELEMETRY_SIGN = "52aad0c09e6b46818a11ee702d6fe0ba";
13
+ /** SM4 key hex; also the SDD telemetry `ws-sign` value. */
14
+ export const DEFAULT_AI_MA_PUB_SALT_KEY = "52aad0c09e6b46818a11ee702d6fe0ba";
15
+ export const SDD_TELEMETRY_SIGN = DEFAULT_AI_MA_PUB_SALT_KEY;
16
+ export const AIMAX_AUTH_TOKEN_ENV = "AIMAX_AUTH_TOKEN";
14
17
  export const WIZARD_LOCAL_DEFAULT_USER_AGENT = "Builder/OpenCode ai-sdk/provider-utils/4.0.21 runtime/bun/1.3.11";
18
+ const SM4_V1_PREFIX = "sm4:v1:";
19
+ /** Decode a plaintext ticket or `sm4:v1:` ciphertext. Unknown prefixes and crypto errors fail closed. */
20
+ export function decodeAimaxAuthToken(raw, keyHex = DEFAULT_AI_MA_PUB_SALT_KEY) {
21
+ const candidate = raw.trim();
22
+ if (!candidate)
23
+ return undefined;
24
+ if (!candidate.startsWith("sm4:"))
25
+ return candidate;
26
+ if (!candidate.startsWith(SM4_V1_PREFIX))
27
+ return undefined;
28
+ const hex = candidate.slice(SM4_V1_PREFIX.length);
29
+ if (!hex || hex.length % 2 !== 0 || /[^0-9a-fA-F]/.test(hex))
30
+ return undefined;
31
+ const key = Buffer.from(keyHex.trim(), "hex");
32
+ if (key.length !== 16)
33
+ return undefined;
34
+ try {
35
+ const encrypted = Buffer.from(hex, "hex");
36
+ if (encrypted.length === 0 || encrypted.length % 16 !== 0)
37
+ return undefined;
38
+ const decipher = createDecipheriv("sm4-ecb", key, null);
39
+ decipher.setAutoPadding(true);
40
+ const plain = Buffer.concat([decipher.update(encrypted), decipher.final()])
41
+ .toString("utf8")
42
+ .trim();
43
+ return plain || undefined;
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ }
49
+ /** Set or delete `AIMAX_AUTH_TOKEN`. Empty string is treated as absent. */
50
+ export function syncAimaxAuthTokenEnv(env, token) {
51
+ if (token)
52
+ env[AIMAX_AUTH_TOKEN_ENV] = token;
53
+ else
54
+ delete env[AIMAX_AUTH_TOKEN_ENV];
55
+ }
15
56
  function sameAuthFileVersion(left, right) {
16
57
  if (!left.exists || !right.exists)
17
58
  return left.exists === right.exists;
@@ -21,9 +62,22 @@ function sameAuthFileVersion(left, right) {
21
62
  /** Reads ~/.aima-go/auth.json with metadata-based parse caching and fail-closed semantics. */
22
63
  export class AimaxAuthReader {
23
64
  authFile;
65
+ env;
24
66
  cached;
25
- constructor(authFile = path.join(os.homedir(), ".aima-go", "auth.json")) {
67
+ constructor(authFile = path.join(os.homedir(), ".aima-go", "auth.json"), env = process.env) {
26
68
  this.authFile = authFile;
69
+ this.env = env;
70
+ }
71
+ resolveKeyHex() {
72
+ const injected = typeof this.env.AI_MA_PUB_SALT_KEY === "string" ? this.env.AI_MA_PUB_SALT_KEY.trim() : "";
73
+ if (injected)
74
+ return injected;
75
+ const host = process.env.AI_MA_PUB_SALT_KEY?.trim();
76
+ return host || DEFAULT_AI_MA_PUB_SALT_KEY;
77
+ }
78
+ finish(token) {
79
+ syncAimaxAuthTokenEnv(this.env, token);
80
+ return token;
27
81
  }
28
82
  async getToken() {
29
83
  let version;
@@ -34,22 +88,24 @@ export class AimaxAuthReader {
34
88
  }
35
89
  catch {
36
90
  this.cached = { version: { exists: false } };
37
- return undefined;
91
+ return this.finish(undefined);
92
+ }
93
+ if (this.cached && sameAuthFileVersion(this.cached.version, version)) {
94
+ return this.finish(this.cached.token);
38
95
  }
39
- if (this.cached && sameAuthFileVersion(this.cached.version, version))
40
- return this.cached.token;
41
96
  let token;
42
97
  try {
43
98
  const parsed = JSON.parse(await readFile(this.authFile, "utf8"));
44
99
  const candidate = parsed["aimax-auth-token"];
45
- if (typeof candidate === "string" && candidate.trim())
46
- token = candidate;
100
+ if (typeof candidate === "string" && candidate.trim()) {
101
+ token = decodeAimaxAuthToken(candidate, this.resolveKeyHex());
102
+ }
47
103
  }
48
104
  catch {
49
105
  // Malformed or unreadable files are intentionally ineligible.
50
106
  }
51
107
  this.cached = { version, ...(token ? { token } : {}) };
52
- return token;
108
+ return this.finish(token);
53
109
  }
54
110
  async isEligible() {
55
111
  return Boolean(await this.getToken());
@@ -36,6 +36,7 @@ export class ChatSessionCatalog {
36
36
  deferredTurns: projectDeferredTurnSummaries(record.deferredTurns ?? []),
37
37
  isBlank: isBlankChatSession(record),
38
38
  ...(workspaceId ? { workspaceId } : {}),
39
+ sessionMode: record.sessionMode,
39
40
  });
40
41
  }
41
42
  remove(sessionId) {
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Browser-safe session-mode types and copy.
3
+ *
4
+ * Keep this module free of Node/runtime tool policy so the Console UI
5
+ * bundle can import it without pulling operator registries.
6
+ */
7
+ export const CONSOLE_CHAT_SESSION_MODES = ["workflow", "pi"];
8
+ export const DEFAULT_CONSOLE_CHAT_SESSION_MODE = "workflow";
9
+ export const SESSION_MODE_COPY = {
10
+ workflow: {
11
+ id: "workflow",
12
+ label: "工作流",
13
+ shortLabel: "工作流",
14
+ description: "规划并推进工作流",
15
+ createLabel: "新建工作流会话",
16
+ bannerTitle: "工作流",
17
+ bannerBody: "用自然语言编排 loop-agent / agent-worker 的全部能力。这是 Operator 面:默认具备完整 Pi 工具与受策略约束的 operator action。软约束:大改与高风险变更优先走受治 DAG。",
18
+ landingTitle: "开始一段新的对话",
19
+ landingBody: "用自然语言编排 loop-agent:查询状态、诊断问题、创建与恢复任务。可先点选下方建议,或选择工作流 / Pi 模式新建会话。",
20
+ emptyTitle: "今天想推进什么?",
21
+ emptyBody: "会话已就绪。点选建议快速开场,或在下方输入框直接描述目标。业务写入仍经受治理 DAG。",
22
+ },
23
+ pi: {
24
+ id: "pi",
25
+ label: "Pi",
26
+ shortLabel: "Pi",
27
+ description: "保留压缩与 Explore,直接协作编码",
28
+ createLabel: "新建 Pi 会话",
29
+ bannerTitle: "Pi",
30
+ bannerBody: "保留 Console 压缩与 Explore,直接协作编码。不注入工作流业务指令、业务工具或 Goal 驱动。",
31
+ landingTitle: "开始一段 Pi 协作",
32
+ landingBody: "Pi 模式保留压缩、Explore、认证与 SDD 接入,取消工作流业务注入。适合有人监督的直接编码与调试。",
33
+ emptyTitle: "直接协作编码",
34
+ emptyBody: "会话已就绪。使用 Pi 工具读写仓库;Explore 负责只读深挖。本会话不加载任务 / DAG 工作流工具。",
35
+ },
36
+ };
37
+ /** Slash actions that only exist in workflow sessions. Browser-safe: pure
38
+ * string constants, no runtime/tool policy imports. */
39
+ export const WORKFLOW_EXCLUSIVE_SLASH_ACTIONS = Object.freeze([
40
+ "focus-panel:task",
41
+ "focus-panel:contract",
42
+ "focus-panel:dag",
43
+ "focus:interview",
44
+ "goal",
45
+ ]);
46
+ export function isWorkflowExclusiveSlashAction(action) {
47
+ return WORKFLOW_EXCLUSIVE_SLASH_ACTIONS.includes(action);
48
+ }