vibe-coding-master 0.0.12 → 0.0.14
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 +33 -14
- package/dist/backend/api/claude-hook-routes.js +5 -0
- package/dist/backend/api/message-routes.js +4 -0
- package/dist/backend/api/round-routes.js +33 -0
- package/dist/backend/api/session-routes.js +2 -0
- package/dist/backend/api/task-routes.js +1 -0
- package/dist/backend/server.js +27 -3
- package/dist/backend/services/app-settings-service.js +2 -1
- package/dist/backend/services/claude-hook-service.js +70 -0
- package/dist/backend/services/harness-service.js +95 -0
- package/dist/backend/services/message-service.js +222 -49
- package/dist/backend/services/project-service.js +1 -1
- package/dist/backend/services/round-service.js +117 -0
- package/dist/backend/services/session-service.js +39 -0
- package/dist/backend/services/status-service.js +0 -76
- package/dist/backend/services/task-service.js +14 -6
- package/dist/backend/templates/harness/architect-agent.js +5 -0
- package/dist/backend/templates/harness/claude-root.js +6 -0
- package/dist/backend/templates/harness/coder-agent.js +5 -0
- package/dist/backend/templates/harness/gitignore.js +2 -1
- package/dist/backend/templates/harness/project-manager-agent.js +7 -0
- package/dist/backend/templates/harness/reviewer-agent.js +5 -0
- package/dist/backend/templates/message-envelope.js +3 -1
- package/dist/backend/ws/terminal-ws.js +15 -1
- package/dist/cli/vcmctl.js +30 -0
- package/dist/shared/types/claude-hook.js +1 -0
- package/dist/shared/types/round.js +1 -0
- package/dist-frontend/assets/index-DVhkEVnA.js +89 -0
- package/dist-frontend/assets/index-jEkUTnIY.css +32 -0
- package/dist-frontend/index.html +2 -2
- package/docs/cc-best-practices.md +15 -4
- package/docs/product-design.md +71 -38
- package/docs/v1-architecture-design.md +108 -49
- package/docs/v1-implementation-plan.md +107 -32
- package/package.json +1 -1
- package/dist-frontend/assets/index-Bi4X3GSR.css +0 -32
- package/dist-frontend/assets/index-DaHXq14j.js +0 -88
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Each role runs as a real Claude Code process inside an embedded terminal. The GU
|
|
|
14
14
|
## Current V1 Capabilities
|
|
15
15
|
|
|
16
16
|
- GUI-first task workspace.
|
|
17
|
-
- Collapsible sidebar with repository connection,
|
|
17
|
+
- Collapsible sidebar with repository connection, settings, harness status, task creation, and task list.
|
|
18
18
|
- Recent repository path dropdown, stored locally with the five most recent paths.
|
|
19
19
|
- Embedded Claude Code terminals powered by `node-pty` and `xterm.js`.
|
|
20
20
|
- One Claude Code session per role, with role tabs in the task header.
|
|
@@ -156,8 +156,6 @@ project-manager
|
|
|
156
156
|
-> project-manager final acceptance, commit, and PR
|
|
157
157
|
```
|
|
158
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
159
|
## Task Worktree Management
|
|
162
160
|
|
|
163
161
|
VCM uses task-level worktree management by default:
|
|
@@ -170,7 +168,7 @@ The `Create worktree and branch` option is selected by default when creating a t
|
|
|
170
168
|
|
|
171
169
|
- task name: `<task>`
|
|
172
170
|
- branch: `feature/<task>`
|
|
173
|
-
- worktree path: `.
|
|
171
|
+
- worktree path: `.claude/worktrees/<task>` inside the connected base repository
|
|
174
172
|
- role session cwd: that task worktree
|
|
175
173
|
|
|
176
174
|
VCM will not create worktrees per role. `project-manager`, `architect`, `coder`, and `reviewer` for the same task share the same task worktree.
|
|
@@ -179,7 +177,7 @@ The user can turn this option off. In that mode, VCM creates the task metadata a
|
|
|
179
177
|
|
|
180
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 mode after creation.
|
|
181
179
|
|
|
182
|
-
Because worktrees live under `.
|
|
180
|
+
Because worktrees live under `.claude/worktrees/`, the connected repository must ignore both `.ai/vcm/` and `.claude/worktrees/`. 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`.
|
|
183
181
|
|
|
184
182
|
When a task is complete, VCM provides a red `Close Task` action. Closing a task shows a destructive confirmation, stops VCM-managed running role sessions for that task, then deletes the task worktree, deletes the task branch by default, removes the base task index entry, and removes task runtime metadata. VCM does not preflight running sessions or uncommitted changes before closing. Tasks created without a worktree only remove VCM metadata because they do not own a separate branch/worktree.
|
|
185
183
|
|
|
@@ -189,8 +187,7 @@ The left sidebar is intentionally compact and collapsible:
|
|
|
189
187
|
|
|
190
188
|
- `Repository Path`: path input on one row; `Recent` and `Connect` on the next row.
|
|
191
189
|
- `Repository`: connected path, branch, and working tree state. `Working tree: uncommitted changes` means `git status --porcelain` is not empty.
|
|
192
|
-
- `
|
|
193
|
-
- `Settings`: `Theme`, `Messages`, `Events`, and the `Auto orchestration` on/off toggle.
|
|
190
|
+
- `Settings`: `Theme`, `Round alert`, `Try alert`, `Messages`, and `Events`.
|
|
194
191
|
- `VCM Harness`: status for `CLAUDE.md`, role agent files, and `.gitignore`.
|
|
195
192
|
- `New Task`: one `task name` input.
|
|
196
193
|
- `Tasks`: task list and task status.
|
|
@@ -199,7 +196,7 @@ All sidebar sections are collapsed by default. When no task is selected, `Reposi
|
|
|
199
196
|
|
|
200
197
|
## Translation
|
|
201
198
|
|
|
202
|
-
The `
|
|
199
|
+
The role toolbar has an `Auto orchestration` on/off button immediately to the left of the `Translate` button. The `Translate` button opens a translation panel beside the embedded terminal. The terminal and translation panel split the available width evenly.
|
|
203
200
|
|
|
204
201
|
Translation settings are local and stored in:
|
|
205
202
|
|
|
@@ -211,6 +208,8 @@ The same file stores recent repository paths. The translation API key is stored
|
|
|
211
208
|
|
|
212
209
|
The sidebar `Settings` section also stores the UI theme preference in this file. The default is `system`, which follows the OS/browser color-scheme preference; users can cycle between `System`, `Light`, and `Dark`.
|
|
213
210
|
|
|
211
|
+
The same sidebar also has a `Round alert` toggle. It is on by default and controls the in-app prompt plus short sound that fires when VCM detects that the current full conversation round is complete. The `Try alert` button triggers the same local prompt and sound for testing.
|
|
212
|
+
|
|
214
213
|
Translation behavior:
|
|
215
214
|
|
|
216
215
|
- Provider type is OpenAI-compatible chat completions.
|
|
@@ -255,10 +254,13 @@ For `.gitignore`, VCM uses a gitignore-native managed block:
|
|
|
255
254
|
```gitignore
|
|
256
255
|
# VCM:BEGIN version=1
|
|
257
256
|
.ai/vcm/
|
|
257
|
+
.claude/worktrees/
|
|
258
258
|
# VCM:END
|
|
259
259
|
```
|
|
260
260
|
|
|
261
|
-
`.ai/vcm/` is the active VCM local control area. The base repo keeps the task index
|
|
261
|
+
`.ai/vcm/` is the active VCM local control area, and `.claude/worktrees/` is the Claude-compatible task worktree area. The base repo keeps the task index; each task runtime repo keeps its own session, message, orchestration, and translation state.
|
|
262
|
+
|
|
263
|
+
VCM also JSON-merges `.claude/settings.json` to install Claude Code `UserPromptSubmit` and `Stop` hooks. Those hooks call `vcmctl hook-event` through the session-provided `VCM_CTL_COMMAND`, so users do not need to configure hook commands manually.
|
|
262
264
|
|
|
263
265
|
After applying harness changes, VCM reports the exact files changed and reminds the user to review and commit them before starting long-running work.
|
|
264
266
|
|
|
@@ -301,14 +303,15 @@ The backend also keeps a compatibility role-command dispatch endpoint, but the p
|
|
|
301
303
|
|
|
302
304
|
## Orchestration Modes
|
|
303
305
|
|
|
304
|
-
VCM has a task-level `Auto orchestration` switch in the
|
|
306
|
+
VCM has a task-level `Auto orchestration` switch in the active role toolbar, immediately to the left of `Translate`.
|
|
305
307
|
|
|
306
308
|
When it is off, VCM is in manual mode:
|
|
307
309
|
|
|
308
310
|
- Roles may send messages through `vcmctl`.
|
|
309
311
|
- Messages appear in the `Messages` modal.
|
|
310
312
|
- The user can inspect them.
|
|
311
|
-
- The current GUI shows sequence, timestamp, status, body preview, path,
|
|
313
|
+
- The current GUI shows sequence, timestamp, status, body preview, path, `Copy`, and `Mark All Done`.
|
|
314
|
+
- `Mark All Done` marks open message records as `acknowledged` after the user manually copied or handled stuck queued/in-flight messages.
|
|
312
315
|
- The user decides what to do next by copying or manually acting on the message.
|
|
313
316
|
- VCM does not write to the target terminal or press Enter for the user.
|
|
314
317
|
|
|
@@ -317,12 +320,28 @@ When it is on, VCM is in auto mode:
|
|
|
317
320
|
- Backend policy still applies.
|
|
318
321
|
- PM can send work to `architect`, `coder`, or `reviewer`.
|
|
319
322
|
- Non-PM roles can reply only to `project-manager`.
|
|
320
|
-
- If the target role session is running, VCM writes a `[VCM MESSAGE]` envelope to the target terminal and submits it
|
|
323
|
+
- If the target role session is running and has no active message, VCM writes a `[VCM MESSAGE]` envelope to the target terminal and submits it as `delivering`.
|
|
324
|
+
- Claude Code `UserPromptSubmit` confirms the prompt really entered the target role session; VCM then marks the message `submitted`.
|
|
325
|
+
- When the GUI observes a newly submitted auto message, it switches the active role tab to that message's target role so the user can watch the next step.
|
|
326
|
+
- VCM enforces per-role turn-taking: each target role can have at most one in-flight `delivering`, `submitted`, `delivered`, or `staged` message.
|
|
327
|
+
- Additional messages to a busy target role are kept as `queued` and are not written to that terminal.
|
|
328
|
+
- When a role sends `vcmctl reply` or `vcmctl result`, VCM acknowledges that role's active message and delivers the next queued message for that role.
|
|
329
|
+
- If auto orchestration gets stuck after a manual copy/paste recovery, `Mark All Done` clears open `pending_approval`, `queued`, `staged`, `delivering`, `submitted`, `delivered`, and failed message records to `acknowledged`.
|
|
330
|
+
|
|
331
|
+
VCM Harness injects Claude Code `UserPromptSubmit` and `Stop` hooks into `.claude/settings.json`. Role tabs show `running` after `UserPromptSubmit` and `idle` after `Stop`; the terminal process status is still tracked separately. The injected role rules also require a role to end its turn after any successful `vcmctl send`, `vcmctl reply`, or `vcmctl result`, rather than running `vcmctl inbox`, looping, polling, or waiting for another role inside the same Claude Code turn.
|
|
321
332
|
|
|
322
333
|
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.
|
|
323
334
|
|
|
324
335
|
The backend still exposes stage/approve/reject compatibility APIs for automation and future UI work. They are not primary controls in the current Messages modal.
|
|
325
336
|
|
|
337
|
+
## Round Completion Alerts
|
|
338
|
+
|
|
339
|
+
VCM detects conversation completion from the hook-driven role activity state, not PTY silence. `UserPromptSubmit` marks a role `running`, and `Stop` marks that role `idle` with a stop timestamp.
|
|
340
|
+
|
|
341
|
+
For role chains, VCM waits for the latest active message's target role to reach hook `Stop`. For example, if PM sends work to Coder and Coder sends a result back to PM, the round is not complete when Coder finishes; it is complete only after PM reaches `Stop` for the final response. If no VCM role message is involved, the latest direct role `Stop` can complete the round.
|
|
342
|
+
|
|
343
|
+
When `Round alert` is enabled, the frontend polls the task round state, deduplicates each completion id, shows a small `Round complete` prompt, and plays a short local sound.
|
|
344
|
+
|
|
326
345
|
## Resume Behavior
|
|
327
346
|
|
|
328
347
|
Each role session stores its Claude session id and transcript path under:
|
|
@@ -345,7 +364,7 @@ For a connected repository, VCM uses:
|
|
|
345
364
|
```text
|
|
346
365
|
~/.vcm/projects/<project-id>/config.json
|
|
347
366
|
<baseRepoRoot>/.ai/vcm/tasks/<task>.json
|
|
348
|
-
<baseRepoRoot>/.
|
|
367
|
+
<baseRepoRoot>/.claude/worktrees/<task>/
|
|
349
368
|
<taskRepoRoot>/.ai/vcm/sessions/<task>.json
|
|
350
369
|
<taskRepoRoot>/.ai/vcm/messages/<task>.jsonl
|
|
351
370
|
<taskRepoRoot>/.ai/vcm/orchestration/<task>.json
|
|
@@ -359,7 +378,7 @@ For a connected repository, VCM uses:
|
|
|
359
378
|
<taskRepoRoot>/.ai/vcm/handoffs/logs/{project-manager,architect,coder,reviewer}.log
|
|
360
379
|
```
|
|
361
380
|
|
|
362
|
-
The project config is stored under `~/.vcm` so it is durable local app state and is not hidden inside a Git-ignored repository directory. For worktree-backed tasks, `taskRepoRoot` is `<baseRepoRoot>/.
|
|
381
|
+
The project config is stored under `~/.vcm` so it is durable local app state and is not hidden inside a Git-ignored repository directory. For worktree-backed tasks, `taskRepoRoot` is `<baseRepoRoot>/.claude/worktrees/<task>`; for inline tasks, `taskRepoRoot` is the connected base repo.
|
|
363
382
|
|
|
364
383
|
Because handoffs are scoped to `taskRepoRoot` without an extra task-name directory, VCM allows only one active inline task per connected repository. Use the default worktree mode for parallel tasks.
|
|
365
384
|
|
|
@@ -12,6 +12,10 @@ export function registerMessageRoutes(app, deps) {
|
|
|
12
12
|
...request.body
|
|
13
13
|
});
|
|
14
14
|
});
|
|
15
|
+
app.post("/api/tasks/:taskSlug/messages/mark-all-done", async (request) => {
|
|
16
|
+
const context = await getRouteContext(deps, request.params.taskSlug);
|
|
17
|
+
return deps.messageService.markAllDone(context);
|
|
18
|
+
});
|
|
15
19
|
app.post("/api/tasks/:taskSlug/messages/:messageId/stage", async (request) => {
|
|
16
20
|
const context = await getRouteContext(deps, request.params.taskSlug);
|
|
17
21
|
return deps.messageService.stageMessage({
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { VcmError } from "../errors.js";
|
|
2
|
+
import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
|
|
3
|
+
export function registerRoundRoutes(app, deps) {
|
|
4
|
+
app.get("/api/tasks/:taskSlug/round", async (request) => {
|
|
5
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
6
|
+
const config = await deps.projectService.loadConfig(project.repoRoot);
|
|
7
|
+
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
8
|
+
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
9
|
+
const sessions = await deps.sessionService.listRoleSessions(project.repoRoot, request.params.taskSlug);
|
|
10
|
+
const messages = await deps.messageService.listMessages({
|
|
11
|
+
repoRoot: project.repoRoot,
|
|
12
|
+
stateRepoRoot: taskRepoRoot,
|
|
13
|
+
stateRoot: config.stateRoot,
|
|
14
|
+
taskSlug: request.params.taskSlug
|
|
15
|
+
});
|
|
16
|
+
return deps.roundService.getTaskRoundState({
|
|
17
|
+
taskSlug: request.params.taskSlug,
|
|
18
|
+
sessions,
|
|
19
|
+
messages
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async function requireCurrentProject(projectService) {
|
|
24
|
+
const project = await projectService.getCurrentProject();
|
|
25
|
+
if (!project) {
|
|
26
|
+
throw new VcmError({
|
|
27
|
+
code: "PROJECT_NOT_CONNECTED",
|
|
28
|
+
message: "Connect a repository first.",
|
|
29
|
+
statusCode: 409
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return project;
|
|
33
|
+
}
|
|
@@ -15,6 +15,7 @@ export function registerSessionRoutes(app, deps) {
|
|
|
15
15
|
const role = parseRole(request.params.role);
|
|
16
16
|
const session = await deps.sessionService.stopRoleSession(project.repoRoot, request.params.taskSlug, role);
|
|
17
17
|
await deps.translationService.stopSession(session.id);
|
|
18
|
+
deps.roundService.stopSession(session.id);
|
|
18
19
|
return session;
|
|
19
20
|
});
|
|
20
21
|
app.post("/api/tasks/:taskSlug/sessions/:role/restart", async (request) => {
|
|
@@ -23,6 +24,7 @@ export function registerSessionRoutes(app, deps) {
|
|
|
23
24
|
const existing = await deps.sessionService.getRoleSession(project.repoRoot, request.params.taskSlug, role);
|
|
24
25
|
if (existing) {
|
|
25
26
|
await deps.translationService.stopSession(existing.id, { clearCache: true });
|
|
27
|
+
deps.roundService.stopSession(existing.id);
|
|
26
28
|
}
|
|
27
29
|
return deps.sessionService.restartRoleSession(project.repoRoot, request.params.taskSlug, role, request.body);
|
|
28
30
|
});
|
|
@@ -22,6 +22,7 @@ export function registerTaskRoutes(app, deps) {
|
|
|
22
22
|
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
23
23
|
await stopRunningRoleSessions(deps, project.repoRoot, request.params.taskSlug);
|
|
24
24
|
await deps.translationService.stopTask(getTaskRuntimeRepoRoot(task), request.params.taskSlug, { clearCache: true });
|
|
25
|
+
deps.roundService.stopTask(request.params.taskSlug);
|
|
25
26
|
return deps.taskService.cleanupTask(project.repoRoot, request.params.taskSlug, request.body ?? {});
|
|
26
27
|
});
|
|
27
28
|
}
|
package/dist/backend/server.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createArtifactService } from "./services/artifact-service.js";
|
|
|
7
7
|
import { createClaudeAdapter } from "./adapters/claude-adapter.js";
|
|
8
8
|
import { createCommandRunner } from "./adapters/command-runner.js";
|
|
9
9
|
import { createCommandDispatcher } from "./services/command-dispatcher.js";
|
|
10
|
+
import { createClaudeHookService } from "./services/claude-hook-service.js";
|
|
10
11
|
import { createGitAdapter } from "./adapters/git-adapter.js";
|
|
11
12
|
import { createAppSettingsService } from "./services/app-settings-service.js";
|
|
12
13
|
import { createClaudeTranscriptService } from "./services/claude-transcript-service.js";
|
|
@@ -18,14 +19,17 @@ import { createProjectService } from "./services/project-service.js";
|
|
|
18
19
|
import { createSessionRegistry } from "./runtime/session-registry.js";
|
|
19
20
|
import { createSessionService } from "./services/session-service.js";
|
|
20
21
|
import { createMessageService } from "./services/message-service.js";
|
|
22
|
+
import { createRoundService } from "./services/round-service.js";
|
|
21
23
|
import { createStatusService } from "./services/status-service.js";
|
|
22
24
|
import { createTaskService } from "./services/task-service.js";
|
|
23
25
|
import { createTranslationService } from "./services/translation-service.js";
|
|
24
26
|
import { registerAppSettingsRoutes } from "./api/app-settings-routes.js";
|
|
25
27
|
import { registerArtifactRoutes } from "./api/artifact-routes.js";
|
|
28
|
+
import { registerClaudeHookRoutes } from "./api/claude-hook-routes.js";
|
|
26
29
|
import { registerHarnessRoutes } from "./api/harness-routes.js";
|
|
27
30
|
import { registerMessageRoutes } from "./api/message-routes.js";
|
|
28
31
|
import { registerProjectRoutes } from "./api/project-routes.js";
|
|
32
|
+
import { registerRoundRoutes } from "./api/round-routes.js";
|
|
29
33
|
import { registerSessionRoutes } from "./api/session-routes.js";
|
|
30
34
|
import { registerTaskRoutes } from "./api/task-routes.js";
|
|
31
35
|
import { registerTranslationRoutes } from "./api/translation-routes.js";
|
|
@@ -46,6 +50,7 @@ export async function createServer(deps, options = {}) {
|
|
|
46
50
|
});
|
|
47
51
|
});
|
|
48
52
|
registerAppSettingsRoutes(app, { appSettings: deps.appSettings });
|
|
53
|
+
registerClaudeHookRoutes(app, { claudeHookService: deps.claudeHookService });
|
|
49
54
|
registerProjectRoutes(app, { projectService: deps.projectService });
|
|
50
55
|
registerHarnessRoutes(app, {
|
|
51
56
|
projectService: deps.projectService,
|
|
@@ -56,13 +61,15 @@ export async function createServer(deps, options = {}) {
|
|
|
56
61
|
taskService: deps.taskService,
|
|
57
62
|
sessionService: deps.sessionService,
|
|
58
63
|
statusService: deps.statusService,
|
|
59
|
-
translationService: deps.translationService
|
|
64
|
+
translationService: deps.translationService,
|
|
65
|
+
roundService: deps.roundService
|
|
60
66
|
});
|
|
61
67
|
registerSessionRoutes(app, {
|
|
62
68
|
projectService: deps.projectService,
|
|
63
69
|
sessionService: deps.sessionService,
|
|
64
70
|
commandDispatcher: deps.commandDispatcher,
|
|
65
|
-
translationService: deps.translationService
|
|
71
|
+
translationService: deps.translationService,
|
|
72
|
+
roundService: deps.roundService
|
|
66
73
|
});
|
|
67
74
|
registerArtifactRoutes(app, {
|
|
68
75
|
projectService: deps.projectService,
|
|
@@ -74,6 +81,13 @@ export async function createServer(deps, options = {}) {
|
|
|
74
81
|
taskService: deps.taskService,
|
|
75
82
|
messageService: deps.messageService
|
|
76
83
|
});
|
|
84
|
+
registerRoundRoutes(app, {
|
|
85
|
+
projectService: deps.projectService,
|
|
86
|
+
taskService: deps.taskService,
|
|
87
|
+
sessionService: deps.sessionService,
|
|
88
|
+
messageService: deps.messageService,
|
|
89
|
+
roundService: deps.roundService
|
|
90
|
+
});
|
|
77
91
|
registerTranslationRoutes(app, {
|
|
78
92
|
projectService: deps.projectService,
|
|
79
93
|
taskService: deps.taskService,
|
|
@@ -146,10 +160,18 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
146
160
|
sessionService,
|
|
147
161
|
taskService
|
|
148
162
|
});
|
|
163
|
+
const claudeHookService = createClaudeHookService({
|
|
164
|
+
projectService,
|
|
165
|
+
taskService,
|
|
166
|
+
sessionService,
|
|
167
|
+
messageService
|
|
168
|
+
});
|
|
169
|
+
const transcripts = createClaudeTranscriptService();
|
|
170
|
+
const roundService = createRoundService();
|
|
149
171
|
const translationService = createTranslationService({
|
|
150
172
|
runtime,
|
|
151
173
|
sessionRegistry: registry,
|
|
152
|
-
transcripts
|
|
174
|
+
transcripts,
|
|
153
175
|
sessionService,
|
|
154
176
|
fs,
|
|
155
177
|
projectService,
|
|
@@ -164,7 +186,9 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
164
186
|
artifactService,
|
|
165
187
|
harnessService,
|
|
166
188
|
commandDispatcher,
|
|
189
|
+
claudeHookService,
|
|
167
190
|
messageService,
|
|
191
|
+
roundService,
|
|
168
192
|
statusService,
|
|
169
193
|
translationService,
|
|
170
194
|
runtime
|
|
@@ -180,7 +180,8 @@ function normalizeSettingsFile(input) {
|
|
|
180
180
|
function normalizePreferences(input) {
|
|
181
181
|
const candidate = isObject(input) ? input : {};
|
|
182
182
|
return {
|
|
183
|
-
themeMode: normalizeThemeMode(candidate.themeMode)
|
|
183
|
+
themeMode: normalizeThemeMode(candidate.themeMode),
|
|
184
|
+
roundCompletionAlerts: candidate.roundCompletionAlerts !== false
|
|
184
185
|
};
|
|
185
186
|
}
|
|
186
187
|
function normalizeThemeMode(input) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { isRoleName } from "../../shared/constants.js";
|
|
2
|
+
import { VcmError } from "../errors.js";
|
|
3
|
+
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
4
|
+
export function createClaudeHookService(deps) {
|
|
5
|
+
return {
|
|
6
|
+
async handleHook(input) {
|
|
7
|
+
if (!isRoleName(input.role)) {
|
|
8
|
+
throw new VcmError({
|
|
9
|
+
code: "HOOK_ROLE_INVALID",
|
|
10
|
+
message: `Unknown hook role: ${input.role}`,
|
|
11
|
+
statusCode: 400
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
const eventName = parseHookEventName(input.event.hook_event_name);
|
|
15
|
+
const project = await deps.projectService.getCurrentProject();
|
|
16
|
+
if (!project) {
|
|
17
|
+
throw new VcmError({
|
|
18
|
+
code: "PROJECT_NOT_CONNECTED",
|
|
19
|
+
message: "Connect a repository before accepting Claude Code hooks.",
|
|
20
|
+
statusCode: 409
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const session = await deps.sessionService.recordClaudeHookEvent(project.repoRoot, {
|
|
24
|
+
taskSlug: input.taskSlug,
|
|
25
|
+
role: input.role,
|
|
26
|
+
eventName,
|
|
27
|
+
claudeSessionId: stringOrUndefined(input.event.session_id),
|
|
28
|
+
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
29
|
+
cwd: stringOrUndefined(input.event.cwd)
|
|
30
|
+
});
|
|
31
|
+
let submittedMessageId;
|
|
32
|
+
if (eventName === "UserPromptSubmit" && typeof input.event.prompt === "string") {
|
|
33
|
+
const config = await deps.projectService.loadConfig(project.repoRoot);
|
|
34
|
+
const task = await deps.taskService.loadTask(project.repoRoot, input.taskSlug);
|
|
35
|
+
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
36
|
+
const submitted = await deps.messageService.confirmPromptSubmitted({
|
|
37
|
+
repoRoot: project.repoRoot,
|
|
38
|
+
stateRepoRoot: taskRepoRoot,
|
|
39
|
+
stateRoot: config.stateRoot,
|
|
40
|
+
taskSlug: input.taskSlug,
|
|
41
|
+
role: input.role,
|
|
42
|
+
prompt: input.event.prompt
|
|
43
|
+
});
|
|
44
|
+
submittedMessageId = submitted?.id;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
ok: true,
|
|
48
|
+
eventName,
|
|
49
|
+
taskSlug: input.taskSlug,
|
|
50
|
+
role: input.role,
|
|
51
|
+
sessionUpdated: Boolean(session),
|
|
52
|
+
submittedMessageId
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function parseHookEventName(value) {
|
|
58
|
+
if (value === "UserPromptSubmit" || value === "Stop") {
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
throw new VcmError({
|
|
62
|
+
code: "HOOK_EVENT_UNSUPPORTED",
|
|
63
|
+
message: `Unsupported Claude Code hook event: ${String(value)}`,
|
|
64
|
+
statusCode: 400,
|
|
65
|
+
hint: "VCM currently accepts UserPromptSubmit and Stop hooks only."
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function stringOrUndefined(value) {
|
|
69
|
+
return typeof value === "string" ? value : undefined;
|
|
70
|
+
}
|
|
@@ -8,6 +8,9 @@ import { renderReviewerHarnessRules } from "../templates/harness/reviewer-agent.
|
|
|
8
8
|
export const VCM_HARNESS_VERSION = 1;
|
|
9
9
|
const MANAGED_BLOCK_PATTERN = /<!-- VCM:BEGIN(?:\s+version=(\d+))? -->[\s\S]*?<!-- VCM:END -->/m;
|
|
10
10
|
const HASH_MANAGED_BLOCK_PATTERN = /# VCM:BEGIN(?:\s+version=(\d+))?\n[\s\S]*?# VCM:END/m;
|
|
11
|
+
const CLAUDE_SETTINGS_PATH = ".claude/settings.json";
|
|
12
|
+
const VCM_HOOK_COMMAND = "sh -c 'if [ -n \"${VCM_CTL_COMMAND:-}\" ]; then eval \"$VCM_CTL_COMMAND hook-event\"; else vcmctl hook-event; fi'";
|
|
13
|
+
const VCM_HOOK_EVENTS = ["UserPromptSubmit", "Stop"];
|
|
11
14
|
const HARNESS_FILES = [
|
|
12
15
|
{
|
|
13
16
|
kind: "root-claude",
|
|
@@ -84,6 +87,7 @@ async function analyzeHarnessFiles(fs, repoRoot) {
|
|
|
84
87
|
for (const definition of HARNESS_FILES) {
|
|
85
88
|
analyses.push(await analyzeHarnessFile(fs, repoRoot, definition));
|
|
86
89
|
}
|
|
90
|
+
analyses.push(await analyzeClaudeSettingsFile(fs, repoRoot));
|
|
87
91
|
return analyses;
|
|
88
92
|
}
|
|
89
93
|
async function analyzeHarnessFile(fs, repoRoot, definition) {
|
|
@@ -188,6 +192,97 @@ function renderNewHarnessFile(definition, block) {
|
|
|
188
192
|
: "";
|
|
189
193
|
return `${frontmatter}# ${definition.title}\n\n${block}\n`;
|
|
190
194
|
}
|
|
195
|
+
async function analyzeClaudeSettingsFile(fs, repoRoot) {
|
|
196
|
+
const definition = {
|
|
197
|
+
kind: "claude-settings",
|
|
198
|
+
path: CLAUDE_SETTINGS_PATH,
|
|
199
|
+
title: "Claude Code Settings",
|
|
200
|
+
renderRules: () => ""
|
|
201
|
+
};
|
|
202
|
+
const absolutePath = resolveHarnessPath(repoRoot, CLAUDE_SETTINGS_PATH);
|
|
203
|
+
const exists = await fs.pathExists(absolutePath);
|
|
204
|
+
const current = exists
|
|
205
|
+
? parseJsonObject(await fs.readText(absolutePath))
|
|
206
|
+
: {};
|
|
207
|
+
const next = withVcmClaudeHooks(current);
|
|
208
|
+
const currentContent = exists
|
|
209
|
+
? `${JSON.stringify(current, null, 2)}\n`
|
|
210
|
+
: "";
|
|
211
|
+
const nextContent = `${JSON.stringify(next, null, 2)}\n`;
|
|
212
|
+
const action = exists
|
|
213
|
+
? currentContent === nextContent ? "ok" : "update"
|
|
214
|
+
: "create";
|
|
215
|
+
return {
|
|
216
|
+
definition,
|
|
217
|
+
status: {
|
|
218
|
+
kind: "claude-settings",
|
|
219
|
+
path: CLAUDE_SETTINGS_PATH,
|
|
220
|
+
exists,
|
|
221
|
+
hasManagedBlock: false,
|
|
222
|
+
action
|
|
223
|
+
},
|
|
224
|
+
plannedChange: action === "ok"
|
|
225
|
+
? undefined
|
|
226
|
+
: {
|
|
227
|
+
path: CLAUDE_SETTINGS_PATH,
|
|
228
|
+
action,
|
|
229
|
+
reason: exists
|
|
230
|
+
? "Claude Code hook settings do not contain the VCM UserPromptSubmit/Stop hook bridge."
|
|
231
|
+
: "Claude Code hook settings are missing; VCM will create them."
|
|
232
|
+
},
|
|
233
|
+
nextContent: action === "ok" ? undefined : nextContent
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function parseJsonObject(content) {
|
|
237
|
+
const parsed = JSON.parse(content);
|
|
238
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
239
|
+
return {};
|
|
240
|
+
}
|
|
241
|
+
return parsed;
|
|
242
|
+
}
|
|
243
|
+
function withVcmClaudeHooks(settings) {
|
|
244
|
+
const hooks = isPlainObject(settings.hooks)
|
|
245
|
+
? { ...settings.hooks }
|
|
246
|
+
: {};
|
|
247
|
+
for (const eventName of VCM_HOOK_EVENTS) {
|
|
248
|
+
const existingMatchers = Array.isArray(hooks[eventName])
|
|
249
|
+
? hooks[eventName]
|
|
250
|
+
: [];
|
|
251
|
+
hooks[eventName] = [
|
|
252
|
+
...existingMatchers.filter((entry) => !isVcmHookMatcher(entry)),
|
|
253
|
+
createVcmHookMatcher()
|
|
254
|
+
];
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
...settings,
|
|
258
|
+
hooks
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function createVcmHookMatcher() {
|
|
262
|
+
return {
|
|
263
|
+
hooks: [
|
|
264
|
+
{
|
|
265
|
+
type: "command",
|
|
266
|
+
command: VCM_HOOK_COMMAND,
|
|
267
|
+
timeout: 5
|
|
268
|
+
}
|
|
269
|
+
]
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function isVcmHookMatcher(value) {
|
|
273
|
+
if (!isPlainObject(value) || !Array.isArray(value.hooks)) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
return value.hooks.some((hook) => {
|
|
277
|
+
if (!isPlainObject(hook)) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
return typeof hook.command === "string" && hook.command.includes("hook-event");
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
function isPlainObject(value) {
|
|
284
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
285
|
+
}
|
|
191
286
|
function renderAgentFrontmatter(name, description) {
|
|
192
287
|
return `---\nname: ${name}\ndescription: ${description}\ntools: Read, Grep, Glob, Bash, Edit, Write\n---`;
|
|
193
288
|
}
|