vibe-coding-master 0.2.13 → 0.3.1
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 +9 -6
- package/dist/backend/adapters/claude-adapter.js +4 -1
- package/dist/backend/api/codex-hook-routes.js +9 -0
- package/dist/backend/api/codex-review-routes.js +58 -0
- package/dist/backend/api/translation-routes.js +2 -2
- package/dist/backend/cli/install-vcm-harness.js +68 -0
- package/dist/backend/gateway/gateway-service.js +4 -3
- package/dist/backend/server.js +25 -0
- package/dist/backend/services/app-settings-service.js +12 -4
- package/dist/backend/services/artifact-service.js +2 -1
- package/dist/backend/services/claude-hook-service.js +3 -3
- package/dist/backend/services/codex-hook-service.js +87 -0
- package/dist/backend/services/codex-review-service.js +850 -0
- package/dist/backend/services/harness-service.js +90 -3
- package/dist/backend/services/message-service.js +8 -7
- package/dist/backend/services/project-service.js +2 -2
- package/dist/backend/services/round-service.js +29 -25
- package/dist/backend/services/session-service.js +141 -12
- package/dist/backend/services/task-service.js +2 -0
- package/dist/backend/templates/harness/claude-root.js +2 -0
- package/dist/backend/templates/harness/codex-review.js +605 -0
- package/dist/backend/templates/harness/project-manager-agent.js +13 -3
- package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +6 -1
- package/dist/shared/constants.js +15 -1
- package/dist/shared/types/app-settings.js +4 -3
- package/dist/shared/types/codex-hook.js +1 -0
- package/dist/shared/types/codex-review.js +5 -0
- package/dist/shared/types/session.js +44 -0
- package/dist-frontend/assets/index-BavJjWQY.js +92 -0
- package/dist-frontend/assets/index-CR1EOe-w.css +32 -0
- package/dist-frontend/index.html +2 -2
- package/docs/ARCHITECTURE.md +1 -0
- package/docs/TESTING.md +82 -0
- package/docs/codex-review-gates.md +553 -0
- package/docs/gateway-design.md +5 -4
- package/docs/known-issues.md +1 -0
- package/docs/product-design.md +55 -10
- package/package.json +1 -1
- package/scripts/verify-package.mjs +4 -0
- package/dist-frontend/assets/index-CT20u9Fk.js +0 -92
- package/dist-frontend/assets/index-DC9-SB7F.css +0 -32
package/README.md
CHANGED
|
@@ -11,6 +11,8 @@ 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 Codex Review Gates are enabled for a task, or when a Codex Reviewer session already exists, the workspace can also show a fifth `Codex Reviewer` terminal role. It runs Codex CLI from `.ai/codex` with Codex model and effort selectors, receives gate prompts in the same long-lived terminal session, reports hook state back to VCM, and stays outside the normal Claude Code PM routing flow.
|
|
15
|
+
|
|
14
16
|
## Current Capabilities
|
|
15
17
|
|
|
16
18
|
- GUI-first task workspace.
|
|
@@ -19,6 +21,7 @@ Each role runs as a real Claude Code process inside an embedded terminal. The GU
|
|
|
19
21
|
- Connected repository status for the base repo, including branch, upstream status, commit hash, dirty state, and fast-forward-only pull.
|
|
20
22
|
- Embedded Claude Code terminals powered by `node-pty` and `xterm.js`.
|
|
21
23
|
- One Claude Code session per role, with role tabs in the task header.
|
|
24
|
+
- Optional Codex Reviewer terminal role when any Codex Review Gate is enabled.
|
|
22
25
|
- Role session recovery through persisted Claude session ids and `claude --resume`.
|
|
23
26
|
- Permission mode selection before start, resume, or restart:
|
|
24
27
|
- `default`
|
|
@@ -26,7 +29,7 @@ Each role runs as a real Claude Code process inside an embedded terminal. The GU
|
|
|
26
29
|
- PM-mediated role messaging through VCM-dispatched route files.
|
|
27
30
|
- Manual and automatic orchestration modes.
|
|
28
31
|
- Two-stage VCM harness setup: deterministic fixed install plus AI-assisted bootstrap.
|
|
29
|
-
- VCM-managed root rules, four role agents, repo-local VCM skills, Claude Code hooks, generated-context tools, and PR template.
|
|
32
|
+
- VCM-managed root rules, four role agents, repo-local VCM skills, Claude Code hooks, Codex Reviewer hooks, generated-context tools, and PR template.
|
|
30
33
|
- Rust generated context for module indexing and crate-external public surface indexing.
|
|
31
34
|
- Translation panel powered by an OpenAI-compatible low-cost model.
|
|
32
35
|
- Mobile Gateway through Tencent iLink Bot API / Weixin DM, for talking to PM and managing tasks from Weixin.
|
|
@@ -233,7 +236,7 @@ the base repo has uncommitted changes, when the current branch has no upstream,
|
|
|
233
236
|
or when the active task is an inline task using the base repo directly. It does
|
|
234
237
|
not stash, merge, or mutate task worktrees.
|
|
235
238
|
|
|
236
|
-
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
|
|
239
|
+
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. If a Round is currently running, the dock also shows the Current Round start time, total elapsed time, role active runtime, and Turn count.
|
|
237
240
|
|
|
238
241
|
## Mobile Gateway
|
|
239
242
|
|
|
@@ -356,7 +359,7 @@ Typical mobile flow:
|
|
|
356
359
|
|
|
357
360
|
`/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.
|
|
358
361
|
|
|
359
|
-
`/create-task` uses the saved launch template from the desktop settings. The template controls permission mode, model, auto orchestration, and translation defaults for the four role sessions.
|
|
362
|
+
`/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
363
|
|
|
361
364
|
`/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.
|
|
362
365
|
|
|
@@ -478,7 +481,7 @@ For `.gitignore`, VCM uses a gitignore-native managed block:
|
|
|
478
481
|
|
|
479
482
|
`.ai/vcm/` is the active VCM local control area, and `.claude/worktrees/` is the Claude-compatible task worktree area. VCM keeps the task index in app-local project state under `~/.vcm/projects/`; each task runtime repo keeps its own session, message, orchestration, and translation state.
|
|
480
483
|
|
|
481
|
-
VCM also JSON-merges `.claude/settings.json` to install Claude Code `PreToolUse`, `UserPromptSubmit`, `Stop`, and `PermissionRequest` hooks plus a managed `env.BASH_DEFAULT_TIMEOUT_MS` so foreground watch windows fit inside the Bash tool timeout. The hooks post directly to the local VCM backend, so roles do not need a VCM CLI command to confirm delivery or report turn completion. The `Stop` hook forwards the backend response to Claude Code, which lets VCM block turn-end while a validation job is still running.
|
|
484
|
+
VCM also JSON-merges `.claude/settings.json` to install Claude Code `PreToolUse`, `UserPromptSubmit`, `Stop`, and `PermissionRequest` hooks plus a managed `env.BASH_DEFAULT_TIMEOUT_MS` so foreground watch windows fit inside the Bash tool timeout. The hooks post directly to the local VCM backend, so roles do not need a VCM CLI command to confirm delivery or report turn completion. The `Stop` hook forwards the backend response to Claude Code, which lets VCM block turn-end while a validation job is still running. When Codex Review Gates are installed, VCM also writes `.ai/codex/.codex/config.toml` and `.ai/codex/.codex/hooks.json` so the embedded Codex Reviewer terminal can POST `UserPromptSubmit` and `Stop` events back to VCM.
|
|
482
485
|
|
|
483
486
|
Bootstrap is AI-assisted. VCM starts a visible temporary Claude Code session in the connected repository and asks it to use the `vcm-harness-bootstrap` skill. Bootstrap fills project-specific content and generated context:
|
|
484
487
|
|
|
@@ -565,7 +568,7 @@ When it is on, VCM is in auto mode:
|
|
|
565
568
|
- When the target role later reaches `Stop`, VCM scans again and may deliver the next pending route file.
|
|
566
569
|
- If auto orchestration gets stuck after a manual copy/paste recovery, `Mark All Done` clears pending route files. It does not mutate message history.
|
|
567
570
|
|
|
568
|
-
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. The terminal process status is still tracked separately.
|
|
571
|
+
VCM Harness injects Claude Code `UserPromptSubmit` and `Stop` hooks into `.claude/settings.json`, and Codex Reviewer hooks into `.ai/codex/.codex/hooks.json`. Role tabs become `running` when Claude Code or Codex accepts a prompt and `idle` after `Stop`; VCM also marks a role `running` immediately after it writes a message to that embedded terminal. 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; Codex Reviewer does not dispatch route files.
|
|
569
572
|
|
|
570
573
|
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.
|
|
571
574
|
|
|
@@ -579,7 +582,7 @@ The normal path is timer-driven: `Stop` starts a backend timer, and `UserPromptS
|
|
|
579
582
|
|
|
580
583
|
When `Flow pause alert` is enabled, the frontend polls the task Round state and deduplicates each stopped Round so the same stopped state does not alert on every poll. Flow duration is measured from the first `UserPromptSubmit` to `stoppedAt`, falling back to the last `Stop` when needed. If the flow lasted less than 2 minutes, it plays the local chime 3 times at 1.4 second intervals. If the flow lasted 2 minutes or longer, it shows a modal alert and repeats the local chime until the user clicks `Confirm`. A stopped Round can mean normal completion, user decision needed, dispatch failure, or another workflow interruption; the point is to get the user to look with the right amount of urgency.
|
|
581
584
|
|
|
582
|
-
The Current Round dock separates wall-clock Round duration from
|
|
585
|
+
The Current Round dock separates wall-clock Round duration from role active runtime. `Total` is `now - Round.startedAt`; `Role runtime` is only the accumulated time between each Turn's `UserPromptSubmit` and `Stop`; `Turn count` is the number of accepted prompts inside the current Round.
|
|
583
586
|
|
|
584
587
|
## Resume Behavior
|
|
585
588
|
|
|
@@ -17,12 +17,15 @@ export function createClaudeAdapter(runner) {
|
|
|
17
17
|
}
|
|
18
18
|
return result.stdout.trim();
|
|
19
19
|
},
|
|
20
|
-
buildRoleStartCommand(role, command = "claude", permissionMode = "default", claudeSessionId, resume = false, model = "default") {
|
|
20
|
+
buildRoleStartCommand(role, command = "claude", permissionMode = "default", claudeSessionId, resume = false, model = "default", effort = "default") {
|
|
21
21
|
const args = ["--agent", role];
|
|
22
22
|
if (claudeSessionId) {
|
|
23
23
|
args.push(resume ? "--resume" : "--session-id", claudeSessionId);
|
|
24
24
|
}
|
|
25
25
|
args.push("--model", model);
|
|
26
|
+
if (effort !== "default") {
|
|
27
|
+
args.push("--effort", effort);
|
|
28
|
+
}
|
|
26
29
|
if (permissionMode === "bypassPermissions") {
|
|
27
30
|
args.push("--permission-mode", "bypassPermissions");
|
|
28
31
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function registerCodexHookRoutes(app, deps) {
|
|
2
|
+
app.post("/api/hooks/codex-reviewer", async (request) => {
|
|
3
|
+
return deps.codexHookService.handleHook(request.body);
|
|
4
|
+
});
|
|
5
|
+
app.post("/api/hooks/codex-reviewer/stop", async (request) => {
|
|
6
|
+
await deps.codexHookService.handleStopHook(request.body);
|
|
7
|
+
return {};
|
|
8
|
+
});
|
|
9
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { VcmError } from "../errors.js";
|
|
2
|
+
import { isCodexReviewGate } from "../services/codex-review-service.js";
|
|
3
|
+
export function registerCodexReviewRoutes(app, deps) {
|
|
4
|
+
app.get("/api/tasks/:taskSlug/codex-review", async (request) => {
|
|
5
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
6
|
+
return deps.codexReviewService.getState(project.repoRoot, request.params.taskSlug);
|
|
7
|
+
});
|
|
8
|
+
app.put("/api/tasks/:taskSlug/codex-review/settings", async (request) => {
|
|
9
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
10
|
+
return deps.codexReviewService.updateSettings(project.repoRoot, request.params.taskSlug, request.body);
|
|
11
|
+
});
|
|
12
|
+
app.post("/api/tasks/:taskSlug/codex-review/:gate/request", async (request) => {
|
|
13
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
14
|
+
const gate = parseGate(request.params.gate);
|
|
15
|
+
return deps.codexReviewService.requestReviewGate(project.repoRoot, request.params.taskSlug, gate);
|
|
16
|
+
});
|
|
17
|
+
app.post("/api/tasks/:taskSlug/codex-review/:gate/retry", async (request) => {
|
|
18
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
19
|
+
const gate = parseGate(request.params.gate);
|
|
20
|
+
return deps.codexReviewService.retryReviewGate(project.repoRoot, request.params.taskSlug, gate);
|
|
21
|
+
});
|
|
22
|
+
app.post("/api/tasks/:taskSlug/codex-review/:gate/skip", async (request) => {
|
|
23
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
24
|
+
const gate = parseGate(request.params.gate);
|
|
25
|
+
return deps.codexReviewService.skipReviewGate(project.repoRoot, request.params.taskSlug, gate, request.body);
|
|
26
|
+
});
|
|
27
|
+
app.post("/api/tasks/:taskSlug/codex-review/:gate/override", async (request) => {
|
|
28
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
29
|
+
const gate = parseGate(request.params.gate);
|
|
30
|
+
return deps.codexReviewService.overrideReviewGate(project.repoRoot, request.params.taskSlug, gate, request.body);
|
|
31
|
+
});
|
|
32
|
+
app.get("/api/tasks/:taskSlug/codex-review/:gate/report", async (request) => {
|
|
33
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
34
|
+
const gate = parseGate(request.params.gate);
|
|
35
|
+
return deps.codexReviewService.readReport(project.repoRoot, request.params.taskSlug, gate);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function parseGate(gate) {
|
|
39
|
+
if (!isCodexReviewGate(gate)) {
|
|
40
|
+
throw new VcmError({
|
|
41
|
+
code: "UNKNOWN_CODEX_REVIEW_GATE",
|
|
42
|
+
message: `Unknown Codex review gate: ${gate}`,
|
|
43
|
+
statusCode: 400
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return gate;
|
|
47
|
+
}
|
|
48
|
+
async function requireCurrentProject(projectService) {
|
|
49
|
+
const project = await projectService.getCurrentProject();
|
|
50
|
+
if (!project) {
|
|
51
|
+
throw new VcmError({
|
|
52
|
+
code: "PROJECT_NOT_CONNECTED",
|
|
53
|
+
message: "Connect a repository first.",
|
|
54
|
+
statusCode: 409
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return project;
|
|
58
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isVcmRoleName } from "../../shared/constants.js";
|
|
2
2
|
import { VcmError } from "../errors.js";
|
|
3
3
|
import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
|
|
4
4
|
export function registerTranslationRoutes(app, deps) {
|
|
@@ -74,7 +74,7 @@ export function registerTranslationRoutes(app, deps) {
|
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
76
|
function parseRole(role) {
|
|
77
|
-
if (!
|
|
77
|
+
if (!isVcmRoleName(role)) {
|
|
78
78
|
throw new VcmError({
|
|
79
79
|
code: "UNKNOWN_ROLE",
|
|
80
80
|
message: `Unknown role: ${role}`,
|
|
@@ -6,6 +6,7 @@ import process from "node:process";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { renderArchitectHarnessRules } from "../templates/harness/architect-agent.js";
|
|
8
8
|
import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
|
|
9
|
+
import { renderCodexAgentsHarnessRules, renderCodexArchitecturePlanPrompt, renderCodexCliConfigHarnessRules, renderCodexConfigHarnessRules, renderCodexFinalDiffPrompt, renderCodexHooksHarnessRules, renderCodexReviewResultSchema, renderCodexValidationAdequacyPrompt, renderRequestCodexReviewTool, renderVcmCodexReviewGateSkillRules } from "../templates/harness/codex-review.js";
|
|
9
10
|
import { renderRootClaudeHarnessRules } from "../templates/harness/claude-root.js";
|
|
10
11
|
import { renderGitignoreHarnessRules } from "../templates/harness/gitignore.js";
|
|
11
12
|
import { renderProjectManagerHarnessRules } from "../templates/harness/project-manager-agent.js";
|
|
@@ -101,6 +102,13 @@ const MANAGED_FILES = [
|
|
|
101
102
|
commentStyle: "html",
|
|
102
103
|
category: "pull-request-template",
|
|
103
104
|
content: renderPullRequestTemplateHarnessRules()
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
path: ".ai/codex/AGENTS.md",
|
|
108
|
+
title: "VCM Codex Reviewer",
|
|
109
|
+
commentStyle: "html",
|
|
110
|
+
category: "codex-reviewer-agent",
|
|
111
|
+
content: renderCodexAgentsHarnessRules()
|
|
104
112
|
}
|
|
105
113
|
];
|
|
106
114
|
const DURABLE_DOC_TEMPLATES = [
|
|
@@ -154,6 +162,60 @@ const WHOLE_FILES = [
|
|
|
154
162
|
mode: 0o644,
|
|
155
163
|
content: renderSkillFile("VCM Route Message Skill", "vcm-route-message", "Use when a VCM role needs to hand off work, ask a question, report a result, report a blocker, or raise a finding to another VCM role.", renderVcmRouteMessageSkillRules())
|
|
156
164
|
},
|
|
165
|
+
{
|
|
166
|
+
path: ".claude/skills/vcm-codex-review-gate/SKILL.md",
|
|
167
|
+
category: "skill",
|
|
168
|
+
mode: 0o644,
|
|
169
|
+
content: renderSkillFile("VCM Codex Review Gate Skill", "vcm-codex-review-gate", "Use when project-manager reaches a Codex Review Gate or receives a VCM Codex Review callback.", renderVcmCodexReviewGateSkillRules())
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
path: ".ai/codex/config.toml",
|
|
173
|
+
category: "codex-review-config",
|
|
174
|
+
mode: 0o644,
|
|
175
|
+
content: renderCodexConfigHarnessRules()
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
path: ".ai/codex/.codex/config.toml",
|
|
179
|
+
category: "codex-review-hooks",
|
|
180
|
+
mode: 0o644,
|
|
181
|
+
content: renderCodexCliConfigHarnessRules()
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
path: ".ai/codex/.codex/hooks.json",
|
|
185
|
+
category: "codex-review-hooks",
|
|
186
|
+
mode: 0o644,
|
|
187
|
+
content: renderCodexHooksHarnessRules()
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
path: ".ai/codex/prompts/architecture-plan-gate.md",
|
|
191
|
+
category: "codex-review-prompt",
|
|
192
|
+
mode: 0o644,
|
|
193
|
+
content: renderCodexArchitecturePlanPrompt()
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
path: ".ai/codex/prompts/validation-adequacy-gate.md",
|
|
197
|
+
category: "codex-review-prompt",
|
|
198
|
+
mode: 0o644,
|
|
199
|
+
content: renderCodexValidationAdequacyPrompt()
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
path: ".ai/codex/prompts/final-diff-gate.md",
|
|
203
|
+
category: "codex-review-prompt",
|
|
204
|
+
mode: 0o644,
|
|
205
|
+
content: renderCodexFinalDiffPrompt()
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
path: ".ai/codex/schemas/codex-review-result.schema.json",
|
|
209
|
+
category: "codex-review-schema",
|
|
210
|
+
mode: 0o644,
|
|
211
|
+
content: renderCodexReviewResultSchema()
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
path: ".ai/tools/request-codex-review",
|
|
215
|
+
category: "runtime-tool",
|
|
216
|
+
mode: 0o755,
|
|
217
|
+
content: renderRequestCodexReviewTool()
|
|
218
|
+
},
|
|
157
219
|
{
|
|
158
220
|
path: ".ai/tools/run-long-check",
|
|
159
221
|
category: "runtime-tool",
|
|
@@ -365,6 +427,12 @@ function fixedDirectories() {
|
|
|
365
427
|
".claude/skills/vcm-harness-bootstrap/",
|
|
366
428
|
".claude/skills/vcm-long-running-validation/",
|
|
367
429
|
".claude/skills/vcm-route-message/",
|
|
430
|
+
".claude/skills/vcm-codex-review-gate/",
|
|
431
|
+
".ai/codex/",
|
|
432
|
+
".ai/codex/.codex/",
|
|
433
|
+
".ai/codex/prompts/",
|
|
434
|
+
".ai/codex/schemas/",
|
|
435
|
+
".ai/vcm/codex-reviews/",
|
|
368
436
|
".ai/tools/",
|
|
369
437
|
".ai/generated/"
|
|
370
438
|
];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { VCM_ROLE_DEFINITIONS } 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";
|
|
@@ -396,14 +396,15 @@ export function createGatewayService(deps) {
|
|
|
396
396
|
mode: template.autoOrchestration ? "auto" : "manual"
|
|
397
397
|
});
|
|
398
398
|
const startedRoles = [];
|
|
399
|
-
for (const definition of
|
|
399
|
+
for (const definition of VCM_ROLE_DEFINITIONS) {
|
|
400
400
|
const roleTemplate = template.roles[definition.name];
|
|
401
401
|
try {
|
|
402
402
|
await deps.sessionService.startRoleSession(project.repoRoot, task.taskSlug, definition.name, {
|
|
403
403
|
cols: 100,
|
|
404
404
|
rows: 28,
|
|
405
405
|
permissionMode: roleTemplate.permissionMode,
|
|
406
|
-
model: roleTemplate.model
|
|
406
|
+
model: roleTemplate.model,
|
|
407
|
+
effort: roleTemplate.effort
|
|
407
408
|
});
|
|
408
409
|
startedRoles.push(definition.name);
|
|
409
410
|
}
|
package/dist/backend/server.js
CHANGED
|
@@ -7,9 +7,11 @@ import { createClaudeAdapter } from "./adapters/claude-adapter.js";
|
|
|
7
7
|
import { createCommandRunner } from "./adapters/command-runner.js";
|
|
8
8
|
import { createCommandDispatcher } from "./services/command-dispatcher.js";
|
|
9
9
|
import { createClaudeHookService } from "./services/claude-hook-service.js";
|
|
10
|
+
import { createCodexHookService } from "./services/codex-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";
|
|
14
|
+
import { createCodexReviewService } from "./services/codex-review-service.js";
|
|
13
15
|
import { createHarnessService, createScriptFixedHarnessInstaller } from "./services/harness-service.js";
|
|
14
16
|
import { createNodeFileSystemAdapter } from "./adapters/filesystem.js";
|
|
15
17
|
import { createNodePtyTerminalRuntime } from "./runtime/node-pty-runtime.js";
|
|
@@ -31,6 +33,8 @@ import { createTranslationService } from "./services/translation-service.js";
|
|
|
31
33
|
import { registerAppSettingsRoutes } from "./api/app-settings-routes.js";
|
|
32
34
|
import { registerArtifactRoutes } from "./api/artifact-routes.js";
|
|
33
35
|
import { registerClaudeHookRoutes } from "./api/claude-hook-routes.js";
|
|
36
|
+
import { registerCodexHookRoutes } from "./api/codex-hook-routes.js";
|
|
37
|
+
import { registerCodexReviewRoutes } from "./api/codex-review-routes.js";
|
|
34
38
|
import { registerHarnessRoutes } from "./api/harness-routes.js";
|
|
35
39
|
import { registerMessageRoutes } from "./api/message-routes.js";
|
|
36
40
|
import { registerProjectRoutes } from "./api/project-routes.js";
|
|
@@ -56,6 +60,11 @@ export async function createServer(deps, options = {}) {
|
|
|
56
60
|
});
|
|
57
61
|
registerAppSettingsRoutes(app, { appSettings: deps.appSettings });
|
|
58
62
|
registerClaudeHookRoutes(app, { claudeHookService: deps.claudeHookService });
|
|
63
|
+
registerCodexHookRoutes(app, { codexHookService: deps.codexHookService });
|
|
64
|
+
registerCodexReviewRoutes(app, {
|
|
65
|
+
projectService: deps.projectService,
|
|
66
|
+
codexReviewService: deps.codexReviewService
|
|
67
|
+
});
|
|
59
68
|
registerProjectRoutes(app, { projectService: deps.projectService });
|
|
60
69
|
registerHarnessRoutes(app, {
|
|
61
70
|
projectService: deps.projectService,
|
|
@@ -175,6 +184,14 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
175
184
|
sessionService,
|
|
176
185
|
taskService
|
|
177
186
|
});
|
|
187
|
+
const codexReviewService = createCodexReviewService({
|
|
188
|
+
fs,
|
|
189
|
+
runner,
|
|
190
|
+
runtime,
|
|
191
|
+
projectService,
|
|
192
|
+
taskService,
|
|
193
|
+
sessionService
|
|
194
|
+
});
|
|
178
195
|
const roundService = createRoundService({
|
|
179
196
|
fs,
|
|
180
197
|
onSessionStatusChange: async ({ repoRoot, taskSlug, status }) => {
|
|
@@ -222,6 +239,12 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
222
239
|
gatewayService,
|
|
223
240
|
jobGuard: createJobGuardService()
|
|
224
241
|
});
|
|
242
|
+
const codexHookService = createCodexHookService({
|
|
243
|
+
projectService,
|
|
244
|
+
taskService,
|
|
245
|
+
sessionService,
|
|
246
|
+
roundService
|
|
247
|
+
});
|
|
225
248
|
return {
|
|
226
249
|
appSettings,
|
|
227
250
|
projectService,
|
|
@@ -231,7 +254,9 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
231
254
|
harnessService,
|
|
232
255
|
commandDispatcher,
|
|
233
256
|
claudeHookService,
|
|
257
|
+
codexHookService,
|
|
234
258
|
messageService,
|
|
259
|
+
codexReviewService,
|
|
235
260
|
roundService,
|
|
236
261
|
statusService,
|
|
237
262
|
translationService,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import {
|
|
4
|
+
import { VCM_ROLE_NAMES } from "../../shared/constants.js";
|
|
5
5
|
import { createDefaultLaunchTemplate } from "../../shared/types/app-settings.js";
|
|
6
|
-
import { CLAUDE_MODEL_OPTIONS } from "../../shared/types/session.js";
|
|
6
|
+
import { CLAUDE_MODEL_OPTIONS, SESSION_EFFORT_OPTIONS } from "../../shared/types/session.js";
|
|
7
7
|
const MAX_RECENT_REPOSITORIES = 5;
|
|
8
8
|
export function createAppSettingsService(deps) {
|
|
9
9
|
const settingsPath = deps.settingsPath ?? path.join(homedir(), ".vcm", "settings.json");
|
|
@@ -211,7 +211,7 @@ function normalizeLaunchTemplate(input) {
|
|
|
211
211
|
}
|
|
212
212
|
const rawRoles = isObject(input.roles) ? input.roles : {};
|
|
213
213
|
const roles = {};
|
|
214
|
-
for (const role of
|
|
214
|
+
for (const role of VCM_ROLE_NAMES) {
|
|
215
215
|
roles[role] = normalizeRoleLaunchTemplateEntry(rawRoles[role], defaults.roles[role]);
|
|
216
216
|
}
|
|
217
217
|
return {
|
|
@@ -225,7 +225,8 @@ function normalizeRoleLaunchTemplateEntry(input, fallback) {
|
|
|
225
225
|
const candidate = isObject(input) ? input : {};
|
|
226
226
|
return {
|
|
227
227
|
permissionMode: normalizeClaudePermissionMode(candidate.permissionMode, fallback.permissionMode),
|
|
228
|
-
model: normalizeClaudeModel(candidate.model, fallback.model)
|
|
228
|
+
model: normalizeClaudeModel(candidate.model, fallback.model),
|
|
229
|
+
effort: normalizeSessionEffort(candidate.effort, fallback.effort)
|
|
229
230
|
};
|
|
230
231
|
}
|
|
231
232
|
function normalizeClaudePermissionMode(input, fallback) {
|
|
@@ -241,6 +242,13 @@ function normalizeClaudeModel(input, fallback) {
|
|
|
241
242
|
const model = CLAUDE_MODEL_OPTIONS.find((option) => option.value === input);
|
|
242
243
|
return model?.value ?? fallback;
|
|
243
244
|
}
|
|
245
|
+
function normalizeSessionEffort(input, fallback) {
|
|
246
|
+
if (typeof input !== "string") {
|
|
247
|
+
return fallback;
|
|
248
|
+
}
|
|
249
|
+
const effort = SESSION_EFFORT_OPTIONS.find((option) => option.value === input);
|
|
250
|
+
return effort?.value ?? fallback;
|
|
251
|
+
}
|
|
244
252
|
function normalizeTranslationConfig(input) {
|
|
245
253
|
if (!input || typeof input !== "object") {
|
|
246
254
|
return undefined;
|
|
@@ -41,7 +41,8 @@ export function createArtifactService(fs) {
|
|
|
41
41
|
"project-manager": path.posix.join(logsDir, "project-manager.log"),
|
|
42
42
|
architect: path.posix.join(logsDir, "architect.log"),
|
|
43
43
|
coder: path.posix.join(logsDir, "coder.log"),
|
|
44
|
-
reviewer: path.posix.join(logsDir, "reviewer.log")
|
|
44
|
+
reviewer: path.posix.join(logsDir, "reviewer.log"),
|
|
45
|
+
"codex-reviewer": path.posix.join(logsDir, "codex-reviewer.log")
|
|
45
46
|
},
|
|
46
47
|
messageRoutePaths: getDefaultMessageRoutePaths(messagesDir),
|
|
47
48
|
architecturePlanPath: path.posix.join(handoffDir, "architecture-plan.md"),
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isVcmRoleName } from "../../shared/constants.js";
|
|
2
2
|
import { VcmError } from "../errors.js";
|
|
3
3
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
4
4
|
export function createClaudeHookService(deps) {
|
|
5
5
|
async function getHookContext(input) {
|
|
6
|
-
if (!
|
|
6
|
+
if (!isVcmRoleName(input.role)) {
|
|
7
7
|
throw new VcmError({
|
|
8
8
|
code: "HOOK_ROLE_INVALID",
|
|
9
9
|
message: `Unknown hook role: ${input.role}`,
|
|
@@ -185,7 +185,7 @@ export function createClaudeHookService(deps) {
|
|
|
185
185
|
};
|
|
186
186
|
}
|
|
187
187
|
async function handlePermissionRequestHook(input) {
|
|
188
|
-
if (!
|
|
188
|
+
if (!isVcmRoleName(input.role)) {
|
|
189
189
|
throw new VcmError({
|
|
190
190
|
code: "HOOK_ROLE_INVALID",
|
|
191
191
|
message: `Unknown hook role: ${input.role}`,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { VcmError } from "../errors.js";
|
|
2
|
+
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
3
|
+
const CODEX_REVIEWER_ROLE = "codex-reviewer";
|
|
4
|
+
export function createCodexHookService(deps) {
|
|
5
|
+
async function getHookContext(input) {
|
|
6
|
+
if (input.role !== CODEX_REVIEWER_ROLE) {
|
|
7
|
+
throw new VcmError({
|
|
8
|
+
code: "CODEX_HOOK_ROLE_INVALID",
|
|
9
|
+
message: `Unknown Codex hook role: ${input.role}`,
|
|
10
|
+
statusCode: 400
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
const project = await deps.projectService.getCurrentProject();
|
|
14
|
+
if (!project) {
|
|
15
|
+
throw new VcmError({
|
|
16
|
+
code: "PROJECT_NOT_CONNECTED",
|
|
17
|
+
message: "Connect a repository before accepting Codex Reviewer hooks.",
|
|
18
|
+
statusCode: 409
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const config = await deps.projectService.loadConfig(project.repoRoot);
|
|
22
|
+
const task = await deps.taskService.loadTask(project.repoRoot, input.taskSlug);
|
|
23
|
+
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
24
|
+
return {
|
|
25
|
+
project,
|
|
26
|
+
config,
|
|
27
|
+
taskRepoRoot
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async function processHook(input, expectedEventName) {
|
|
31
|
+
const eventName = parseHookEvent(input.event.hook_event_name);
|
|
32
|
+
if (eventName !== expectedEventName) {
|
|
33
|
+
throw new VcmError({
|
|
34
|
+
code: "CODEX_HOOK_EVENT_UNSUPPORTED",
|
|
35
|
+
message: `Unsupported Codex Reviewer hook event for this endpoint: ${eventName}`,
|
|
36
|
+
statusCode: 400
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const context = await getHookContext(input);
|
|
40
|
+
const session = await deps.sessionService.recordRoleHookEvent(context.project.repoRoot, {
|
|
41
|
+
taskSlug: input.taskSlug,
|
|
42
|
+
role: CODEX_REVIEWER_ROLE,
|
|
43
|
+
eventName,
|
|
44
|
+
sessionId: stringOrUndefined(input.event.session_id),
|
|
45
|
+
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
46
|
+
cwd: stringOrUndefined(input.event.cwd),
|
|
47
|
+
allowSessionMismatch: true
|
|
48
|
+
});
|
|
49
|
+
await deps.roundService.recordRoleTurnEvent({
|
|
50
|
+
repoRoot: context.project.repoRoot,
|
|
51
|
+
stateRepoRoot: context.taskRepoRoot,
|
|
52
|
+
stateRoot: context.config.stateRoot,
|
|
53
|
+
taskSlug: input.taskSlug,
|
|
54
|
+
role: CODEX_REVIEWER_ROLE,
|
|
55
|
+
eventName
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
eventName,
|
|
60
|
+
taskSlug: input.taskSlug,
|
|
61
|
+
role: CODEX_REVIEWER_ROLE,
|
|
62
|
+
sessionUpdated: Boolean(session)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
handleHook(input) {
|
|
67
|
+
return processHook(input, "UserPromptSubmit");
|
|
68
|
+
},
|
|
69
|
+
handleStopHook(input) {
|
|
70
|
+
return processHook(input, "Stop");
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function parseHookEvent(value) {
|
|
75
|
+
if (value === "UserPromptSubmit" || value === "Stop") {
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
throw new VcmError({
|
|
79
|
+
code: "CODEX_HOOK_EVENT_UNSUPPORTED",
|
|
80
|
+
message: `Unsupported Codex Reviewer hook event: ${String(value)}`,
|
|
81
|
+
statusCode: 400,
|
|
82
|
+
hint: "VCM accepts Codex Reviewer UserPromptSubmit and Stop hooks only."
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function stringOrUndefined(value) {
|
|
86
|
+
return typeof value === "string" ? value : undefined;
|
|
87
|
+
}
|