vibe-coding-master 0.0.6 → 0.0.8
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.
- package/README.md +207 -66
- package/dist/backend/adapters/filesystem.js +13 -0
- package/dist/backend/adapters/git-adapter.js +79 -1
- package/dist/backend/adapters/translation-provider.js +145 -0
- package/dist/backend/api/artifact-routes.js +16 -7
- package/dist/backend/api/harness-routes.js +22 -0
- package/dist/backend/api/message-routes.js +2 -0
- package/dist/backend/api/project-routes.js +3 -8
- package/dist/backend/api/task-routes.js +14 -0
- package/dist/backend/api/translation-routes.js +70 -0
- package/dist/backend/runtime/node-pty-runtime.js +20 -18
- package/dist/backend/server.js +33 -2
- package/dist/backend/services/app-settings-service.js +128 -0
- package/dist/backend/services/artifact-service.js +7 -4
- package/dist/backend/services/claude-transcript-service.js +509 -0
- package/dist/backend/services/command-dispatcher.js +4 -2
- package/dist/backend/services/harness-service.js +196 -0
- package/dist/backend/services/message-service.js +1 -1
- package/dist/backend/services/project-service.js +50 -9
- package/dist/backend/services/session-service.js +13 -9
- package/dist/backend/services/status-service.js +79 -1
- package/dist/backend/services/task-service.js +118 -4
- package/dist/backend/services/translation-prompts.js +173 -0
- package/dist/backend/services/translation-queue.js +39 -0
- package/dist/backend/services/translation-service.js +546 -0
- package/dist/backend/templates/handoff.js +32 -0
- package/dist/backend/templates/harness/architect-agent.js +12 -0
- package/dist/backend/templates/harness/claude-root.js +14 -0
- package/dist/backend/templates/harness/coder-agent.js +11 -0
- package/dist/backend/templates/harness/gitignore.js +9 -0
- package/dist/backend/templates/harness/project-manager-agent.js +14 -0
- package/dist/backend/templates/harness/reviewer-agent.js +13 -0
- package/dist/backend/ws/translation-ws.js +35 -0
- package/dist/shared/types/harness.js +1 -0
- package/dist/shared/types/translation.js +5 -0
- package/dist/shared/validation/artifact-check.js +15 -1
- package/dist/shared/validation/language-detect.js +46 -0
- package/dist-frontend/assets/index-CuiNNOzj.css +32 -0
- package/dist-frontend/assets/index-D59GuHCR.js +58 -0
- package/dist-frontend/index.html +2 -2
- package/docs/cc-best-practices.md +109 -40
- package/docs/product-design.md +370 -1374
- package/docs/v1-architecture-design.md +595 -1114
- package/docs/v1-implementation-plan.md +898 -1603
- package/package.json +1 -1
- package/scripts/verify-package.mjs +8 -0
- package/dist/backend/templates/role-messaging-context.js +0 -44
- package/dist-frontend/assets/index-Bah6k-Ix.css +0 -32
- package/dist-frontend/assets/index-EMaQuIB6.js +0 -58
- 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
|
-
|
|
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,
|
|
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
|
-
##
|
|
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
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
|
|
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
|
-
-
|
|
28
|
+
- VCM harness installer for `CLAUDE.md`, `.claude/agents/*.md`, and the VCM-managed `.gitignore` block.
|
|
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
|
|
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
|
-
|
|
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
|
|
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,141 @@ 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
|
|
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
|
-
-
|
|
151
|
-
- VCM
|
|
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.
|
|
160
|
-
4.
|
|
161
|
-
5.
|
|
162
|
-
6.
|
|
163
|
-
7.
|
|
164
|
-
8.
|
|
165
|
-
9.
|
|
166
|
-
10.
|
|
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
|
+
## Task Worktree Management
|
|
167
162
|
|
|
168
|
-
|
|
163
|
+
VCM uses task-level worktree management:
|
|
164
|
+
|
|
165
|
+
```text
|
|
166
|
+
one task = one branch + one git worktree + one handoff directory + one role-session set
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The default when creating a task:
|
|
170
|
+
|
|
171
|
+
- task name: `<task>`
|
|
172
|
+
- branch: `feature/<task>`
|
|
173
|
+
- worktree path: `.ai/vcm/worktrees/<task>` inside the connected base repository
|
|
174
|
+
- role session cwd: that task worktree
|
|
175
|
+
|
|
176
|
+
VCM will not create worktrees per role. `project-manager`, `architect`, `coder`, and `reviewer` for the same task share the same task worktree.
|
|
177
|
+
|
|
178
|
+
VCM will not offer a separate `Create task worktree` button after a task exists, and a task should not be switched to another branch/worktree after creation.
|
|
179
|
+
|
|
180
|
+
Because worktrees live under `.ai/vcm/worktrees/`, the connected repository must ignore `.ai/vcm/`. Apply the VCM Harness before creating tasks so `.gitignore` contains the managed ignore block. The base repository must also be clean because the task branch/worktree is created from the connected repo's current `HEAD`.
|
|
181
|
+
|
|
182
|
+
When a task is complete, VCM provides a guarded cleanup action that removes the task worktree and VCM task metadata. Cleanup refuses to remove a dirty task worktree unless a force cleanup is requested through the API. Branch deletion stays separate and requires explicit confirmation.
|
|
183
|
+
|
|
184
|
+
## Sidebar UI
|
|
185
|
+
|
|
186
|
+
The left sidebar is intentionally compact and collapsible:
|
|
187
|
+
|
|
188
|
+
- `Repository Path`: path input on one row; `Recent` and `Connect` on the next row.
|
|
189
|
+
- `Repository`: connected path, branch, and working tree state. `Working tree: uncommitted changes` means `git status --porcelain` is not empty.
|
|
190
|
+
- `Workflow`: current soft gate and five workflow steps.
|
|
191
|
+
- `Settings`: `Messages`, `Events`, and the `Auto orchestration` on/off toggle.
|
|
192
|
+
- `VCM Harness`: status for `CLAUDE.md`, role agent files, and `.gitignore`.
|
|
193
|
+
- `New Task`: one `task name` input.
|
|
194
|
+
- `Tasks`: task list and task status.
|
|
195
|
+
|
|
196
|
+
All sidebar sections are collapsed by default. When no task is selected, `Repository Path` opens by default.
|
|
197
|
+
|
|
198
|
+
## Translation
|
|
199
|
+
|
|
200
|
+
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.
|
|
201
|
+
|
|
202
|
+
Translation settings are local and stored in:
|
|
203
|
+
|
|
204
|
+
```text
|
|
205
|
+
~/.vcm/settings.json
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
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.
|
|
209
|
+
|
|
210
|
+
Translation behavior:
|
|
211
|
+
|
|
212
|
+
- Provider type is OpenAI-compatible chat completions.
|
|
213
|
+
- Prompt slots are `zh-to-en`, `zh-to-en-with-context`, and `en-to-zh`.
|
|
214
|
+
- The settings modal shows default prompts and allows per-slot overrides.
|
|
215
|
+
- Claude Code output translation reads semantic Claude transcript JSONL files under `~/.claude/projects`, not raw PTY output.
|
|
216
|
+
- Assistant prose is shown as English source while translating, then replaced by the translated Chinese result.
|
|
217
|
+
- Tool calls and tool results are preserved as dim one-line rows such as `● Bash({"command":"npm test"})`.
|
|
218
|
+
- User input uses one textarea. Press `Enter` to translate or send the current English draft; press `Shift+Enter` for a newline.
|
|
219
|
+
- After user input is translated, the English draft replaces the original text in the same textarea.
|
|
220
|
+
- `Send English` writes the current English draft to the active embedded terminal and submits it.
|
|
221
|
+
- The translation panel `Auto-send` toggle sends the translated draft automatically when translation succeeds without warnings.
|
|
222
|
+
|
|
223
|
+
## Project Harness
|
|
224
|
+
|
|
225
|
+
VCM works best when the connected repository contains VCM collaboration rules as normal project files. On first connect, VCM checks:
|
|
226
|
+
|
|
227
|
+
```text
|
|
228
|
+
CLAUDE.md
|
|
229
|
+
.gitignore
|
|
230
|
+
.claude/agents/project-manager.md
|
|
231
|
+
.claude/agents/architect.md
|
|
232
|
+
.claude/agents/coder.md
|
|
233
|
+
.claude/agents/reviewer.md
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
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:
|
|
237
|
+
|
|
238
|
+
```md
|
|
239
|
+
<!-- VCM:BEGIN version=1 -->
|
|
240
|
+
VCM-managed rules live here.
|
|
241
|
+
<!-- VCM:END -->
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
For `.gitignore`, VCM uses a gitignore-native managed block:
|
|
245
|
+
|
|
246
|
+
```gitignore
|
|
247
|
+
# VCM:BEGIN version=1
|
|
248
|
+
.ai/vcm/
|
|
249
|
+
.vcm/
|
|
250
|
+
# VCM:END
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`.ai/vcm/` is the active VCM local control area. `.vcm/` is ignored only as a legacy safety rule so older local state cannot be accidentally committed during migration.
|
|
254
|
+
|
|
255
|
+
After applying harness changes, VCM reports the exact files changed and reminds the user to review and commit them before starting long-running work.
|
|
256
|
+
|
|
257
|
+
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
258
|
|
|
170
259
|
## Message Bus
|
|
171
260
|
|
|
@@ -175,9 +264,9 @@ Role communication works like this:
|
|
|
175
264
|
|
|
176
265
|
```text
|
|
177
266
|
Claude Code role
|
|
178
|
-
-> runs vcmctl send / vcmctl reply
|
|
267
|
+
-> runs vcmctl send / vcmctl reply / vcmctl result
|
|
179
268
|
-> vcmctl calls VCM backend API
|
|
180
|
-
-> backend validates policy and persists the message
|
|
269
|
+
-> backend validates message policy and persists the message
|
|
181
270
|
-> backend writes to the target embedded terminal when allowed
|
|
182
271
|
```
|
|
183
272
|
|
|
@@ -190,26 +279,28 @@ vcmctl result --body-file /tmp/result.md --artifact .ai/handoffs/task/implementa
|
|
|
190
279
|
vcmctl inbox
|
|
191
280
|
```
|
|
192
281
|
|
|
193
|
-
|
|
282
|
+
Durable message and handoff files:
|
|
194
283
|
|
|
195
284
|
```text
|
|
196
|
-
.vcm/messages/<task>.jsonl
|
|
197
|
-
.vcm/orchestration/<task>.json
|
|
285
|
+
.ai/vcm/messages/<task>.jsonl
|
|
286
|
+
.ai/vcm/orchestration/<task>.json
|
|
198
287
|
.ai/handoffs/<task>/messages/<message-id>.md
|
|
199
288
|
.ai/handoffs/<task>/role-commands/
|
|
200
289
|
.ai/handoffs/<task>/logs/
|
|
201
290
|
```
|
|
202
291
|
|
|
292
|
+
The backend also keeps a compatibility role-command dispatch endpoint, but the primary workflow is PM-mediated `vcmctl` messaging.
|
|
293
|
+
|
|
203
294
|
## Orchestration Modes
|
|
204
295
|
|
|
205
|
-
VCM has a task-level `Auto orchestration` switch.
|
|
296
|
+
VCM has a task-level `Auto orchestration` switch in the sidebar `Settings` section.
|
|
206
297
|
|
|
207
298
|
When it is off, VCM is in manual mode:
|
|
208
299
|
|
|
209
300
|
- Roles may send messages through `vcmctl`.
|
|
210
|
-
- Messages appear in the
|
|
301
|
+
- Messages appear in the `Messages` modal.
|
|
211
302
|
- The user can inspect them.
|
|
212
|
-
- Clicking `Stage` writes
|
|
303
|
+
- Clicking `Stage` writes a prompt into the target embedded terminal input line.
|
|
213
304
|
- VCM does not press Enter for the user.
|
|
214
305
|
|
|
215
306
|
When it is on, VCM is in auto mode:
|
|
@@ -217,13 +308,62 @@ When it is on, VCM is in auto mode:
|
|
|
217
308
|
- Backend policy still applies.
|
|
218
309
|
- PM can send work to `architect`, `coder`, or `reviewer`.
|
|
219
310
|
- Non-PM roles can reply only to `project-manager`.
|
|
220
|
-
- If the target role session is running
|
|
311
|
+
- If the target role session is running, VCM writes a `[VCM MESSAGE]` envelope to the target terminal and submits it.
|
|
221
312
|
|
|
222
|
-
|
|
313
|
+
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
314
|
|
|
224
315
|
## Resume Behavior
|
|
225
316
|
|
|
226
|
-
Each role session stores
|
|
317
|
+
Each role session stores its Claude session id and transcript path under:
|
|
318
|
+
|
|
319
|
+
```text
|
|
320
|
+
.ai/vcm/sessions/<task>.json
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Session buttons behave as follows:
|
|
324
|
+
|
|
325
|
+
- `Start`: creates a fresh UUID, builds `claude --agent <role> --session-id <uuid>`, and stores the transcript path.
|
|
326
|
+
- `Resume`: reuses the persisted Claude session id and builds `claude --agent <role> --resume <uuid>`.
|
|
327
|
+
- `Restart`: stops the current process if needed, creates a new UUID, and starts a fresh Claude session.
|
|
328
|
+
- `Stop`: stops the embedded terminal process and leaves the persisted Claude session id resumable.
|
|
329
|
+
|
|
330
|
+
## Local Project Files
|
|
331
|
+
|
|
332
|
+
For a connected repository, VCM uses:
|
|
333
|
+
|
|
334
|
+
```text
|
|
335
|
+
.ai/vcm/config.json
|
|
336
|
+
.ai/vcm/tasks/<task>.json
|
|
337
|
+
.ai/vcm/sessions/<task>.json
|
|
338
|
+
.ai/vcm/messages/<task>.jsonl
|
|
339
|
+
.ai/vcm/orchestration/<task>.json
|
|
340
|
+
.ai/vcm/worktrees/<task>/
|
|
341
|
+
.ai/handoffs/<task>/architecture-plan.md
|
|
342
|
+
.ai/handoffs/<task>/implementation-log.md
|
|
343
|
+
.ai/handoffs/<task>/validation-log.md
|
|
344
|
+
.ai/handoffs/<task>/review-report.md
|
|
345
|
+
.ai/handoffs/<task>/docs-sync-report.md
|
|
346
|
+
.ai/handoffs/<task>/role-commands/{architect,coder,reviewer}.md
|
|
347
|
+
.ai/handoffs/<task>/logs/{project-manager,architect,coder,reviewer}.log
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Packaging
|
|
351
|
+
|
|
352
|
+
The npm package publishes built output, not raw TypeScript entry files. `package.json` includes:
|
|
353
|
+
|
|
354
|
+
- `bin.vcm`: `dist/main.js`
|
|
355
|
+
- `bin.vcmctl`: `dist/cli/vcmctl.js`
|
|
356
|
+
- `files`: `dist`, `dist-frontend`, `docs`, `scripts`, `README.md`
|
|
357
|
+
- `prepack`: `npm run build && npm run verify:package`
|
|
358
|
+
|
|
359
|
+
Use this before publishing:
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
npm run typecheck
|
|
363
|
+
npm test
|
|
364
|
+
npm run build
|
|
365
|
+
npm run verify:package
|
|
366
|
+
```
|
|
227
367
|
|
|
228
368
|
## Validation
|
|
229
369
|
|
|
@@ -237,9 +377,10 @@ npm run build
|
|
|
237
377
|
|
|
238
378
|
- VCM does not use tmux.
|
|
239
379
|
- VCM does not auto-confirm Claude Code permission prompts.
|
|
240
|
-
- VCM does not deeply parse Claude Code output.
|
|
241
380
|
- VCM does not isolate roles with separate worktrees in V1.
|
|
242
|
-
-
|
|
381
|
+
- VCM does not translate Claude output from raw PTY output; translation reads Claude transcript JSONL files.
|
|
382
|
+
- VCM does not write translation output into handoff artifacts unless a user or role explicitly copies it there.
|
|
383
|
+
- Role file writes happen in the task worktree when a task has a worktree.
|
|
243
384
|
- The safest sandbox today is a container or VM boundary controlled by the user.
|
|
244
385
|
|
|
245
386
|
See also:
|
|
@@ -247,4 +388,4 @@ See also:
|
|
|
247
388
|
- `docs/product-design.md`
|
|
248
389
|
- `docs/v1-architecture-design.md`
|
|
249
390
|
- `docs/v1-implementation-plan.md`
|
|
250
|
-
- `docs/
|
|
391
|
+
- `docs/cc-best-practices.md`
|
|
@@ -46,6 +46,19 @@ export function createNodeFileSystemAdapter() {
|
|
|
46
46
|
}
|
|
47
47
|
await this.writeText(targetPath, content);
|
|
48
48
|
return true;
|
|
49
|
+
},
|
|
50
|
+
async copyDir(sourcePath, targetPath) {
|
|
51
|
+
await fs.cp(sourcePath, targetPath, {
|
|
52
|
+
recursive: true,
|
|
53
|
+
force: false,
|
|
54
|
+
errorOnExist: false
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
async removePath(targetPath, options = {}) {
|
|
58
|
+
await fs.rm(targetPath, {
|
|
59
|
+
recursive: options.recursive ?? false,
|
|
60
|
+
force: options.force ?? false
|
|
61
|
+
});
|
|
49
62
|
}
|
|
50
63
|
};
|
|
51
64
|
}
|
|
@@ -22,6 +22,9 @@ export function createGitAdapter(runner) {
|
|
|
22
22
|
return result.stdout.trim() || "detached";
|
|
23
23
|
},
|
|
24
24
|
async isDirty(repoRoot) {
|
|
25
|
+
return (await this.getStatusPorcelain(repoRoot)).trim().length > 0;
|
|
26
|
+
},
|
|
27
|
+
async getStatusPorcelain(repoRoot) {
|
|
25
28
|
const result = await runGit(runner, repoRoot, ["status", "--porcelain"]);
|
|
26
29
|
if (result.exitCode !== 0) {
|
|
27
30
|
throw new VcmError({
|
|
@@ -31,7 +34,82 @@ export function createGitAdapter(runner) {
|
|
|
31
34
|
hint: result.stderr
|
|
32
35
|
});
|
|
33
36
|
}
|
|
34
|
-
return result.stdout
|
|
37
|
+
return result.stdout;
|
|
38
|
+
},
|
|
39
|
+
async isIgnored(repoRoot, repoRelativePath) {
|
|
40
|
+
const result = await runGit(runner, repoRoot, ["check-ignore", "-q", "--", repoRelativePath]);
|
|
41
|
+
if (result.exitCode === 0) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (result.exitCode === 1) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
throw new VcmError({
|
|
48
|
+
code: "GIT_ERROR",
|
|
49
|
+
message: `Unable to check whether Git ignores ${repoRelativePath}.`,
|
|
50
|
+
statusCode: 400,
|
|
51
|
+
hint: result.stderr
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
async branchExists(repoRoot, branch) {
|
|
55
|
+
const result = await runGit(runner, repoRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`]);
|
|
56
|
+
if (result.exitCode === 0) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
if (result.exitCode === 1) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
throw new VcmError({
|
|
63
|
+
code: "GIT_ERROR",
|
|
64
|
+
message: `Unable to check Git branch: ${branch}`,
|
|
65
|
+
statusCode: 400,
|
|
66
|
+
hint: result.stderr
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
async createWorktree(input) {
|
|
70
|
+
const result = await runGit(runner, input.repoRoot, [
|
|
71
|
+
"worktree",
|
|
72
|
+
"add",
|
|
73
|
+
"-b",
|
|
74
|
+
input.branch,
|
|
75
|
+
input.worktreePath,
|
|
76
|
+
input.baseRef ?? "HEAD"
|
|
77
|
+
]);
|
|
78
|
+
if (result.exitCode !== 0) {
|
|
79
|
+
throw new VcmError({
|
|
80
|
+
code: "GIT_WORKTREE_CREATE_FAILED",
|
|
81
|
+
message: `Unable to create task worktree: ${input.worktreePath}`,
|
|
82
|
+
statusCode: 400,
|
|
83
|
+
hint: result.stderr
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
async removeWorktree(repoRoot, worktreePath, options = {}) {
|
|
88
|
+
const args = ["worktree", "remove"];
|
|
89
|
+
if (options.force) {
|
|
90
|
+
args.push("--force");
|
|
91
|
+
}
|
|
92
|
+
args.push(worktreePath);
|
|
93
|
+
const result = await runGit(runner, repoRoot, args);
|
|
94
|
+
if (result.exitCode !== 0) {
|
|
95
|
+
throw new VcmError({
|
|
96
|
+
code: "GIT_WORKTREE_REMOVE_FAILED",
|
|
97
|
+
message: `Unable to remove task worktree: ${worktreePath}`,
|
|
98
|
+
statusCode: 400,
|
|
99
|
+
hint: result.stderr
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
async deleteBranch(repoRoot, branch, options = {}) {
|
|
104
|
+
const result = await runGit(runner, repoRoot, ["branch", options.force ? "-D" : "-d", branch]);
|
|
105
|
+
if (result.exitCode !== 0) {
|
|
106
|
+
throw new VcmError({
|
|
107
|
+
code: "GIT_BRANCH_DELETE_FAILED",
|
|
108
|
+
message: `Unable to delete Git branch: ${branch}`,
|
|
109
|
+
statusCode: 400,
|
|
110
|
+
hint: result.stderr
|
|
111
|
+
});
|
|
112
|
+
}
|
|
35
113
|
}
|
|
36
114
|
};
|
|
37
115
|
}
|
|
@@ -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
|
+
}
|