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.
Files changed (37) hide show
  1. package/README.md +33 -14
  2. package/dist/backend/api/claude-hook-routes.js +5 -0
  3. package/dist/backend/api/message-routes.js +4 -0
  4. package/dist/backend/api/round-routes.js +33 -0
  5. package/dist/backend/api/session-routes.js +2 -0
  6. package/dist/backend/api/task-routes.js +1 -0
  7. package/dist/backend/server.js +27 -3
  8. package/dist/backend/services/app-settings-service.js +2 -1
  9. package/dist/backend/services/claude-hook-service.js +70 -0
  10. package/dist/backend/services/harness-service.js +95 -0
  11. package/dist/backend/services/message-service.js +222 -49
  12. package/dist/backend/services/project-service.js +1 -1
  13. package/dist/backend/services/round-service.js +117 -0
  14. package/dist/backend/services/session-service.js +39 -0
  15. package/dist/backend/services/status-service.js +0 -76
  16. package/dist/backend/services/task-service.js +14 -6
  17. package/dist/backend/templates/harness/architect-agent.js +5 -0
  18. package/dist/backend/templates/harness/claude-root.js +6 -0
  19. package/dist/backend/templates/harness/coder-agent.js +5 -0
  20. package/dist/backend/templates/harness/gitignore.js +2 -1
  21. package/dist/backend/templates/harness/project-manager-agent.js +7 -0
  22. package/dist/backend/templates/harness/reviewer-agent.js +5 -0
  23. package/dist/backend/templates/message-envelope.js +3 -1
  24. package/dist/backend/ws/terminal-ws.js +15 -1
  25. package/dist/cli/vcmctl.js +30 -0
  26. package/dist/shared/types/claude-hook.js +1 -0
  27. package/dist/shared/types/round.js +1 -0
  28. package/dist-frontend/assets/index-DVhkEVnA.js +89 -0
  29. package/dist-frontend/assets/index-jEkUTnIY.css +32 -0
  30. package/dist-frontend/index.html +2 -2
  31. package/docs/cc-best-practices.md +15 -4
  32. package/docs/product-design.md +71 -38
  33. package/docs/v1-architecture-design.md +108 -49
  34. package/docs/v1-implementation-plan.md +107 -32
  35. package/package.json +1 -1
  36. package/dist-frontend/assets/index-Bi4X3GSR.css +0 -32
  37. 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, workflow, settings, harness status, task creation, and task list.
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: `.ai/vcm/worktrees/<task>` inside the connected base repository
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 `.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`.
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
- - `Workflow`: current soft gate and five workflow steps.
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 `Translate` button in the role toolbar opens a translation panel beside the embedded terminal. The terminal and translation panel split the available width evenly.
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 and nested worktrees; each task runtime repo keeps its own session, message, orchestration, and translation state.
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 sidebar `Settings` section.
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, and a `Copy` button for each message.
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>/.ai/vcm/worktrees/<task>/
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>/.ai/vcm/worktrees/<task>`; for inline tasks, `taskRepoRoot` is the connected base repo.
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
 
@@ -0,0 +1,5 @@
1
+ export function registerClaudeHookRoutes(app, deps) {
2
+ app.post("/api/hooks/claude-code", async (request) => {
3
+ return deps.claudeHookService.handleHook(request.body);
4
+ });
5
+ }
@@ -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
  }
@@ -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: createClaudeTranscriptService(),
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
  }