vibe-coding-master 0.3.27 → 0.3.29

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 CHANGED
@@ -11,7 +11,7 @@ VCM is designed for long-running coding work where one Claude Code conversation
11
11
 
12
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
- When Gate Review Gates are enabled for a task, or when a Gate Reviewer session already exists, the workspace can also show a fifth `Gate Reviewer` terminal role. It runs Claude Code as a project-scoped long-lived review session, receives short gate prompts that include the current task and worktree path, writes reports under the task worktree, and stays outside normal PM role routing.
14
+ When Gate Review Gates are enabled for a task, or when a Gate Reviewer session already exists, the workspace can also show a fifth `Gate Reviewer` terminal role. It is an optional VCM flow role powered by Claude Code. Its session is project-scoped and reusable across tasks, but each gate turn is bound to the current task/worktree, writes reports under the task worktree, and stays outside normal PM route-file dispatch.
15
15
 
16
16
  ## Current Capabilities
17
17
 
@@ -21,7 +21,7 @@ When Gate Review Gates are enabled for a task, or when a Gate Reviewer session a
21
21
  - Connected repository status for the base repo, including branch, upstream status, commit hash, dirty state, and fast-forward-only pull.
22
22
  - Embedded Claude Code terminals powered by `node-pty` and `xterm.js`.
23
23
  - One Claude Code session per role, with role tabs in the task header.
24
- - Optional Gate Reviewer terminal role when any Gate Review Gate is enabled.
24
+ - Optional Gate Reviewer VCM flow role when any Gate Review Gate is enabled.
25
25
  - Role session recovery through persisted Claude session ids and `claude --resume`.
26
26
  - Permission mode selection before start, resume, or restart:
27
27
  - `default`
@@ -33,7 +33,7 @@ When Gate Review Gates are enabled for a task, or when a Gate Reviewer session a
33
33
  - Rust generated context for module indexing and crate-external public surface indexing.
34
34
  - Translation panel powered by the long-lived Codex Translator session.
35
35
  - Mobile Gateway through Tencent iLink Bot API / Weixin DM, for talking to PM and managing tasks from Weixin.
36
- - Durable task state, session state, raw terminal logs, handoff artifacts, and message history.
36
+ - Durable task state, role session state, handoff artifacts, and message history.
37
37
 
38
38
  ## Requirements
39
39
 
@@ -193,14 +193,12 @@ In this terminology, `Session` is a VCM statistics term. It is different from a
193
193
 
194
194
  ## Task Worktree Management
195
195
 
196
- VCM uses task-level worktree management by default:
196
+ VCM uses task-level worktree management for every task:
197
197
 
198
198
  ```text
199
199
  one task = one branch + one git worktree + one handoff directory + one role-session set
200
200
  ```
201
201
 
202
- The `Create worktree and branch` option is selected by default when creating a task:
203
-
204
202
  - task name: `<task>`
205
203
  - branch: `feature/<task>`
206
204
  - worktree path: `.claude/worktrees/<task>` inside the connected base repository
@@ -208,13 +206,11 @@ The `Create worktree and branch` option is selected by default when creating a t
208
206
 
209
207
  VCM will not create worktrees per role. `project-manager`, `architect`, `coder`, and `reviewer` for the same task share the same task worktree.
210
208
 
211
- The user can turn this option off. In that mode, VCM creates app-local task metadata, creates the handoff structure in the connected repository, records the current branch, and starts role sessions from the connected repository path.
212
-
213
209
  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.
214
210
 
215
211
  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`.
216
212
 
217
- 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 app-local task record, 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.
213
+ 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 app-local task record, and removes task runtime metadata. VCM does not preflight running sessions or uncommitted changes before closing. Project-scoped Gate Reviewer and Codex Translator sessions are not task-owned and are not stopped by Close Task.
218
214
 
219
215
  ## Sidebar UI
220
216
 
@@ -223,6 +219,8 @@ The left sidebar is intentionally compact and collapsible:
223
219
  - `Repository Path`: path input on one row; `Recent` and `Connect` on the next row.
224
220
  - `Connected Repository`: connected base repo path, branch, upstream/ahead-behind status, commit hash, working tree state, and a `Pull` button.
225
221
  - `Settings`: `Theme`, `Flow pause alert`, `Try alert`, `Messages`, and `Events`.
222
+ - `Translation`: global conversation translation, auto-send, target language, output scope, file translation, bootstrap, memory update, session status, and Codex Translator session access.
223
+ - `Gate Review Gates`: global gate switches for architecture plan, validation adequacy, and final diff.
226
224
  - `Gateway`: Weixin iLink binding, Gateway on/off, Gateway translation, and QR login.
227
225
  - `VCM Harness`: fixed-install status, bootstrap completion checks, and the bootstrap terminal when one is running.
228
226
  - `New Task`: one `task name` input.
@@ -233,9 +231,8 @@ All sidebar sections are collapsed by default. When no task is selected, `Reposi
233
231
  Opening `Connected Repository` refreshes the base repo status through the
234
232
  backend. VCM does not poll it continuously. The `Pull` button runs
235
233
  `git pull --ff-only` against the connected base repo only. It is disabled when
236
- the base repo has uncommitted changes, when the current branch has no upstream,
237
- or when the active task is an inline task using the base repo directly. It does
238
- not stash, merge, or mutate task worktrees.
234
+ the base repo has uncommitted changes or when the current branch has no
235
+ upstream. It does not stash, merge, or mutate task worktrees.
239
236
 
240
237
  When VCM is connected to an active task, the bottom of the sidebar shows a task status dock. It stays outside the collapsible groups and shows the active VCM Session title, task status, start time, total elapsed time, total Round count, and role active runtime. The dock also shows the Current Round while it is running, or the Last Round after a flow pause, including start time, total elapsed time, role active runtime, Turn count, and Round status.
241
238
 
@@ -353,14 +350,14 @@ Typical mobile flow:
353
350
  - `/pull-current`: runs the same fast-forward-only connected repository pull as the desktop `Pull` button.
354
351
  - `/tasks`: lists tasks for the selected project.
355
352
  - `/use-task <index-or-task-slug>`: selects the Gateway's current task context.
356
- - `/create-task <task-slug> [title]`: creates a worktree-backed task and starts the four role sessions using the saved launch template.
353
+ - `/create-task <task-slug> [title]`: creates a worktree-backed task and starts the four core role sessions using the saved launch template. If any Gate Review Gate is enabled, Gateway also resumes or starts Gate Reviewer from the saved template.
357
354
  - `/close-task`: starts a destructive close confirmation for the current task.
358
355
  - `/close-task confirm <task-slug>`: closes the task through VCM cleanup after exact slug confirmation.
359
356
  - `/translate on` and `/translate off`: changes Gateway translation for mobile messages.
360
357
 
361
358
  `/pull-current` only pulls the connected base repository. It does not pull task worktrees, stash local changes, merge divergent branches, or run arbitrary shell commands.
362
359
 
363
- `/create-task` uses the saved launch template from the desktop settings. The template controls permission mode, model, effort, auto orchestration, and translation defaults for the four role sessions.
360
+ `/create-task` uses the saved launch template from the desktop settings. The template controls permission mode, model, effort, and auto orchestration for the four core role sessions plus the optional Gate Reviewer. Gateway translation uses the global Gateway translation setting, not the launch template.
364
361
 
365
362
  `/close-task` is destructive. It stops VCM-managed role sessions and removes task-owned worktree/branch state according to the same cleanup behavior as the desktop `Close Task` action.
366
363
 
@@ -378,7 +375,7 @@ Typical mobile flow:
378
375
 
379
376
  ## Translation
380
377
 
381
- The task header has a global `Translate` button next to `Close Task`. It opens a translation panel beside the embedded terminal for the role consoles and keeps the same on/off setting while switching roles. The terminal and translation panel split the available width evenly.
378
+ Translation is controlled from the sidebar `Translation` group. When conversation translation is enabled, each running core VCM role console shows a translation panel beside the embedded terminal. The terminal and translation panel split the available width evenly.
382
379
 
383
380
  The task header does not include a manual `Refresh` button. Task status, role status, messages, orchestration state, and flow pause state refresh automatically. The remaining `Refresh` button lives only in the sidebar `VCM Harness` section and is for rechecking harness files.
384
381
 
@@ -401,7 +398,7 @@ When Gateway is on, `Flow pause alert` is forced off because mobile notification
401
398
  Translation behavior:
402
399
 
403
400
  - Conversation translation is routed through the Codex Translator session and result files.
404
- - Global translation controls live in the sidebar Translation section: enablement, auto-send, target language, bootstrap, memory update, and file translation.
401
+ - Global translation controls live in the sidebar Translation section: enablement, auto-send, target language, output scope, bootstrap, memory update, file translation, session status, and `Open Session`.
405
402
  - File and conversation translation share `<baseRepoRoot>/.ai/vcm/translations/`; conversation result files are temporary runtime artifacts.
406
403
  - Claude Code output translation reads semantic Claude transcript JSONL files under `~/.claude/projects`, not raw PTY output.
407
404
  - Claude Code prose output waits 10 seconds before dispatch so adjacent output can be translated in one Codex batch.
@@ -415,10 +412,10 @@ Translation behavior:
415
412
  - Assistant prose renders Markdown in the panel, including headings, lists, code fences, tables, and links.
416
413
  - Tool calls and tool results are preserved as dim one-line rows such as `● Bash({"command":"npm test"})`.
417
414
  - User input uses one textarea. Press `Enter` to translate or send the current English draft; press `Shift+Enter` for a newline.
418
- - After user input is translated, the translated draft is appended after the original text in the same textarea.
415
+ - After user input is translated, the English draft is appended after the original source text in the same textarea.
419
416
  - `Send English` writes the current English draft to the active embedded terminal and submits it.
420
417
  - Automatic terminal submission uses bracketed paste first, then sends Enter separately for Claude Code TUI reliability.
421
- - The translation panel `Auto-send` toggle sends the translated draft automatically when translation succeeds without warnings.
418
+ - The sidebar `Auto-send` toggle sends the translated draft automatically when translation succeeds without warnings.
422
419
 
423
420
  ## Project Harness
424
421
 
@@ -438,7 +435,13 @@ CLAUDE.md
438
435
  .claude/skills/vcm-final-acceptance/SKILL.md
439
436
  .claude/skills/vcm-long-running-validation/SKILL.md
440
437
  .claude/skills/vcm-harness-bootstrap/SKILL.md
441
- .ai/vcm-harness-manifest.json
438
+ .claude/skills/vcm-gate-review/SKILL.md
439
+ .claude/agents/gate-reviewer.md
440
+ .ai/codex-translator/AGENTS.md
441
+ .ai/codex-translator/config.toml
442
+ .ai/codex-translator/.codex/config.toml
443
+ .ai/codex-translator/.codex/hooks.json
444
+ .ai/tools/request-gate-review
442
445
  .ai/tools/generate-module-index
443
446
  .ai/tools/generate-public-surface
444
447
  .ai/tools/run-long-check
@@ -536,7 +539,6 @@ Runtime message and handoff files:
536
539
  .ai/vcm/messages/<task>.jsonl # under the task runtime repo
537
540
  .ai/vcm/orchestration/<task>.json # under the task runtime repo
538
541
  .ai/vcm/handoffs/messages/<from-role>-<to-role>.md
539
- .ai/vcm/handoffs/logs/
540
542
  ```
541
543
 
542
544
  Each directed role route has exactly one message file. Route messages are the only dynamic task-dispatch files. If a role changes its mind during one turn, it edits the same route file instead of creating another message. A blank file means no pending message; a non-empty file means pending work for VCM to submit.
@@ -572,7 +574,7 @@ When it is on, VCM is in auto mode:
572
574
  - When the target role later reaches `Stop`, VCM scans again and may deliver the next pending route file.
573
575
  - If auto orchestration gets stuck after a manual copy/paste recovery, `Mark All Done` clears pending route files. It does not mutate message history.
574
576
 
575
- VCM Harness injects Claude Code `UserPromptSubmit` and `Stop` hooks into `.claude/settings.json`. Role tabs become `running` when Claude Code accepts a prompt and `idle` after `Stop`; VCM also marks a role `running` immediately after it writes a message to that embedded terminal. Gate Reviewer is a Claude Code role, but VCM triggers and settles gate turns directly around the gate prompt and report callback. The terminal process status is still tracked separately. Claude role rules require a role to end its turn after writing or updating a message route file, rather than polling, looping, or waiting for another role inside the same Claude Code turn.
577
+ VCM Harness injects Claude Code `UserPromptSubmit` and `Stop` hooks into `.claude/settings.json`. Role tabs become `running` when Claude Code accepts a prompt and `idle` after `Stop`; VCM also marks a role `running` immediately after it writes a message to that embedded terminal. Gate Reviewer uses the same Claude hook/Round/translation path as other VCM flow roles, while VCM still owns gate report polling and the PM callback. The terminal process status is tracked separately. Claude role rules require a role to end its turn after writing or updating a message route file, rather than polling, looping, or waiting for another role inside the same Claude Code turn.
576
578
 
577
579
  The implementation keeps only the active manual/auto orchestration mode. It does not expose pause/resume, stage/approve/reject, or a separate agent-facing message CLI.
578
580
 
@@ -603,7 +605,7 @@ Session buttons behave as follows:
603
605
  - `Restart`: stops the current process if needed, creates a new UUID, and starts a fresh Claude session.
604
606
  - `Stop`: stops the embedded terminal process and leaves the persisted Claude session id resumable.
605
607
 
606
- Embedded terminal output is still written to raw role log files under `.ai/vcm/handoffs/logs/`. When a browser reconnects to a running terminal, VCM replays only the tail of that log, capped at 2 MB, rather than streaming the entire historical log back into xterm. This keeps long sessions responsive while preserving full logs on disk until task cleanup.
608
+ Ordinary VCM role sessions do not persist raw terminal logs under handoff state. Claude Code transcript JSONL files under `~/.claude/projects/` are the semantic record used for output translation and recovery.
607
609
 
608
610
  ## Local Project Files
609
611
 
@@ -622,7 +624,9 @@ For a connected repository, VCM uses:
622
624
  <taskRepoRoot>/.ai/vcm/handoffs/review-report.md
623
625
  <taskRepoRoot>/.ai/vcm/handoffs/docs-sync-report.md
624
626
  <taskRepoRoot>/.ai/vcm/handoffs/messages/<from-role>-<to-role>.md
625
- <taskRepoRoot>/.ai/vcm/handoffs/logs/{project-manager,architect,coder,reviewer}.log
627
+ <taskRepoRoot>/.ai/vcm/gate-reviews/
628
+ <baseRepoRoot>/.ai/vcm/gate-reviewer/session.json
629
+ <baseRepoRoot>/.ai/vcm/translations/
626
630
  ```
627
631
 
628
632
  The project config is stored under `vcmDataDir` so it is durable local app state. `vcmDataDir` is `VCM_DATA_DIR` when set, otherwise `~/.vcm`. For Dev Containers, prefer:
@@ -635,9 +639,7 @@ The project config is stored under `vcmDataDir` so it is durable local app state
635
639
  }
636
640
  ```
637
641
 
638
- For worktree-backed tasks, `taskRepoRoot` is `<baseRepoRoot>/.claude/worktrees/<task>`; for inline tasks, `taskRepoRoot` is the connected base repo.
639
-
640
- 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.
642
+ For every task, `taskRepoRoot` is `<baseRepoRoot>/.claude/worktrees/<task>`.
641
643
 
642
644
  ## Packaging
643
645
 
@@ -680,4 +682,6 @@ See also:
680
682
  - `docs/v0.2-implementation-plan.md`
681
683
  - `docs/vcm-cc-best-practices.md`
682
684
  - `docs/full-harness-baseline.md`
685
+ - `docs/gate-review-gates.md`
686
+ - `docs/codex-translation-plan.md`
683
687
  - `docs/cc-best-practices.md` is archived and no longer maintained.
@@ -1,4 +1,4 @@
1
- import { DISPATCHABLE_ROLES, isVcmRoleName } from "../../shared/constants.js";
1
+ import { CORE_VCM_ROLE_NAMES, DISPATCHABLE_ROLES } from "../../shared/constants.js";
2
2
  import { isOpenFileLimitError, VcmError } from "../errors.js";
3
3
  import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
4
4
  export function registerTaskRoutes(app, deps) {
@@ -40,7 +40,7 @@ export function registerTaskRoutes(app, deps) {
40
40
  async function stopRunningRoleSessions(deps, repoRoot, taskSlug) {
41
41
  const sessions = await deps.sessionService.listRoleSessions(repoRoot, taskSlug);
42
42
  for (const session of sessions) {
43
- if (session.status === "running" && isVcmRoleName(session.role)) {
43
+ if (session.status === "running" && CORE_VCM_ROLE_NAMES.some((role) => role === session.role)) {
44
44
  await deps.sessionService.stopRoleSession(repoRoot, taskSlug, session.role);
45
45
  }
46
46
  }
@@ -1,5 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
- import { VCM_ROLE_DEFINITIONS } from "../../shared/constants.js";
2
+ import { CORE_VCM_ROLE_DEFINITIONS, CORE_VCM_ROLE_NAMES, GATE_REVIEWER_ROLE_DEFINITION } from "../../shared/constants.js";
3
3
  import { VcmError } from "../errors.js";
4
4
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
5
5
  import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
@@ -407,17 +407,36 @@ export function createGatewayService(deps) {
407
407
  taskSlug: task.taskSlug,
408
408
  mode: template.autoOrchestration ? "auto" : "manual"
409
409
  });
410
+ const gateReviewSettings = await deps.appSettings.getGateReviewSettings(project.repoRoot, task.taskSlug);
411
+ const roleDefinitions = [
412
+ ...CORE_VCM_ROLE_DEFINITIONS,
413
+ ...(gateReviewSettings.enabled ? [GATE_REVIEWER_ROLE_DEFINITION] : [])
414
+ ];
410
415
  const startedRoles = [];
411
- for (const definition of VCM_ROLE_DEFINITIONS) {
416
+ for (const definition of roleDefinitions) {
412
417
  const roleTemplate = template.roles[definition.name];
413
418
  try {
414
- await deps.sessionService.startRoleSession(project.repoRoot, task.taskSlug, definition.name, {
419
+ const sessionInput = {
415
420
  cols: 100,
416
421
  rows: 28,
417
422
  permissionMode: roleTemplate.permissionMode,
418
423
  model: roleTemplate.model,
419
424
  effort: roleTemplate.effort
420
- });
425
+ };
426
+ const existing = await deps.sessionService.getRoleSession(project.repoRoot, task.taskSlug, definition.name);
427
+ if (existing?.status === "running") {
428
+ if (definition.name === "gate-reviewer") {
429
+ await deps.sessionService.resumeRoleSession(project.repoRoot, task.taskSlug, definition.name, sessionInput);
430
+ }
431
+ startedRoles.push(definition.name);
432
+ continue;
433
+ }
434
+ if (existing?.claudeSessionId) {
435
+ await deps.sessionService.resumeRoleSession(project.repoRoot, task.taskSlug, definition.name, sessionInput);
436
+ }
437
+ else {
438
+ await deps.sessionService.startRoleSession(project.repoRoot, task.taskSlug, definition.name, sessionInput);
439
+ }
421
440
  startedRoles.push(definition.name);
422
441
  }
423
442
  catch (error) {
@@ -536,7 +555,7 @@ export function createGatewayService(deps) {
536
555
  async function stopRunningRoleSessions(repoRoot, taskSlug) {
537
556
  const sessions = await deps.sessionService.listRoleSessions(repoRoot, taskSlug);
538
557
  for (const session of sessions) {
539
- if (session.status === "running") {
558
+ if (session.status === "running" && CORE_VCM_ROLE_NAMES.some((role) => role === session.role)) {
540
559
  await deps.sessionService.stopRoleSession(repoRoot, taskSlug, session.role);
541
560
  }
542
561
  }
@@ -1,4 +1,4 @@
1
- import { isVcmRoleName } from "../../shared/constants.js";
1
+ import { isGateReviewerRoleName, isVcmRoleName } from "../../shared/constants.js";
2
2
  import { VcmError } from "../errors.js";
3
3
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
4
4
  import { getTaskRuntimeRepoRoot } from "./task-service.js";
@@ -22,15 +22,32 @@ export function createClaudeHookService(deps) {
22
22
  });
23
23
  }
24
24
  const config = await deps.projectService.loadConfig(project.repoRoot);
25
- const task = await deps.taskService.loadTask(project.repoRoot, input.taskSlug);
25
+ const taskSlug = await resolveHookTaskSlug(project.repoRoot, input);
26
+ const task = await deps.taskService.loadTask(project.repoRoot, taskSlug);
26
27
  const taskRepoRoot = getTaskRuntimeRepoRoot(task);
27
28
  return {
28
29
  project,
29
30
  config,
30
31
  task,
32
+ taskSlug,
31
33
  taskRepoRoot
32
34
  };
33
35
  }
36
+ async function resolveHookTaskSlug(repoRoot, input) {
37
+ if (!isGateReviewerRoleName(input.role)) {
38
+ return input.taskSlug;
39
+ }
40
+ const session = await deps.sessionService.getProjectGateReviewerSession(repoRoot);
41
+ if (session?.activeTaskSlug) {
42
+ return session.activeTaskSlug;
43
+ }
44
+ throw new VcmError({
45
+ code: "GATE_REVIEWER_TASK_UNBOUND",
46
+ message: "Gate Reviewer hook arrived without an active task binding.",
47
+ statusCode: 409,
48
+ hint: "Start or resume Gate Reviewer from the current task before submitting work."
49
+ });
50
+ }
34
51
  async function handleUserPromptSubmitHook(input) {
35
52
  const eventName = parseHookEvent(input.event.hook_event_name);
36
53
  if (eventName !== "UserPromptSubmit") {
@@ -39,11 +56,11 @@ export function createClaudeHookService(deps) {
39
56
  const context = await getHookContext(input);
40
57
  deps.jobGuard?.notePromptSubmitted({
41
58
  repoRoot: context.project.repoRoot,
42
- taskSlug: input.taskSlug,
59
+ taskSlug: context.taskSlug,
43
60
  role: input.role
44
61
  });
45
62
  const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
46
- taskSlug: input.taskSlug,
63
+ taskSlug: context.taskSlug,
47
64
  role: input.role,
48
65
  eventName,
49
66
  claudeSessionId: stringOrUndefined(input.event.session_id),
@@ -54,7 +71,7 @@ export function createClaudeHookService(deps) {
54
71
  repoRoot: context.project.repoRoot,
55
72
  stateRepoRoot: context.taskRepoRoot,
56
73
  stateRoot: context.config.stateRoot,
57
- taskSlug: input.taskSlug,
74
+ taskSlug: context.taskSlug,
58
75
  role: input.role,
59
76
  eventName
60
77
  });
@@ -62,7 +79,7 @@ export function createClaudeHookService(deps) {
62
79
  await deps.translationService.recordConversationBoundary({
63
80
  repoRoot: context.project.repoRoot,
64
81
  taskRepoRoot: context.taskRepoRoot,
65
- taskSlug: input.taskSlug,
82
+ taskSlug: context.taskSlug,
66
83
  role: input.role,
67
84
  sessionId: session.id,
68
85
  boundaryKind: "start",
@@ -75,14 +92,14 @@ export function createClaudeHookService(deps) {
75
92
  stateRepoRoot: context.taskRepoRoot,
76
93
  stateRoot: context.config.stateRoot,
77
94
  handoffDir: context.task.handoffDir,
78
- taskSlug: input.taskSlug,
95
+ taskSlug: context.taskSlug,
79
96
  role: input.role,
80
97
  prompt: stringOrUndefined(input.event.prompt)
81
98
  });
82
99
  return {
83
100
  ok: true,
84
101
  eventName,
85
- taskSlug: input.taskSlug,
102
+ taskSlug: context.taskSlug,
86
103
  role: input.role,
87
104
  sessionUpdated: Boolean(session),
88
105
  dispatchedCount: 0,
@@ -95,11 +112,11 @@ export function createClaudeHookService(deps) {
95
112
  throwUnsupportedEvent(eventName);
96
113
  }
97
114
  const context = await getHookContext(input);
98
- clearStopFailureRecovery(context.project.repoRoot, input.taskSlug, input.role);
115
+ clearStopFailureRecovery(context.project.repoRoot, context.taskSlug, input.role);
99
116
  if (options.allowBlock && deps.jobGuard) {
100
117
  const verdict = await deps.jobGuard.evaluateStop({
101
118
  repoRoot: context.project.repoRoot,
102
- taskSlug: input.taskSlug,
119
+ taskSlug: context.taskSlug,
103
120
  role: input.role,
104
121
  taskRepoRoot: context.taskRepoRoot
105
122
  });
@@ -109,7 +126,7 @@ export function createClaudeHookService(deps) {
109
126
  return {
110
127
  ok: true,
111
128
  eventName,
112
- taskSlug: input.taskSlug,
129
+ taskSlug: context.taskSlug,
113
130
  role: input.role,
114
131
  sessionUpdated: false,
115
132
  dispatchedCount: 0,
@@ -118,7 +135,7 @@ export function createClaudeHookService(deps) {
118
135
  }
119
136
  }
120
137
  return recordTurnEnd(input, context, eventName, {
121
- dispatchRouteFiles: true,
138
+ dispatchRouteFiles: !isGateReviewerRoleName(input.role),
122
139
  notifyGateway: true,
123
140
  settleGuard: true
124
141
  });
@@ -133,9 +150,9 @@ export function createClaudeHookService(deps) {
133
150
  const pending = await deps.messageService.listPendingRouteFiles(routeDispatchInput);
134
151
  const hasCompletionEvidence = pending.some((routeFile) => routeFile.fromRole === input.role);
135
152
  if (hasCompletionEvidence) {
136
- clearStopFailureRecovery(context.project.repoRoot, input.taskSlug, input.role);
153
+ clearStopFailureRecovery(context.project.repoRoot, context.taskSlug, input.role);
137
154
  return recordTurnEnd(input, context, eventName, {
138
- dispatchRouteFiles: true,
155
+ dispatchRouteFiles: !isGateReviewerRoleName(input.role),
139
156
  notifyGateway: false,
140
157
  settleGuard: true
141
158
  });
@@ -145,7 +162,7 @@ export function createClaudeHookService(deps) {
145
162
  return {
146
163
  ok: true,
147
164
  eventName,
148
- taskSlug: input.taskSlug,
165
+ taskSlug: context.taskSlug,
149
166
  role: input.role,
150
167
  sessionUpdated: true,
151
168
  dispatchedCount: 0
@@ -164,7 +181,7 @@ export function createClaudeHookService(deps) {
164
181
  }
165
182
  const context = await getHookContext(input);
166
183
  const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
167
- taskSlug: input.taskSlug,
184
+ taskSlug: context.taskSlug,
168
185
  role: input.role,
169
186
  eventName,
170
187
  claudeSessionId: stringOrUndefined(input.event.session_id),
@@ -174,7 +191,7 @@ export function createClaudeHookService(deps) {
174
191
  return {
175
192
  ok: true,
176
193
  eventName,
177
- taskSlug: input.taskSlug,
194
+ taskSlug: context.taskSlug,
178
195
  role: input.role,
179
196
  sessionUpdated: Boolean(session),
180
197
  dispatchedCount: 0
@@ -184,7 +201,7 @@ export function createClaudeHookService(deps) {
184
201
  const scopedRouteDispatchInput = createRouteDispatchInput(input, context, input.role);
185
202
  const settleRouteDispatchInput = createRouteDispatchInput(input, context);
186
203
  const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
187
- taskSlug: input.taskSlug,
204
+ taskSlug: context.taskSlug,
188
205
  role: input.role,
189
206
  eventName,
190
207
  claudeSessionId: stringOrUndefined(input.event.session_id),
@@ -195,7 +212,7 @@ export function createClaudeHookService(deps) {
195
212
  repoRoot: context.project.repoRoot,
196
213
  stateRepoRoot: context.taskRepoRoot,
197
214
  stateRoot: context.config.stateRoot,
198
- taskSlug: input.taskSlug,
215
+ taskSlug: context.taskSlug,
199
216
  role: input.role,
200
217
  eventName,
201
218
  ...(options.settleGuard
@@ -217,7 +234,7 @@ export function createClaudeHookService(deps) {
217
234
  await deps.translationService.recordConversationBoundary({
218
235
  repoRoot: context.project.repoRoot,
219
236
  taskRepoRoot: context.taskRepoRoot,
220
- taskSlug: input.taskSlug,
237
+ taskSlug: context.taskSlug,
221
238
  role: input.role,
222
239
  sessionId: session.id,
223
240
  boundaryKind: "end",
@@ -227,7 +244,7 @@ export function createClaudeHookService(deps) {
227
244
  if (options.notifyGateway && session && input.role === "project-manager") {
228
245
  void deps.gatewayService?.handlePmStop({
229
246
  repoRoot: context.project.repoRoot,
230
- taskSlug: input.taskSlug,
247
+ taskSlug: context.taskSlug,
231
248
  session
232
249
  }).catch(() => undefined);
233
250
  }
@@ -237,7 +254,7 @@ export function createClaudeHookService(deps) {
237
254
  return {
238
255
  ok: true,
239
256
  eventName,
240
- taskSlug: input.taskSlug,
257
+ taskSlug: context.taskSlug,
241
258
  role: input.role,
242
259
  sessionUpdated: Boolean(session),
243
260
  dispatchedCount: dispatched.filter((result) => result.delivered).length
@@ -250,7 +267,7 @@ export function createClaudeHookService(deps) {
250
267
  stateRepoRoot: context.taskRepoRoot,
251
268
  stateRoot: context.config.stateRoot,
252
269
  handoffDir: context.task.handoffDir,
253
- taskSlug: input.taskSlug,
270
+ taskSlug: context.taskSlug,
254
271
  ...(stoppedRole ? { stoppedRole } : {})
255
272
  };
256
273
  }
@@ -258,18 +275,18 @@ export function createClaudeHookService(deps) {
258
275
  if (!deps.runtime) {
259
276
  return false;
260
277
  }
261
- const key = stopFailureRecoveryKey(context.project.repoRoot, input.taskSlug, input.role);
278
+ const key = stopFailureRecoveryKey(context.project.repoRoot, context.taskSlug, input.role);
262
279
  const attempt = (stopFailureRecoveryAttempts.get(key) ?? 0) + 1;
263
280
  if (attempt > MAX_STOP_FAILURE_RECOVERY_ATTEMPTS) {
264
281
  return false;
265
282
  }
266
- const session = await deps.sessionService.getRoleSession(context.project.repoRoot, input.taskSlug, input.role);
283
+ const session = await deps.sessionService.getRoleSession(context.project.repoRoot, context.taskSlug, input.role);
267
284
  if (!session || session.status !== "running") {
268
285
  return false;
269
286
  }
270
287
  stopFailureRecoveryAttempts.set(key, attempt);
271
288
  await submitTerminalInput(deps.runtime, session.id, renderStopFailureRecoveryPrompt(attempt));
272
- await deps.sessionService.markRoleActivityRunning(context.project.repoRoot, input.taskSlug, input.role);
289
+ await deps.sessionService.markRoleActivityRunning(context.project.repoRoot, context.taskSlug, input.role);
273
290
  return true;
274
291
  }
275
292
  function clearStopFailureRecovery(repoRoot, taskSlug, role) {
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { randomUUID } from "node:crypto";
3
- import { VCM_ROLE_NAMES, isVcmRoleName } from "../../shared/constants.js";
3
+ import { CORE_VCM_ROLE_NAMES } from "../../shared/constants.js";
4
4
  import { VcmError } from "../errors.js";
5
5
  import { resolveRepoPath } from "../adapters/filesystem.js";
6
6
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
@@ -282,8 +282,8 @@ async function listRouteFiles(fs, input) {
282
282
  return routeFiles;
283
283
  }
284
284
  function parseRouteFileName(fileName) {
285
- for (const fromRole of VCM_ROLE_NAMES) {
286
- for (const toRole of VCM_ROLE_NAMES) {
285
+ for (const fromRole of CORE_VCM_ROLE_NAMES) {
286
+ for (const toRole of CORE_VCM_ROLE_NAMES) {
287
287
  if (fromRole === toRole) {
288
288
  continue;
289
289
  }
@@ -371,14 +371,14 @@ function selectDispatchCandidates(routeFiles, stoppedRole) {
371
371
  });
372
372
  }
373
373
  function validateMessagePolicy(fromRole, toRole, type) {
374
- if (!VCM_ROLE_NAMES.includes(toRole)) {
374
+ if (!CORE_VCM_ROLE_NAMES.some((role) => role === toRole)) {
375
375
  throw new VcmError({
376
376
  code: "MESSAGE_TARGET_UNKNOWN",
377
377
  message: `Unknown target role: ${toRole}`,
378
378
  statusCode: 400
379
379
  });
380
380
  }
381
- if (!isVcmRoleName(String(fromRole))) {
381
+ if (!CORE_VCM_ROLE_NAMES.some((role) => role === fromRole)) {
382
382
  throw new VcmError({
383
383
  code: "MESSAGE_SENDER_UNKNOWN",
384
384
  message: `Unknown sender role: ${fromRole}`,
@@ -425,7 +425,7 @@ function getMessageDeliveryTime(message) {
425
425
  }
426
426
  async function clearRouteFileIfStillMatchesMessage(fs, input, message) {
427
427
  const fromRole = message.fromRole;
428
- if (!message.routePath || !isVcmRoleName(fromRole)) {
428
+ if (!message.routePath || !isCoreRouteRole(fromRole) || !isCoreRouteRole(message.toRole)) {
429
429
  return;
430
430
  }
431
431
  const absolutePath = resolveRepoPath(input.taskRepoRoot ?? input.repoRoot, message.routePath);
@@ -440,6 +440,9 @@ async function clearRouteFileIfStillMatchesMessage(fs, input, message) {
440
440
  await fs.writeText(absolutePath, "");
441
441
  }
442
442
  }
443
+ function isCoreRouteRole(role) {
444
+ return CORE_VCM_ROLE_NAMES.some((candidate) => candidate === role);
445
+ }
443
446
  function arraysEqual(left, right) {
444
447
  if (left.length !== right.length) {
445
448
  return false;
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { VCM_ROLE_NAMES } from "../../shared/constants.js";
2
+ import { CORE_VCM_ROLE_NAMES } from "../../shared/constants.js";
3
3
  import { VcmError } from "../errors.js";
4
4
  const DEFAULT_HANDOFF_ROOT = ".ai/vcm/handoffs";
5
5
  const DEFAULT_STATE_ROOT = ".ai/vcm";
@@ -179,7 +179,7 @@ export function buildDefaultProjectConfig(repoRoot) {
179
179
  return {
180
180
  version: 1,
181
181
  repoRoot,
182
- defaultRoles: [...VCM_ROLE_NAMES],
182
+ defaultRoles: [...CORE_VCM_ROLE_NAMES],
183
183
  handoffRoot: DEFAULT_HANDOFF_ROOT,
184
184
  stateRoot: DEFAULT_STATE_ROOT,
185
185
  terminalBackend: "node-pty",