vibe-coding-master 0.0.5 → 0.0.7

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 (42) hide show
  1. package/README.md +168 -63
  2. package/dist/backend/adapters/translation-provider.js +145 -0
  3. package/dist/backend/api/artifact-routes.js +3 -0
  4. package/dist/backend/api/harness-routes.js +22 -0
  5. package/dist/backend/api/project-routes.js +3 -8
  6. package/dist/backend/api/translation-routes.js +70 -0
  7. package/dist/backend/runtime/node-pty-runtime.js +20 -18
  8. package/dist/backend/server.js +31 -1
  9. package/dist/backend/services/app-settings-service.js +128 -0
  10. package/dist/backend/services/artifact-service.js +7 -4
  11. package/dist/backend/services/claude-transcript-service.js +509 -0
  12. package/dist/backend/services/harness-service.js +178 -0
  13. package/dist/backend/services/project-service.js +4 -0
  14. package/dist/backend/services/session-service.js +7 -5
  15. package/dist/backend/services/status-service.js +76 -0
  16. package/dist/backend/services/translation-prompts.js +173 -0
  17. package/dist/backend/services/translation-queue.js +39 -0
  18. package/dist/backend/services/translation-service.js +546 -0
  19. package/dist/backend/templates/handoff.js +32 -0
  20. package/dist/backend/templates/harness/architect-agent.js +12 -0
  21. package/dist/backend/templates/harness/claude-root.js +14 -0
  22. package/dist/backend/templates/harness/coder-agent.js +11 -0
  23. package/dist/backend/templates/harness/project-manager-agent.js +14 -0
  24. package/dist/backend/templates/harness/reviewer-agent.js +13 -0
  25. package/dist/backend/ws/translation-ws.js +35 -0
  26. package/dist/shared/types/harness.js +1 -0
  27. package/dist/shared/types/translation.js +5 -0
  28. package/dist/shared/validation/artifact-check.js +15 -1
  29. package/dist/shared/validation/language-detect.js +46 -0
  30. package/dist-frontend/assets/index-BNASqKEK.css +32 -0
  31. package/dist-frontend/assets/index-Bp49_End.js +58 -0
  32. package/dist-frontend/index.html +2 -2
  33. package/docs/cc-best-practices.md +93 -36
  34. package/docs/product-design.md +313 -1408
  35. package/docs/v1-architecture-design.md +500 -1153
  36. package/docs/v1-implementation-plan.md +783 -1604
  37. package/package.json +3 -1
  38. package/scripts/verify-package.mjs +121 -0
  39. package/dist/backend/templates/role-messaging-context.js +0 -44
  40. package/dist-frontend/assets/index-Bah6k-Ix.css +0 -32
  41. package/dist-frontend/assets/index-EMaQuIB6.js +0 -58
  42. package/docs/v1-message-bus-orchestration-design.md +0 -534
package/README.md CHANGED
@@ -2,41 +2,32 @@
2
2
 
3
3
  VibeCodingMaster is a local GUI workspace for managing multiple Claude Code role sessions around one engineering task.
4
4
 
5
- It is designed for long-running coding work where one Claude Code conversation is not enough. VCM gives the user a task workspace with four role sessions:
5
+ VCM is designed for long-running coding work where one Claude Code conversation is not enough. It gives the user a task workspace with four embedded Claude Code sessions:
6
6
 
7
7
  - `project-manager`
8
8
  - `architect`
9
9
  - `coder`
10
10
  - `reviewer`
11
11
 
12
- Each role runs as a real Claude Code process inside an embedded terminal. The GUI lets the user start, stop, resume, switch, inspect, and manually intervene in those sessions without juggling several terminal windows.
12
+ Each role runs as a real Claude Code process inside an embedded terminal. The GUI lets the user start, stop, resume, restart, switch, observe, and manually intervene in those sessions without juggling separate terminal windows.
13
13
 
14
- ## What It Does
15
-
16
- ```text
17
- Open local GUI
18
- -> connect a Git repository
19
- -> create a task
20
- -> start Claude Code role sessions
21
- -> talk to Claude Code through embedded terminals
22
- -> let project-manager coordinate architect / coder / reviewer
23
- -> approve or automate role-to-role messages
24
- -> resume interrupted role sessions later
25
- ```
26
-
27
- Current V1 capabilities:
14
+ ## Current V1 Capabilities
28
15
 
29
16
  - GUI-first task workspace.
30
- - Embedded Claude Code terminals powered by `node-pty`.
31
- - One Claude Code session per role.
32
- - Role session recovery with persisted Claude session ids and `claude --resume`.
33
- - Permission mode selection before start/restart:
34
- - default
17
+ - Collapsible sidebar with repository connection, workflow, settings, harness status, task creation, and task list.
18
+ - Recent repository path dropdown, stored locally with the five most recent paths.
19
+ - Embedded Claude Code terminals powered by `node-pty` and `xterm.js`.
20
+ - One Claude Code session per role, with role tabs in the task header.
21
+ - Role session recovery through persisted Claude session ids and `claude --resume`.
22
+ - Permission mode selection before start, resume, or restart:
23
+ - `default`
35
24
  - `bypassPermissions`
36
25
  - `--dangerously-skip-permissions`
37
26
  - PM-mediated role messaging through `vcmctl`.
38
27
  - Manual and automatic orchestration modes.
39
- - Durable task state, session state, raw logs, handoff artifacts, and message history.
28
+ - VCM harness installer for `CLAUDE.md` and `.claude/agents/*.md`.
29
+ - Translation panel powered by an OpenAI-compatible low-cost model.
30
+ - Durable task state, session state, raw terminal logs, handoff artifacts, and message history.
40
31
 
41
32
  ## Requirements
42
33
 
@@ -56,20 +47,16 @@ From npm:
56
47
 
57
48
  ```bash
58
49
  npm install -g vibe-coding-master
59
- ```
60
-
61
- Then start the packaged app:
62
-
63
- ```bash
64
50
  vcm
65
51
  ```
66
52
 
67
- The published package also installs `vcmctl`, which Claude Code role sessions use internally for VCM message bus commands.
53
+ The package also installs `vcmctl`, which Claude Code role sessions use internally to send VCM messages.
68
54
 
69
55
  From source:
70
56
 
71
57
  ```bash
72
58
  npm install
59
+ npm run dev
73
60
  ```
74
61
 
75
62
  ## Run Locally
@@ -107,7 +94,7 @@ Then open:
107
94
  http://127.0.0.1:4173/
108
95
  ```
109
96
 
110
- If installed globally from npm, `vcm` runs the production-style app and serves the GUI from the backend port:
97
+ The global `vcm` command runs the production-style app and serves the GUI from the backend port:
111
98
 
112
99
  ```text
113
100
  http://127.0.0.1:4173/
@@ -115,7 +102,7 @@ http://127.0.0.1:4173/
115
102
 
116
103
  ## Run In VS Code Dev Containers
117
104
 
118
- VCM works well inside a VS Code `devContainer` as long as VCM, Claude Code, and the target repository are all inside the same container filesystem.
105
+ VCM works inside a VS Code `devContainer` when VCM, Claude Code, and the target repository all run inside the same container filesystem.
119
106
 
120
107
  Add port forwarding to `.devcontainer/devcontainer.json`:
121
108
 
@@ -133,39 +120,106 @@ Add port forwarding to `.devcontainer/devcontainer.json`:
133
120
  }
134
121
  ```
135
122
 
136
- Use:
137
-
138
- ```bash
139
- npm install
140
- npm run dev
141
- ```
142
-
143
- Open the forwarded `5173` port for development mode. If you run `npm run build && npm start`, only `4173` is required.
123
+ Use the path as seen from inside the container, for example `/workspace`.
144
124
 
145
125
  Important container notes:
146
126
 
147
- - Install Claude Code inside the container, or make the `claude` command available in the container PATH.
127
+ - Install Claude Code inside the container, or make `claude` available in the container `PATH`.
148
128
  - Make sure Claude Code authentication works inside the container.
149
- - Make sure the container has network access to Claude services.
150
- - Use the path as seen from inside the container, for example `/workspace`.
151
- - VCM accepts normal repositories by checking `/workspace/.git` directly; it does not require global Git `safe.directory` config to connect.
152
- - Keep the user project, `.vcm`, and `.ai/handoffs` on the same mounted workspace so paths are consistent.
129
+ - Make sure the container has network access to Claude services and to the translation provider if translation is enabled.
130
+ - VCM accepts normal Git repositories by checking `.git` directly. It also supports `.git` files that point to worktree gitdirs.
131
+ - VCM uses per-command `git -c safe.directory=...` for Git metadata reads and does not require global `git config --global --add safe.directory`.
153
132
  - Treat the container as the sandbox boundary, especially when using relaxed Claude Code permission modes.
154
133
 
155
134
  ## Basic Usage
156
135
 
157
136
  1. Start VCM.
158
137
  2. Open the GUI.
159
- 3. Connect a Git repository.
160
- 4. Create a task.
161
- 5. Select a role tab.
162
- 6. Choose the Claude Code permission mode for that role.
163
- 7. Click `Start`.
164
- 8. Talk to `project-manager` first.
165
- 9. Let PM delegate to `architect`, `coder`, or `reviewer`.
166
- 10. Inspect and approve role messages in the `Messages` panel.
138
+ 3. In the sidebar, open `Repository Path`, enter a repository path or choose one from `Recent`, then click `Connect`.
139
+ 4. Review `VCM Harness`; if files need install/update, click `Install / Update`.
140
+ 5. Review any changed harness files and commit them if they look right.
141
+ 6. Create a task from `New Task` with a single task name.
142
+ 7. Select the task from `Tasks`.
143
+ 8. Use the role tabs in the task header to switch between `Project Manager`, `Architect`, `Coder`, and `Reviewer`.
144
+ 9. Choose the permission mode for the active role.
145
+ 10. Click `Start`, `Resume`, `Restart`, or `Stop` as needed.
146
+ 11. Talk mostly to `project-manager`; let PM coordinate the other roles through VCM messaging.
147
+
148
+ The recommended flow is:
149
+
150
+ ```text
151
+ project-manager
152
+ -> architect architecture plan
153
+ -> coder implementation and validation
154
+ -> reviewer independent review
155
+ -> architect docs sync / architecture drift check
156
+ -> project-manager final acceptance, commit, and PR
157
+ ```
158
+
159
+ The workflow status is shown in the sidebar `Workflow` section. It is a soft guide in V1: VCM highlights missing or incomplete handoff artifacts and suggests the next step, but it does not hard-block the user from manually starting or switching roles.
160
+
161
+ ## Sidebar UI
162
+
163
+ The left sidebar is intentionally compact and collapsible:
164
+
165
+ - `Repository Path`: path input on one row; `Recent` and `Connect` on the next row.
166
+ - `Repository`: connected path, branch, and working tree state. `Working tree: uncommitted changes` means `git status --porcelain` is not empty.
167
+ - `Workflow`: current soft gate and five workflow steps.
168
+ - `Settings`: `Messages`, `Events`, and the `Auto orchestration` on/off toggle.
169
+ - `VCM Harness`: status for `CLAUDE.md` and role agent files.
170
+ - `New Task`: one `task name` input.
171
+ - `Tasks`: task list and task status.
172
+
173
+ All sidebar sections are collapsed by default. When no task is selected, `Repository Path` opens by default.
174
+
175
+ ## Translation
176
+
177
+ The `Translate` button in the role toolbar opens a translation panel beside the embedded terminal. The terminal and translation panel split the available width evenly.
178
+
179
+ Translation settings are local and stored in:
180
+
181
+ ```text
182
+ ~/.vcm/settings.json
183
+ ```
184
+
185
+ The same file stores recent repository paths. The translation API key is stored locally under `translation.secrets.apiKey`; it is not written to the connected repository, `.ai/handoffs`, raw terminal logs, or git diffs.
186
+
187
+ Translation behavior:
188
+
189
+ - Provider type is OpenAI-compatible chat completions.
190
+ - Prompt slots are `zh-to-en`, `zh-to-en-with-context`, and `en-to-zh`.
191
+ - The settings modal shows default prompts and allows per-slot overrides.
192
+ - Claude Code output translation reads semantic Claude transcript JSONL files under `~/.claude/projects`, not raw PTY output.
193
+ - Assistant prose is shown as English source while translating, then replaced by the translated Chinese result.
194
+ - Tool calls and tool results are preserved as dim one-line rows such as `● Bash({"command":"npm test"})`.
195
+ - User input uses one textarea. Press `Enter` to translate or send the current English draft; press `Shift+Enter` for a newline.
196
+ - After user input is translated, the English draft replaces the original text in the same textarea.
197
+ - `Send English` writes the current English draft to the active embedded terminal and submits it.
198
+ - The translation panel `Auto-send` toggle sends the translated draft automatically when translation succeeds without warnings.
167
199
 
168
- The recommended flow is to mostly talk to `project-manager`. The PM role should coordinate the other roles through VCM messaging instead of asking the user to copy prompts between terminals.
200
+ ## Project Harness
201
+
202
+ VCM works best when the connected repository contains VCM collaboration rules as normal project files. On first connect, VCM checks:
203
+
204
+ ```text
205
+ CLAUDE.md
206
+ .claude/agents/project-manager.md
207
+ .claude/agents/architect.md
208
+ .claude/agents/coder.md
209
+ .claude/agents/reviewer.md
210
+ ```
211
+
212
+ If a file is missing, VCM can create a recommended default. If a file already exists, VCM preserves user-authored content and only inserts or replaces a managed block:
213
+
214
+ ```md
215
+ <!-- VCM:BEGIN version=1 -->
216
+ VCM-managed rules live here.
217
+ <!-- VCM:END -->
218
+ ```
219
+
220
+ After applying harness changes, VCM reports the exact files changed and reminds the user to review and commit them before starting long-running work.
221
+
222
+ Role sessions learn VCM rules from `CLAUDE.md` and `.claude/agents/*.md`. VCM does not paste a long context block into the terminal at session start.
169
223
 
170
224
  ## Message Bus
171
225
 
@@ -175,9 +229,9 @@ Role communication works like this:
175
229
 
176
230
  ```text
177
231
  Claude Code role
178
- -> runs vcmctl send / vcmctl reply
232
+ -> runs vcmctl send / vcmctl reply / vcmctl result
179
233
  -> vcmctl calls VCM backend API
180
- -> backend validates policy and persists the message
234
+ -> backend validates message policy and persists the message
181
235
  -> backend writes to the target embedded terminal when allowed
182
236
  ```
183
237
 
@@ -190,7 +244,7 @@ vcmctl result --body-file /tmp/result.md --artifact .ai/handoffs/task/implementa
190
244
  vcmctl inbox
191
245
  ```
192
246
 
193
- Files are still used for durability and auditability:
247
+ Durable message and handoff files:
194
248
 
195
249
  ```text
196
250
  .vcm/messages/<task>.jsonl
@@ -200,16 +254,18 @@ Files are still used for durability and auditability:
200
254
  .ai/handoffs/<task>/logs/
201
255
  ```
202
256
 
257
+ The backend also keeps a compatibility role-command dispatch endpoint, but the primary workflow is PM-mediated `vcmctl` messaging.
258
+
203
259
  ## Orchestration Modes
204
260
 
205
- VCM has a task-level `Auto orchestration` switch.
261
+ VCM has a task-level `Auto orchestration` switch in the sidebar `Settings` section.
206
262
 
207
263
  When it is off, VCM is in manual mode:
208
264
 
209
265
  - Roles may send messages through `vcmctl`.
210
- - Messages appear in the GUI.
266
+ - Messages appear in the `Messages` modal.
211
267
  - The user can inspect them.
212
- - Clicking `Stage` writes the message prompt into the target embedded terminal input line.
268
+ - Clicking `Stage` writes a prompt into the target embedded terminal input line.
213
269
  - VCM does not press Enter for the user.
214
270
 
215
271
  When it is on, VCM is in auto mode:
@@ -217,13 +273,61 @@ When it is on, VCM is in auto mode:
217
273
  - Backend policy still applies.
218
274
  - PM can send work to `architect`, `coder`, or `reviewer`.
219
275
  - Non-PM roles can reply only to `project-manager`.
220
- - If the target role session is running and orchestration is not paused, VCM can deliver the message directly to the target terminal.
276
+ - If the target role session is running, VCM writes a `[VCM MESSAGE]` envelope to the target terminal and submits it.
221
277
 
222
- High-risk work should still stop for human review.
278
+ The backend state model still contains a `paused` field for compatibility with existing API routes, but the current GUI exposes only a single on/off orchestration toggle.
223
279
 
224
280
  ## Resume Behavior
225
281
 
226
- Each role session stores a Claude session id under `.vcm/sessions`. If VCM exits or a task is interrupted, reopen the task and use `Resume` for the role. VCM starts Claude Code with `claude --resume <session-id>` where supported by Claude Code.
282
+ Each role session stores its Claude session id and transcript path under:
283
+
284
+ ```text
285
+ .vcm/sessions/<task>.json
286
+ ```
287
+
288
+ Session buttons behave as follows:
289
+
290
+ - `Start`: creates a fresh UUID, builds `claude --agent <role> --session-id <uuid>`, and stores the transcript path.
291
+ - `Resume`: reuses the persisted Claude session id and builds `claude --agent <role> --resume <uuid>`.
292
+ - `Restart`: stops the current process if needed, creates a new UUID, and starts a fresh Claude session.
293
+ - `Stop`: stops the embedded terminal process and leaves the persisted Claude session id resumable.
294
+
295
+ ## Local Project Files
296
+
297
+ For a connected repository, VCM uses:
298
+
299
+ ```text
300
+ .vcm/config.json
301
+ .vcm/tasks/<task>.json
302
+ .vcm/sessions/<task>.json
303
+ .vcm/messages/<task>.jsonl
304
+ .vcm/orchestration/<task>.json
305
+ .ai/handoffs/<task>/architecture-plan.md
306
+ .ai/handoffs/<task>/implementation-log.md
307
+ .ai/handoffs/<task>/validation-log.md
308
+ .ai/handoffs/<task>/review-report.md
309
+ .ai/handoffs/<task>/docs-sync-report.md
310
+ .ai/handoffs/<task>/role-commands/{architect,coder,reviewer}.md
311
+ .ai/handoffs/<task>/logs/{project-manager,architect,coder,reviewer}.log
312
+ ```
313
+
314
+ ## Packaging
315
+
316
+ The npm package publishes built output, not raw TypeScript entry files. `package.json` includes:
317
+
318
+ - `bin.vcm`: `dist/main.js`
319
+ - `bin.vcmctl`: `dist/cli/vcmctl.js`
320
+ - `files`: `dist`, `dist-frontend`, `docs`, `scripts`, `README.md`
321
+ - `prepack`: `npm run build && npm run verify:package`
322
+
323
+ Use this before publishing:
324
+
325
+ ```bash
326
+ npm run typecheck
327
+ npm test
328
+ npm run build
329
+ npm run verify:package
330
+ ```
227
331
 
228
332
  ## Validation
229
333
 
@@ -237,8 +341,9 @@ npm run build
237
341
 
238
342
  - VCM does not use tmux.
239
343
  - VCM does not auto-confirm Claude Code permission prompts.
240
- - VCM does not deeply parse Claude Code output.
241
344
  - VCM does not isolate roles with separate worktrees in V1.
345
+ - VCM does not translate Claude output from raw PTY output; translation reads Claude transcript JSONL files.
346
+ - VCM does not write translation output into handoff artifacts unless a user or role explicitly copies it there.
242
347
  - File writes still happen in the connected repository environment.
243
348
  - The safest sandbox today is a container or VM boundary controlled by the user.
244
349
 
@@ -247,4 +352,4 @@ See also:
247
352
  - `docs/product-design.md`
248
353
  - `docs/v1-architecture-design.md`
249
354
  - `docs/v1-implementation-plan.md`
250
- - `docs/v1-message-bus-orchestration-design.md`
355
+ - `docs/cc-best-practices.md`
@@ -0,0 +1,145 @@
1
+ export class TranslationProviderError extends Error {
2
+ code;
3
+ elapsedMs;
4
+ constructor(message, code, elapsedMs = 0) {
5
+ super(message);
6
+ this.name = "TranslationProviderError";
7
+ this.code = code;
8
+ this.elapsedMs = elapsedMs;
9
+ }
10
+ }
11
+ export function createOpenAiCompatibleTranslationProvider(fetchImpl = fetch) {
12
+ return {
13
+ async testConnection(settings, secrets) {
14
+ const startedAt = performance.now();
15
+ try {
16
+ await this.translate({
17
+ settings,
18
+ secrets,
19
+ systemPrompt: "Reply with exactly: ok",
20
+ userPrompt: "ok"
21
+ });
22
+ return {
23
+ ok: true,
24
+ model: settings.model,
25
+ elapsedMs: Math.round(performance.now() - startedAt)
26
+ };
27
+ }
28
+ catch (error) {
29
+ return {
30
+ ok: false,
31
+ model: settings.model,
32
+ elapsedMs: Math.round(performance.now() - startedAt),
33
+ error: error instanceof Error ? error.message : "Translation provider failed."
34
+ };
35
+ }
36
+ },
37
+ async translate(input) {
38
+ const apiKey = input.secrets.apiKey?.trim();
39
+ if (!apiKey) {
40
+ throw new TranslationProviderError("Translation API key is not configured.", "config");
41
+ }
42
+ const startedAt = performance.now();
43
+ const elapsed = () => Math.round(performance.now() - startedAt);
44
+ const controller = new AbortController();
45
+ const timeout = setTimeout(() => controller.abort(), input.settings.requestTimeoutMs);
46
+ const externalAbort = () => controller.abort();
47
+ if (input.signal) {
48
+ if (input.signal.aborted) {
49
+ controller.abort();
50
+ }
51
+ else {
52
+ input.signal.addEventListener("abort", externalAbort, { once: true });
53
+ }
54
+ }
55
+ try {
56
+ const response = await fetchImpl(buildChatCompletionsUrl(input.settings.baseUrl), {
57
+ method: "POST",
58
+ headers: {
59
+ "content-type": "application/json",
60
+ authorization: `Bearer ${apiKey}`
61
+ },
62
+ body: JSON.stringify({
63
+ model: input.settings.model,
64
+ messages: [
65
+ { role: "system", content: input.systemPrompt },
66
+ { role: "user", content: input.userPrompt }
67
+ ],
68
+ temperature: input.settings.temperature,
69
+ stream: false
70
+ }),
71
+ signal: controller.signal
72
+ });
73
+ const rawText = await response.text();
74
+ let payload = null;
75
+ try {
76
+ payload = rawText ? JSON.parse(rawText) : null;
77
+ }
78
+ catch {
79
+ if (!response.ok) {
80
+ throw new TranslationProviderError(rawText || response.statusText, `HTTP ${response.status}`, elapsed());
81
+ }
82
+ throw new TranslationProviderError("Translation provider returned invalid JSON.", "parse", elapsed());
83
+ }
84
+ if (!response.ok) {
85
+ throw new TranslationProviderError(extractErrorMessage(payload) ?? response.statusText, `HTTP ${response.status}`, elapsed());
86
+ }
87
+ const content = extractContent(payload);
88
+ if (!content) {
89
+ throw new TranslationProviderError("Translation provider returned empty content.", "parse", elapsed());
90
+ }
91
+ return {
92
+ text: content,
93
+ elapsedMs: elapsed(),
94
+ tokenUsage: parseOpenAiUsage(payload?.usage)
95
+ };
96
+ }
97
+ catch (error) {
98
+ if (error instanceof TranslationProviderError) {
99
+ throw error;
100
+ }
101
+ const message = error instanceof Error ? error.message : String(error);
102
+ const code = message.toLowerCase().includes("abort") ? "timeout" : "network";
103
+ throw new TranslationProviderError(message, code, elapsed());
104
+ }
105
+ finally {
106
+ clearTimeout(timeout);
107
+ input.signal?.removeEventListener("abort", externalAbort);
108
+ }
109
+ }
110
+ };
111
+ }
112
+ export function buildChatCompletionsUrl(baseUrl) {
113
+ return `${baseUrl.replace(/\/$/, "")}/chat/completions`;
114
+ }
115
+ export function parseOpenAiUsage(raw) {
116
+ if (!raw || typeof raw !== "object") {
117
+ return undefined;
118
+ }
119
+ const usage = raw;
120
+ const input = typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : 0;
121
+ const output = typeof usage.completion_tokens === "number" ? usage.completion_tokens : 0;
122
+ const total = typeof usage.total_tokens === "number" ? usage.total_tokens : input + output;
123
+ if (input === 0 && output === 0 && total === 0) {
124
+ return undefined;
125
+ }
126
+ return { input, output, total };
127
+ }
128
+ function extractContent(payload) {
129
+ const choices = payload?.choices;
130
+ if (!Array.isArray(choices) || choices.length === 0) {
131
+ return null;
132
+ }
133
+ const content = choices[0].message?.content;
134
+ return typeof content === "string" ? content.trim() : null;
135
+ }
136
+ function extractErrorMessage(payload) {
137
+ const error = payload?.error;
138
+ if (!error) {
139
+ return null;
140
+ }
141
+ if (typeof error === "string") {
142
+ return error;
143
+ }
144
+ return error.message ?? JSON.stringify(error);
145
+ }
@@ -90,6 +90,9 @@ function artifactNameToPath(paths, artifactName) {
90
90
  if (artifactName === "review-report.md") {
91
91
  return paths.reviewReportPath;
92
92
  }
93
+ if (artifactName === "docs-sync-report.md") {
94
+ return paths.docsSyncReportPath;
95
+ }
93
96
  throw new VcmError({
94
97
  code: "ARTIFACT_UNKNOWN",
95
98
  message: `Unknown artifact: ${artifactName}`,
@@ -0,0 +1,22 @@
1
+ import { VcmError } from "../errors.js";
2
+ export function registerHarnessRoutes(app, deps) {
3
+ app.get("/api/projects/harness", async () => {
4
+ const project = await requireCurrentProject(deps.projectService);
5
+ return deps.harnessService.getHarnessStatus(project.repoRoot);
6
+ });
7
+ app.post("/api/projects/harness/apply", async () => {
8
+ const project = await requireCurrentProject(deps.projectService);
9
+ return deps.harnessService.applyHarness(project.repoRoot);
10
+ });
11
+ }
12
+ async function requireCurrentProject(projectService) {
13
+ const project = await projectService.getCurrentProject();
14
+ if (!project) {
15
+ throw new VcmError({
16
+ code: "PROJECT_NOT_CONNECTED",
17
+ message: "Connect a repository first.",
18
+ statusCode: 409
19
+ });
20
+ }
21
+ return project;
22
+ }
@@ -1,17 +1,12 @@
1
1
  export function registerProjectRoutes(app, deps) {
2
2
  app.get("/api/health", async () => ({ ok: true }));
3
+ app.get("/api/projects/recent", async () => {
4
+ return deps.projectService.getRecentRepositoryPaths();
5
+ });
3
6
  app.post("/api/projects/connect", async (request) => {
4
7
  return deps.projectService.connectProject(request.body);
5
8
  });
6
9
  app.get("/api/projects/current", async () => {
7
10
  return deps.projectService.getCurrentProject();
8
11
  });
9
- app.get("/api/projects/harness", async () => ({
10
- checks: [
11
- {
12
- name: "local-gui",
13
- status: "ok"
14
- }
15
- ]
16
- }));
17
12
  }
@@ -0,0 +1,70 @@
1
+ import { isRoleName } from "../../shared/constants.js";
2
+ import { VcmError } from "../errors.js";
3
+ export function registerTranslationRoutes(app, deps) {
4
+ app.get("/api/translation/settings", async () => {
5
+ return deps.translationService.getSettings();
6
+ });
7
+ app.put("/api/translation/settings", async (request) => {
8
+ const { apiKey, ...settings } = request.body ?? {};
9
+ return deps.translationService.updateSettings(settings, apiKey !== undefined ? { apiKey } : undefined);
10
+ });
11
+ app.get("/api/translation/prompts", async () => {
12
+ return deps.translationService.getPromptPreviews();
13
+ });
14
+ app.post("/api/translation/test", async () => {
15
+ return deps.translationService.testProvider();
16
+ });
17
+ app.post("/api/tasks/:taskSlug/sessions/:role/translation/input", async (request) => {
18
+ const project = await requireCurrentProject(deps.projectService);
19
+ const role = parseRole(request.params.role);
20
+ await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
21
+ return deps.translationService.translateUserInput({
22
+ repoRoot: project.repoRoot,
23
+ taskSlug: request.params.taskSlug,
24
+ role,
25
+ ...(request.body ?? { text: "" })
26
+ });
27
+ });
28
+ app.post("/api/tasks/:taskSlug/sessions/:role/translation/send", async (request) => {
29
+ const project = await requireCurrentProject(deps.projectService);
30
+ const role = parseRole(request.params.role);
31
+ await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
32
+ await deps.translationService.sendTranslatedInput({
33
+ repoRoot: project.repoRoot,
34
+ taskSlug: request.params.taskSlug,
35
+ role,
36
+ englishText: request.body?.englishText ?? ""
37
+ });
38
+ return { ok: true };
39
+ });
40
+ app.post("/api/translation/sessions/:sessionId/clear", async (request) => {
41
+ await requireCurrentProject(deps.projectService);
42
+ deps.translationService.clearSession(request.params.sessionId);
43
+ return { ok: true };
44
+ });
45
+ app.post("/api/translation/sessions/:sessionId/retry/:translationId", async (request) => {
46
+ await requireCurrentProject(deps.projectService);
47
+ return deps.translationService.retryTranslation(request.params.sessionId, request.params.translationId);
48
+ });
49
+ }
50
+ function parseRole(role) {
51
+ if (!isRoleName(role)) {
52
+ throw new VcmError({
53
+ code: "UNKNOWN_ROLE",
54
+ message: `Unknown role: ${role}`,
55
+ statusCode: 400
56
+ });
57
+ }
58
+ return role;
59
+ }
60
+ async function requireCurrentProject(projectService) {
61
+ const project = await projectService.getCurrentProject();
62
+ if (!project) {
63
+ throw new VcmError({
64
+ code: "PROJECT_NOT_CONNECTED",
65
+ message: "Connect a repository first.",
66
+ statusCode: 409
67
+ });
68
+ }
69
+ return project;
70
+ }
@@ -109,27 +109,29 @@ export function createNodePtyTerminalRuntime(deps) {
109
109
  entry.process.kill();
110
110
  return create(entry.input, sessionId);
111
111
  },
112
- subscribe(sessionId, listener) {
112
+ subscribe(sessionId, listener, options = {}) {
113
113
  const entry = getEntry(entries, sessionId);
114
114
  entry.listeners.add(listener);
115
- void deps.fs.readText(entry.input.logPath)
116
- .then((data) => {
117
- if (!data || !entry.listeners.has(listener)) {
118
- return;
119
- }
120
- listener({
121
- id: `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`,
122
- sessionId,
123
- taskSlug: entry.session.taskSlug,
124
- role: entry.session.role,
125
- type: "output",
126
- timestamp: now(),
127
- data
115
+ if (options.replay !== false) {
116
+ void deps.fs.readText(entry.input.logPath)
117
+ .then((data) => {
118
+ if (!data || !entry.listeners.has(listener)) {
119
+ return;
120
+ }
121
+ listener({
122
+ id: `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`,
123
+ sessionId,
124
+ taskSlug: entry.session.taskSlug,
125
+ role: entry.session.role,
126
+ type: "output",
127
+ timestamp: now(),
128
+ data
129
+ });
130
+ })
131
+ .catch(() => {
132
+ // The log file may not exist yet for a brand-new session.
128
133
  });
129
- })
130
- .catch(() => {
131
- // The log file may not exist yet for a brand-new session.
132
- });
134
+ }
133
135
  return () => {
134
136
  entry.listeners.delete(listener);
135
137
  };