vibe-coding-master 0.3.26 → 0.3.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -29
- package/dist/backend/api/codex-hook-routes.js +0 -7
- package/dist/backend/api/gate-review-routes.js +58 -0
- package/dist/backend/api/task-routes.js +2 -2
- package/dist/backend/cli/install-vcm-harness.js +15 -57
- package/dist/backend/server.js +6 -8
- package/dist/backend/services/app-settings-service.js +15 -15
- package/dist/backend/services/codex-hook-service.js +8 -50
- package/dist/backend/services/{codex-review-service.js → gate-review-service.js} +108 -108
- package/dist/backend/services/harness-service.js +15 -63
- package/dist/backend/services/session-service.js +257 -41
- package/dist/backend/services/task-service.js +2 -2
- package/dist/backend/templates/harness/claude-root.js +2 -2
- package/dist/backend/templates/harness/{codex-review.js → gate-review.js} +35 -278
- package/dist/backend/templates/harness/project-manager-agent.js +7 -7
- package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +5 -5
- package/dist/shared/constants.js +8 -5
- package/dist/shared/types/{codex-review.js → gate-review.js} +1 -1
- package/dist-frontend/assets/{index-CKWy15WL.js → index-x9I-bGUt.js} +37 -37
- package/dist-frontend/index.html +1 -1
- package/docs/codex-translation-plan.md +83 -136
- package/docs/full-harness-baseline.md +113 -207
- package/docs/gate-review-gates.md +133 -0
- package/docs/gateway-design.md +6 -8
- package/docs/product-design.md +66 -38
- package/docs/v0.2-implementation-plan.md +4 -0
- package/docs/vcm-cc-best-practices.md +36 -33
- package/package.json +1 -1
- package/scripts/verify-package.mjs +4 -4
- package/dist/backend/api/codex-review-routes.js +0 -58
- package/docs/codex-review-gates.md +0 -593
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { renderArchitectHarnessRules } from "../templates/harness/architect-agent.js";
|
|
6
6
|
import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
|
|
7
|
-
import {
|
|
7
|
+
import { renderCodexCliConfigHarnessRules, renderCodexHooksHarnessRules, renderCodexTranslatorAgentsHarnessRules, renderCodexTranslatorConfigHarnessRules, renderGateReviewerAgentRules, renderRequestGateReviewTool, renderVcmGateReviewSkillRules } from "../templates/harness/gate-review.js";
|
|
8
8
|
import { renderRootClaudeHarnessRules } from "../templates/harness/claude-root.js";
|
|
9
9
|
import { renderGitignoreHarnessRules } from "../templates/harness/gitignore.js";
|
|
10
10
|
import { renderProjectManagerHarnessRules } from "../templates/harness/project-manager-agent.js";
|
|
@@ -92,39 +92,19 @@ const HARNESS_FILES = [
|
|
|
92
92
|
renderRules: renderVcmLongRunningValidationSkillRules
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
|
-
kind: "skill-vcm-
|
|
96
|
-
path: ".claude/skills/vcm-
|
|
97
|
-
title: "VCM
|
|
98
|
-
frontmatter: renderSkillFrontmatter("vcm-
|
|
95
|
+
kind: "skill-vcm-gate-review",
|
|
96
|
+
path: ".claude/skills/vcm-gate-review/SKILL.md",
|
|
97
|
+
title: "VCM Gate Review Skill",
|
|
98
|
+
frontmatter: renderSkillFrontmatter("vcm-gate-review", "Use when project-manager reaches a Gate Review trigger or receives a VCM Gate Review callback."),
|
|
99
99
|
ownership: "whole-file",
|
|
100
|
-
renderRules:
|
|
100
|
+
renderRules: renderVcmGateReviewSkillRules
|
|
101
101
|
},
|
|
102
102
|
{
|
|
103
|
-
kind: "
|
|
104
|
-
path: ".
|
|
105
|
-
title: "
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
{
|
|
109
|
-
kind: "codex-config",
|
|
110
|
-
path: ".ai/codex/config.toml",
|
|
111
|
-
title: "VCM Codex Config",
|
|
112
|
-
ownership: "raw-file",
|
|
113
|
-
renderRules: renderCodexConfigHarnessRules
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
kind: "codex-cli-config",
|
|
117
|
-
path: ".ai/codex/.codex/config.toml",
|
|
118
|
-
title: "VCM Codex CLI Config",
|
|
119
|
-
ownership: "raw-file",
|
|
120
|
-
renderRules: renderCodexCliConfigHarnessRules
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
kind: "codex-hooks",
|
|
124
|
-
path: ".ai/codex/.codex/hooks.json",
|
|
125
|
-
title: "VCM Codex Hooks",
|
|
126
|
-
ownership: "raw-file",
|
|
127
|
-
renderRules: renderCodexHooksHarnessRules
|
|
103
|
+
kind: "agent-gate-reviewer",
|
|
104
|
+
path: ".claude/agents/gate-reviewer.md",
|
|
105
|
+
title: "Gate Reviewer Agent",
|
|
106
|
+
frontmatter: renderAgentFrontmatter("gate-reviewer", "VCM independent gate review role for architecture plans, validation adequacy, and final diffs."),
|
|
107
|
+
renderRules: renderGateReviewerAgentRules
|
|
128
108
|
},
|
|
129
109
|
{
|
|
130
110
|
kind: "codex-translator-agents",
|
|
@@ -154,39 +134,11 @@ const HARNESS_FILES = [
|
|
|
154
134
|
renderRules: () => renderCodexHooksHarnessRules("codex-translator")
|
|
155
135
|
},
|
|
156
136
|
{
|
|
157
|
-
kind: "
|
|
158
|
-
path: ".ai/
|
|
159
|
-
title: "
|
|
160
|
-
ownership: "raw-file",
|
|
161
|
-
renderRules: renderCodexArchitecturePlanPrompt
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
kind: "codex-prompt-validation-adequacy",
|
|
165
|
-
path: ".ai/codex/prompts/validation-adequacy-gate.md",
|
|
166
|
-
title: "Codex Validation Adequacy Gate Prompt",
|
|
167
|
-
ownership: "raw-file",
|
|
168
|
-
renderRules: renderCodexValidationAdequacyPrompt
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
kind: "codex-prompt-final-diff",
|
|
172
|
-
path: ".ai/codex/prompts/final-diff-gate.md",
|
|
173
|
-
title: "Codex Final Diff Gate Prompt",
|
|
174
|
-
ownership: "raw-file",
|
|
175
|
-
renderRules: renderCodexFinalDiffPrompt
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
kind: "codex-review-schema",
|
|
179
|
-
path: ".ai/codex/schemas/codex-review-result.schema.json",
|
|
180
|
-
title: "Codex Review Result Schema",
|
|
181
|
-
ownership: "raw-file",
|
|
182
|
-
renderRules: renderCodexReviewResultSchema
|
|
183
|
-
},
|
|
184
|
-
{
|
|
185
|
-
kind: "tool-request-codex-review",
|
|
186
|
-
path: ".ai/tools/request-codex-review",
|
|
187
|
-
title: "Request Codex Review Tool",
|
|
137
|
+
kind: "tool-request-gate-review",
|
|
138
|
+
path: ".ai/tools/request-gate-review",
|
|
139
|
+
title: "Request Gate Review Tool",
|
|
188
140
|
ownership: "raw-file",
|
|
189
|
-
renderRules:
|
|
141
|
+
renderRules: renderRequestGateReviewTool
|
|
190
142
|
},
|
|
191
143
|
{
|
|
192
144
|
kind: "agent-project-manager",
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { VCM_ROLE_NAMES, isCodexRoleName, isDispatchableRole } from "../../shared/constants.js";
|
|
4
4
|
import { VcmError } from "../errors.js";
|
|
5
5
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
6
6
|
import { claudeTranscriptPath } from "./claude-transcript-service.js";
|
|
7
7
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
8
|
-
const
|
|
8
|
+
const GATE_REVIEWER_ROLE = "gate-reviewer";
|
|
9
9
|
const CODEX_TRANSLATOR_ROLE = "codex-translator";
|
|
10
|
-
const CODEX_DIR = ".ai/codex";
|
|
11
|
-
const CODEX_REVIEW_DIR = ".ai/vcm/codex-reviews";
|
|
12
|
-
const CODEX_CONFIG_PATH = ".ai/codex/config.toml";
|
|
13
10
|
const CODEX_TRANSLATOR_DIR = ".ai/codex-translator";
|
|
14
11
|
const CODEX_TRANSLATION_DIR = ".ai/vcm/translations";
|
|
15
12
|
const CODEX_TRANSLATOR_SESSION_PATH = ".ai/vcm/translations/session.json";
|
|
16
13
|
const CODEX_TRANSLATOR_CONFIG_PATH = ".ai/codex-translator/config.toml";
|
|
14
|
+
const GATE_REVIEWER_SESSION_PATH = ".ai/vcm/gate-reviewer/session.json";
|
|
17
15
|
const PROJECT_TRANSLATOR_SCOPE = "__project__";
|
|
16
|
+
const PROJECT_GATE_REVIEWER_SCOPE = "__project_gate_reviewer__";
|
|
18
17
|
export function createSessionService(deps) {
|
|
19
18
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
20
19
|
async function launchRoleSession(repoRoot, taskSlug, role, input, launchMode) {
|
|
@@ -102,6 +101,75 @@ export function createSessionService(deps) {
|
|
|
102
101
|
await persistRoleSessionRecord(deps.fs, repoRoot, taskRepoRoot, config.stateRoot, record);
|
|
103
102
|
return record;
|
|
104
103
|
}
|
|
104
|
+
async function launchProjectGateReviewerSession(repoRoot, input, launchMode) {
|
|
105
|
+
const live = toRoleSessionRecordView(getRegisteredProjectGateReviewerSession(deps.registry, deps.runtime), deps.runtime);
|
|
106
|
+
if (live && live.status === "running") {
|
|
107
|
+
return live;
|
|
108
|
+
}
|
|
109
|
+
const config = await deps.projectService.loadConfig(repoRoot);
|
|
110
|
+
const persisted = await loadPersistedProjectGateReviewerSession(deps.fs, repoRoot);
|
|
111
|
+
const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
|
|
112
|
+
const model = normalizeClaudeModel(input.model ?? persisted?.model);
|
|
113
|
+
const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort);
|
|
114
|
+
const claudeSessionId = launchMode === "resume"
|
|
115
|
+
? persisted?.claudeSessionId
|
|
116
|
+
: randomUUID();
|
|
117
|
+
if (!claudeSessionId) {
|
|
118
|
+
throw new VcmError({
|
|
119
|
+
code: "GATE_REVIEWER_SESSION_MISSING",
|
|
120
|
+
message: "Gate Reviewer does not have a session id to resume.",
|
|
121
|
+
statusCode: 409,
|
|
122
|
+
hint: "Start Gate Reviewer once before using Resume."
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
|
|
126
|
+
? persisted.transcriptPath
|
|
127
|
+
: claudeTranscriptPath(repoRoot, claudeSessionId);
|
|
128
|
+
const startCommand = {
|
|
129
|
+
...deps.claude.buildRoleStartCommand(GATE_REVIEWER_ROLE, config.claudeCommand, permissionMode, claudeSessionId, launchMode === "resume", model, effort),
|
|
130
|
+
cwd: repoRoot
|
|
131
|
+
};
|
|
132
|
+
const runtimeSession = await deps.runtime.createSession({
|
|
133
|
+
taskSlug: PROJECT_GATE_REVIEWER_SCOPE,
|
|
134
|
+
role: GATE_REVIEWER_ROLE,
|
|
135
|
+
command: startCommand.command,
|
|
136
|
+
args: startCommand.args,
|
|
137
|
+
cwd: startCommand.cwd,
|
|
138
|
+
env: {
|
|
139
|
+
VCM_API_URL: deps.apiUrl,
|
|
140
|
+
VCM_TASK_REPO_ROOT: repoRoot,
|
|
141
|
+
VCM_TASK_SLUG: PROJECT_GATE_REVIEWER_SCOPE,
|
|
142
|
+
VCM_ROLE: GATE_REVIEWER_ROLE,
|
|
143
|
+
VCM_SESSION_ID: claudeSessionId
|
|
144
|
+
},
|
|
145
|
+
cols: input.cols,
|
|
146
|
+
rows: input.rows
|
|
147
|
+
});
|
|
148
|
+
const timestamp = now();
|
|
149
|
+
const record = {
|
|
150
|
+
id: runtimeSession.id,
|
|
151
|
+
claudeSessionId,
|
|
152
|
+
transcriptPath,
|
|
153
|
+
taskSlug: PROJECT_GATE_REVIEWER_SCOPE,
|
|
154
|
+
role: GATE_REVIEWER_ROLE,
|
|
155
|
+
status: runtimeSession.status,
|
|
156
|
+
activityStatus: "idle",
|
|
157
|
+
command: startCommand.display,
|
|
158
|
+
permissionMode,
|
|
159
|
+
model,
|
|
160
|
+
effort,
|
|
161
|
+
cwd: startCommand.cwd,
|
|
162
|
+
terminalBackend: "node-pty",
|
|
163
|
+
pid: runtimeSession.pid,
|
|
164
|
+
startedAt: runtimeSession.startedAt,
|
|
165
|
+
updatedAt: timestamp,
|
|
166
|
+
lastOutputAt: runtimeSession.lastOutputAt,
|
|
167
|
+
exitCode: runtimeSession.exitCode
|
|
168
|
+
};
|
|
169
|
+
deps.registry.upsert(record);
|
|
170
|
+
await persistProjectGateReviewerSession(deps.fs, repoRoot, record);
|
|
171
|
+
return record;
|
|
172
|
+
}
|
|
105
173
|
async function launchProjectTranslatorSession(repoRoot, input, launchMode) {
|
|
106
174
|
const live = toRoleSessionRecordView(getRegisteredProjectTranslatorSession(deps.registry, deps.runtime), deps.runtime);
|
|
107
175
|
if (live && live.status === "running") {
|
|
@@ -163,6 +231,67 @@ export function createSessionService(deps) {
|
|
|
163
231
|
return record;
|
|
164
232
|
}
|
|
165
233
|
return {
|
|
234
|
+
startProjectGateReviewerSession(repoRoot, input = {}) {
|
|
235
|
+
return launchProjectGateReviewerSession(repoRoot, input, "fresh");
|
|
236
|
+
},
|
|
237
|
+
resumeProjectGateReviewerSession(repoRoot, input = {}) {
|
|
238
|
+
return launchProjectGateReviewerSession(repoRoot, input, "resume");
|
|
239
|
+
},
|
|
240
|
+
async stopProjectGateReviewerSession(repoRoot) {
|
|
241
|
+
const existing = await this.getProjectGateReviewerSession(repoRoot);
|
|
242
|
+
if (!existing) {
|
|
243
|
+
throw new VcmError({
|
|
244
|
+
code: "SESSION_MISSING",
|
|
245
|
+
message: "Gate Reviewer session has not been started.",
|
|
246
|
+
statusCode: 404
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (deps.runtime.getSession(existing.id)) {
|
|
250
|
+
await deps.runtime.stop(existing.id);
|
|
251
|
+
}
|
|
252
|
+
const updated = {
|
|
253
|
+
...existing,
|
|
254
|
+
taskSlug: PROJECT_GATE_REVIEWER_SCOPE,
|
|
255
|
+
status: "exited",
|
|
256
|
+
activityStatus: "idle",
|
|
257
|
+
updatedAt: now()
|
|
258
|
+
};
|
|
259
|
+
deps.registry.upsert(updated);
|
|
260
|
+
await persistProjectGateReviewerSession(deps.fs, repoRoot, updated);
|
|
261
|
+
return updated;
|
|
262
|
+
},
|
|
263
|
+
async restartProjectGateReviewerSession(repoRoot, input = {}) {
|
|
264
|
+
const existing = await this.getProjectGateReviewerSession(repoRoot);
|
|
265
|
+
if (!existing) {
|
|
266
|
+
return launchProjectGateReviewerSession(repoRoot, input, "fresh");
|
|
267
|
+
}
|
|
268
|
+
if (deps.runtime.getSession(existing.id)) {
|
|
269
|
+
await deps.runtime.stop(existing.id);
|
|
270
|
+
}
|
|
271
|
+
deps.registry.remove(existing.id);
|
|
272
|
+
return launchProjectGateReviewerSession(repoRoot, input, "fresh");
|
|
273
|
+
},
|
|
274
|
+
async getProjectGateReviewerSession(repoRoot) {
|
|
275
|
+
const record = getRegisteredProjectGateReviewerSession(deps.registry, deps.runtime)
|
|
276
|
+
?? await loadPersistedProjectGateReviewerSession(deps.fs, repoRoot);
|
|
277
|
+
return toRoleSessionRecordView(record, deps.runtime);
|
|
278
|
+
},
|
|
279
|
+
async ensureProjectGateReviewerSession(repoRoot, input = {}) {
|
|
280
|
+
const existing = await this.getProjectGateReviewerSession(repoRoot);
|
|
281
|
+
if (existing?.status === "running") {
|
|
282
|
+
return existing;
|
|
283
|
+
}
|
|
284
|
+
if (existing?.claudeSessionId) {
|
|
285
|
+
return this.resumeProjectGateReviewerSession(repoRoot, {
|
|
286
|
+
permissionMode: input.permissionMode ?? existing.permissionMode,
|
|
287
|
+
model: input.model ?? existing.model,
|
|
288
|
+
effort: input.effort ?? existing.effort,
|
|
289
|
+
cols: input.cols,
|
|
290
|
+
rows: input.rows
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return this.startProjectGateReviewerSession(repoRoot, input);
|
|
294
|
+
},
|
|
166
295
|
startProjectTranslatorSession(repoRoot, input = {}) {
|
|
167
296
|
return launchProjectTranslatorSession(repoRoot, input, "fresh");
|
|
168
297
|
},
|
|
@@ -247,6 +376,10 @@ export function createSessionService(deps) {
|
|
|
247
376
|
return updated;
|
|
248
377
|
},
|
|
249
378
|
startRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
379
|
+
if (role === GATE_REVIEWER_ROLE) {
|
|
380
|
+
return this.startProjectGateReviewerSession(repoRoot, input)
|
|
381
|
+
.then((session) => scopeProjectRoleSession(session, taskSlug));
|
|
382
|
+
}
|
|
250
383
|
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
251
384
|
void taskSlug;
|
|
252
385
|
return this.startProjectTranslatorSession(repoRoot, input);
|
|
@@ -254,6 +387,10 @@ export function createSessionService(deps) {
|
|
|
254
387
|
return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
|
|
255
388
|
},
|
|
256
389
|
resumeRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
390
|
+
if (role === GATE_REVIEWER_ROLE) {
|
|
391
|
+
return this.resumeProjectGateReviewerSession(repoRoot, input)
|
|
392
|
+
.then((session) => scopeProjectRoleSession(session, taskSlug));
|
|
393
|
+
}
|
|
257
394
|
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
258
395
|
void taskSlug;
|
|
259
396
|
return this.resumeProjectTranslatorSession(repoRoot, input);
|
|
@@ -261,6 +398,9 @@ export function createSessionService(deps) {
|
|
|
261
398
|
return launchRoleSession(repoRoot, taskSlug, role, input, "resume");
|
|
262
399
|
},
|
|
263
400
|
async stopRoleSession(repoRoot, taskSlug, role) {
|
|
401
|
+
if (role === GATE_REVIEWER_ROLE) {
|
|
402
|
+
return scopeProjectRoleSession(await this.stopProjectGateReviewerSession(repoRoot), taskSlug);
|
|
403
|
+
}
|
|
264
404
|
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
265
405
|
void taskSlug;
|
|
266
406
|
return this.stopProjectTranslatorSession(repoRoot);
|
|
@@ -289,6 +429,9 @@ export function createSessionService(deps) {
|
|
|
289
429
|
return updated;
|
|
290
430
|
},
|
|
291
431
|
async restartRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
432
|
+
if (role === GATE_REVIEWER_ROLE) {
|
|
433
|
+
return scopeProjectRoleSession(await this.restartProjectGateReviewerSession(repoRoot, input), taskSlug);
|
|
434
|
+
}
|
|
292
435
|
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
293
436
|
void taskSlug;
|
|
294
437
|
return this.restartProjectTranslatorSession(repoRoot, input);
|
|
@@ -304,6 +447,9 @@ export function createSessionService(deps) {
|
|
|
304
447
|
return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
|
|
305
448
|
},
|
|
306
449
|
async getRoleSession(repoRoot, taskSlug, role) {
|
|
450
|
+
if (role === GATE_REVIEWER_ROLE) {
|
|
451
|
+
return scopeProjectRoleSession(await this.getProjectGateReviewerSession(repoRoot), taskSlug);
|
|
452
|
+
}
|
|
307
453
|
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
308
454
|
void taskSlug;
|
|
309
455
|
return this.getProjectTranslatorSession(repoRoot);
|
|
@@ -324,7 +470,7 @@ export function createSessionService(deps) {
|
|
|
324
470
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
325
471
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
326
472
|
const persistedTaskSession = await loadPersistedTaskSessionRecord(deps.fs, taskRepoRoot, config.stateRoot, taskSlug);
|
|
327
|
-
for (const role of
|
|
473
|
+
for (const role of VCM_ROLE_NAMES) {
|
|
328
474
|
const record = deps.registry.getByRole(taskSlug, role)
|
|
329
475
|
?? normalizePersistedRoleRecord(persistedTaskSession?.roles[role]?.record);
|
|
330
476
|
const session = toRoleSessionRecordView(record, deps.runtime);
|
|
@@ -332,9 +478,38 @@ export function createSessionService(deps) {
|
|
|
332
478
|
sessions.push(session);
|
|
333
479
|
}
|
|
334
480
|
}
|
|
481
|
+
const gateReviewerSession = scopeProjectRoleSession(await this.getProjectGateReviewerSession(repoRoot), taskSlug);
|
|
482
|
+
if (gateReviewerSession) {
|
|
483
|
+
sessions.push(gateReviewerSession);
|
|
484
|
+
}
|
|
335
485
|
return sessions;
|
|
336
486
|
},
|
|
337
487
|
async recordRoleHookEvent(repoRoot, input) {
|
|
488
|
+
if (input.role === GATE_REVIEWER_ROLE) {
|
|
489
|
+
const current = await this.getProjectGateReviewerSession(repoRoot);
|
|
490
|
+
if (!current || (!input.allowSessionMismatch && !matchesRoleHookSession(current, input))) {
|
|
491
|
+
return undefined;
|
|
492
|
+
}
|
|
493
|
+
const timestamp = now();
|
|
494
|
+
const isTurnEnd = isTurnEndHook(input.eventName);
|
|
495
|
+
const isCompact = isCompactHook(input.eventName);
|
|
496
|
+
const updated = {
|
|
497
|
+
...current,
|
|
498
|
+
taskSlug: PROJECT_GATE_REVIEWER_SCOPE,
|
|
499
|
+
claudeSessionId: input.sessionId ?? current.claudeSessionId,
|
|
500
|
+
transcriptPath: input.transcriptPath ?? current.transcriptPath,
|
|
501
|
+
cwd: input.cwd ?? current.cwd,
|
|
502
|
+
activityStatus: isTurnEnd ? "idle" : isCompact ? current.activityStatus : "running",
|
|
503
|
+
lastHookEventAt: timestamp,
|
|
504
|
+
lastTurnEndedAt: isTurnEnd ? timestamp : current.lastTurnEndedAt,
|
|
505
|
+
lastTurnStartedAt: isTurnEnd || isCompact ? current.lastTurnStartedAt : timestamp,
|
|
506
|
+
lastCompactAt: isCompact ? timestamp : current.lastCompactAt,
|
|
507
|
+
updatedAt: timestamp
|
|
508
|
+
};
|
|
509
|
+
deps.registry.upsert(updated);
|
|
510
|
+
await persistProjectGateReviewerSession(deps.fs, repoRoot, updated);
|
|
511
|
+
return scopeProjectRoleSession(updated, input.taskSlug);
|
|
512
|
+
}
|
|
338
513
|
if (input.role === CODEX_TRANSLATOR_ROLE) {
|
|
339
514
|
void input.taskSlug;
|
|
340
515
|
return this.recordProjectTranslatorHookEvent(repoRoot, {
|
|
@@ -392,6 +567,15 @@ export function createSessionService(deps) {
|
|
|
392
567
|
lastHookEventAt: timestamp,
|
|
393
568
|
updatedAt: timestamp
|
|
394
569
|
};
|
|
570
|
+
if (role === GATE_REVIEWER_ROLE) {
|
|
571
|
+
const persisted = {
|
|
572
|
+
...updated,
|
|
573
|
+
taskSlug: PROJECT_GATE_REVIEWER_SCOPE
|
|
574
|
+
};
|
|
575
|
+
deps.registry.upsert(persisted);
|
|
576
|
+
await persistProjectGateReviewerSession(deps.fs, repoRoot, persisted);
|
|
577
|
+
return updated;
|
|
578
|
+
}
|
|
395
579
|
deps.registry.upsert(updated);
|
|
396
580
|
const config = await deps.projectService.loadConfig(repoRoot);
|
|
397
581
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
@@ -410,6 +594,15 @@ export function createSessionService(deps) {
|
|
|
410
594
|
lastTurnEndedAt: timestamp,
|
|
411
595
|
updatedAt: timestamp
|
|
412
596
|
};
|
|
597
|
+
if (role === GATE_REVIEWER_ROLE) {
|
|
598
|
+
const persisted = {
|
|
599
|
+
...updated,
|
|
600
|
+
taskSlug: PROJECT_GATE_REVIEWER_SCOPE
|
|
601
|
+
};
|
|
602
|
+
deps.registry.upsert(persisted);
|
|
603
|
+
await persistProjectGateReviewerSession(deps.fs, repoRoot, persisted);
|
|
604
|
+
return updated;
|
|
605
|
+
}
|
|
413
606
|
deps.registry.upsert(updated);
|
|
414
607
|
const config = await deps.projectService.loadConfig(repoRoot);
|
|
415
608
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
@@ -440,20 +633,27 @@ function toRoleSessionRecordView(record, runtime) {
|
|
|
440
633
|
exitCode: runtimeSession.exitCode
|
|
441
634
|
};
|
|
442
635
|
}
|
|
443
|
-
async function buildCodexStartCommand(fs, baseRepoRoot,
|
|
636
|
+
async function buildCodexStartCommand(fs, baseRepoRoot, _taskRepoRoot, role, launchMode, selectedModel, selectedEffort, sandboxMode, resumeSessionId) {
|
|
444
637
|
const isTranslator = role === CODEX_TRANSLATOR_ROLE;
|
|
445
|
-
|
|
446
|
-
|
|
638
|
+
if (!isTranslator) {
|
|
639
|
+
throw new VcmError({
|
|
640
|
+
code: "CODEX_ROLE_UNSUPPORTED",
|
|
641
|
+
message: `${role} is not a Codex role.`,
|
|
642
|
+
statusCode: 400
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
const codexDir = resolveRepoPath(baseRepoRoot, CODEX_TRANSLATOR_DIR);
|
|
646
|
+
const outputDir = resolveRepoPath(baseRepoRoot, CODEX_TRANSLATION_DIR);
|
|
447
647
|
if (!(await fs.pathExists(codexDir))) {
|
|
448
648
|
throw new VcmError({
|
|
449
|
-
code:
|
|
450
|
-
message: `${
|
|
649
|
+
code: "CODEX_TRANSLATOR_CONFIG_MISSING",
|
|
650
|
+
message: `${CODEX_TRANSLATOR_DIR} does not exist.`,
|
|
451
651
|
statusCode: 409,
|
|
452
|
-
hint:
|
|
652
|
+
hint: "Apply the VCM harness before starting Codex Translator."
|
|
453
653
|
});
|
|
454
654
|
}
|
|
455
655
|
await fs.ensureDir(outputDir);
|
|
456
|
-
const config = await loadCodexSessionConfig(fs,
|
|
656
|
+
const config = await loadCodexSessionConfig(fs, baseRepoRoot, CODEX_TRANSLATOR_CONFIG_PATH);
|
|
457
657
|
const args = launchMode === "resume"
|
|
458
658
|
? resumeSessionId ? ["resume", resumeSessionId] : ["resume", "--last"]
|
|
459
659
|
: [];
|
|
@@ -462,12 +662,7 @@ async function buildCodexStartCommand(fs, baseRepoRoot, taskRepoRoot, role, laun
|
|
|
462
662
|
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
463
663
|
}
|
|
464
664
|
else {
|
|
465
|
-
|
|
466
|
-
args.push("--add-dir", baseRepoRoot);
|
|
467
|
-
}
|
|
468
|
-
else {
|
|
469
|
-
args.push("--add-dir", outputDir);
|
|
470
|
-
}
|
|
665
|
+
args.push("--add-dir", baseRepoRoot);
|
|
471
666
|
args.push("--sandbox", "workspace-write", "--ask-for-approval", "never");
|
|
472
667
|
}
|
|
473
668
|
args.push("--dangerously-bypass-hook-trust", "--search");
|
|
@@ -480,7 +675,7 @@ async function buildCodexStartCommand(fs, baseRepoRoot, taskRepoRoot, role, laun
|
|
|
480
675
|
return {
|
|
481
676
|
command: config.command,
|
|
482
677
|
args,
|
|
483
|
-
cwd:
|
|
678
|
+
cwd: baseRepoRoot,
|
|
484
679
|
display: [config.command, ...args].map(formatDisplayArg).join(" ")
|
|
485
680
|
};
|
|
486
681
|
}
|
|
@@ -508,9 +703,8 @@ async function loadCodexSessionConfig(fs, repoRoot, configRelativePath) {
|
|
|
508
703
|
};
|
|
509
704
|
}
|
|
510
705
|
const content = await fs.readText(configPath);
|
|
511
|
-
const reviewSection = extractTomlSection(content, "vcm.codex_review");
|
|
512
706
|
return {
|
|
513
|
-
command: parseTomlString(content, "command") ??
|
|
707
|
+
command: parseTomlString(content, "command") ?? "codex"
|
|
514
708
|
};
|
|
515
709
|
}
|
|
516
710
|
function matchesRoleHookSession(record, input) {
|
|
@@ -550,7 +744,7 @@ function getHandoffArtifactPath(paths, role) {
|
|
|
550
744
|
return undefined;
|
|
551
745
|
}
|
|
552
746
|
function getRegisteredRoleSession(registry, runtime, taskSlug, role) {
|
|
553
|
-
if (role !== CODEX_TRANSLATOR_ROLE) {
|
|
747
|
+
if (role !== CODEX_TRANSLATOR_ROLE && role !== GATE_REVIEWER_ROLE) {
|
|
554
748
|
return registry.getByRole(taskSlug, role);
|
|
555
749
|
}
|
|
556
750
|
const candidates = registry.list().filter((session) => session.role === role);
|
|
@@ -560,6 +754,11 @@ function getRegisteredRoleSession(registry, runtime, taskSlug, role) {
|
|
|
560
754
|
?? candidates.sort(compareSessionUpdatedAtDesc)[0];
|
|
561
755
|
return scopeProjectRoleSession(scoped, taskSlug);
|
|
562
756
|
}
|
|
757
|
+
function getRegisteredProjectGateReviewerSession(registry, runtime) {
|
|
758
|
+
const candidates = registry.list().filter((session) => session.role === GATE_REVIEWER_ROLE);
|
|
759
|
+
const live = candidates.find((session) => runtime.getSession(session.id)?.status === "running");
|
|
760
|
+
return live ?? candidates.sort(compareSessionUpdatedAtDesc)[0];
|
|
761
|
+
}
|
|
563
762
|
function getRegisteredProjectTranslatorSession(registry, runtime) {
|
|
564
763
|
const candidates = registry.list().filter((session) => session.role === CODEX_TRANSLATOR_ROLE);
|
|
565
764
|
const live = candidates.find((session) => runtime.getSession(session.id)?.status === "running");
|
|
@@ -569,7 +768,7 @@ function compareSessionUpdatedAtDesc(left, right) {
|
|
|
569
768
|
return (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
|
|
570
769
|
}
|
|
571
770
|
function scopeProjectRoleSession(record, taskSlug) {
|
|
572
|
-
if (!record || record.role !== CODEX_TRANSLATOR_ROLE) {
|
|
771
|
+
if (!record || (record.role !== CODEX_TRANSLATOR_ROLE && record.role !== GATE_REVIEWER_ROLE)) {
|
|
573
772
|
return record;
|
|
574
773
|
}
|
|
575
774
|
return {
|
|
@@ -578,12 +777,31 @@ function scopeProjectRoleSession(record, taskSlug) {
|
|
|
578
777
|
};
|
|
579
778
|
}
|
|
580
779
|
async function loadPersistedRoleRecordForRole(fs, baseRepoRoot, taskRepoRoot, stateRoot, taskSlug, role) {
|
|
780
|
+
if (role === GATE_REVIEWER_ROLE) {
|
|
781
|
+
void taskSlug;
|
|
782
|
+
return loadPersistedProjectGateReviewerSession(fs, baseRepoRoot);
|
|
783
|
+
}
|
|
581
784
|
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
582
785
|
void taskSlug;
|
|
583
786
|
return loadPersistedCodexTranslatorSession(fs, baseRepoRoot);
|
|
584
787
|
}
|
|
585
788
|
return loadPersistedRoleRecord(fs, taskRepoRoot, stateRoot, taskSlug, role);
|
|
586
789
|
}
|
|
790
|
+
async function loadPersistedProjectGateReviewerSession(fs, repoRoot) {
|
|
791
|
+
const sessionPath = resolveRepoPath(repoRoot, GATE_REVIEWER_SESSION_PATH);
|
|
792
|
+
if (!(await fs.pathExists(sessionPath))) {
|
|
793
|
+
return undefined;
|
|
794
|
+
}
|
|
795
|
+
const payload = await fs.readJson(sessionPath);
|
|
796
|
+
const record = normalizePersistedRoleRecord(isProjectRoleSessionFile(payload) ? payload.record : payload);
|
|
797
|
+
if (!record) {
|
|
798
|
+
return undefined;
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
...record,
|
|
802
|
+
taskSlug: PROJECT_GATE_REVIEWER_SCOPE
|
|
803
|
+
};
|
|
804
|
+
}
|
|
587
805
|
async function loadPersistedCodexTranslatorSession(fs, repoRoot) {
|
|
588
806
|
const sessionPath = resolveRepoPath(repoRoot, CODEX_TRANSLATOR_SESSION_PATH);
|
|
589
807
|
if (!(await fs.pathExists(sessionPath))) {
|
|
@@ -657,12 +875,27 @@ async function persistTaskSession(fs, repoRoot, stateRoot, session) {
|
|
|
657
875
|
});
|
|
658
876
|
}
|
|
659
877
|
async function persistRoleSessionRecord(fs, baseRepoRoot, taskRepoRoot, stateRoot, session) {
|
|
878
|
+
if (session.role === GATE_REVIEWER_ROLE) {
|
|
879
|
+
await persistProjectGateReviewerSession(fs, baseRepoRoot, session);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
660
882
|
if (session.role === CODEX_TRANSLATOR_ROLE) {
|
|
661
883
|
await persistCodexTranslatorSession(fs, baseRepoRoot, session);
|
|
662
884
|
return;
|
|
663
885
|
}
|
|
664
886
|
await persistTaskSession(fs, taskRepoRoot, stateRoot, session);
|
|
665
887
|
}
|
|
888
|
+
async function persistProjectGateReviewerSession(fs, repoRoot, session) {
|
|
889
|
+
await fs.writeJsonAtomic(resolveRepoPath(repoRoot, GATE_REVIEWER_SESSION_PATH), {
|
|
890
|
+
version: 1,
|
|
891
|
+
role: session.role,
|
|
892
|
+
updatedAt: session.updatedAt,
|
|
893
|
+
record: {
|
|
894
|
+
...session,
|
|
895
|
+
taskSlug: PROJECT_GATE_REVIEWER_SCOPE
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
}
|
|
666
899
|
async function persistCodexTranslatorSession(fs, repoRoot, session) {
|
|
667
900
|
await fs.writeJsonAtomic(resolveRepoPath(repoRoot, CODEX_TRANSLATOR_SESSION_PATH), {
|
|
668
901
|
version: 1,
|
|
@@ -736,23 +969,6 @@ function normalizeCodexEffort(value) {
|
|
|
736
969
|
}
|
|
737
970
|
return "default";
|
|
738
971
|
}
|
|
739
|
-
function extractTomlSection(content, sectionName) {
|
|
740
|
-
const lines = content.split(/\r?\n/);
|
|
741
|
-
const header = `[${sectionName}]`;
|
|
742
|
-
const start = lines.findIndex((line) => line.trim() === header);
|
|
743
|
-
if (start < 0) {
|
|
744
|
-
return "";
|
|
745
|
-
}
|
|
746
|
-
const section = [];
|
|
747
|
-
for (let index = start + 1; index < lines.length; index += 1) {
|
|
748
|
-
const line = lines[index];
|
|
749
|
-
if (/^\s*\[[^\]]+\]\s*$/.test(line)) {
|
|
750
|
-
break;
|
|
751
|
-
}
|
|
752
|
-
section.push(line);
|
|
753
|
-
}
|
|
754
|
-
return section.join("\n");
|
|
755
|
-
}
|
|
756
972
|
function parseTomlString(content, key) {
|
|
757
973
|
const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"([^"]*)"\\s*$`, "m");
|
|
758
974
|
return pattern.exec(content)?.[1];
|
|
@@ -179,7 +179,7 @@ async function ensureTaskRuntimeStateDirs(fs, taskRepoRoot, stateRoot) {
|
|
|
179
179
|
await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "messages"));
|
|
180
180
|
await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "orchestration"));
|
|
181
181
|
await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "translation"));
|
|
182
|
-
await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "
|
|
182
|
+
await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "gate-reviews"));
|
|
183
183
|
}
|
|
184
184
|
function getTaskStatePaths(taskStoreRoot, taskRepoRoot, stateRoot, handoffRoot, taskSlug) {
|
|
185
185
|
return [
|
|
@@ -188,7 +188,7 @@ function getTaskStatePaths(taskStoreRoot, taskRepoRoot, stateRoot, handoffRoot,
|
|
|
188
188
|
path.join(taskRepoRoot, stateRoot, "messages", `${taskSlug}.jsonl`),
|
|
189
189
|
path.join(taskRepoRoot, stateRoot, "orchestration", `${taskSlug}.json`),
|
|
190
190
|
path.join(taskRepoRoot, stateRoot, "translation", taskSlug),
|
|
191
|
-
path.join(taskRepoRoot, stateRoot, "
|
|
191
|
+
path.join(taskRepoRoot, stateRoot, "gate-reviews"),
|
|
192
192
|
path.join(taskRepoRoot, handoffRoot)
|
|
193
193
|
];
|
|
194
194
|
}
|
|
@@ -5,7 +5,7 @@ export function renderRootClaudeHarnessRules() {
|
|
|
5
5
|
- Read module-local \`CLAUDE.md\` before editing a subdirectory if one exists.
|
|
6
6
|
- Use \`vcm-route-message\` whenever a VCM role hands off work, asks another role a question, reports a result, reports a blocker, or raises a finding. Follow its write-then-stop rule.
|
|
7
7
|
- Use \`vcm-long-running-validation\` for long-running validation. Follow the background job limits below.
|
|
8
|
-
- Project-manager uses \`vcm-
|
|
8
|
+
- Project-manager uses \`vcm-gate-review\` at enabled Gate Review trigger points and on VCM Gate Review callbacks.
|
|
9
9
|
|
|
10
10
|
## VCM Background Jobs
|
|
11
11
|
|
|
@@ -32,7 +32,7 @@ export function renderRootClaudeHarnessRules() {
|
|
|
32
32
|
- Test-only or validation-only work may use: \`project-manager -> reviewer -> project-manager final acceptance\`.
|
|
33
33
|
- If a docs/test/validation-only task reveals required code, architecture, public contract, dependency, durable-doc, or test-strategy changes, route back through the full code-change flow.
|
|
34
34
|
- Keep role outputs under \`.ai/vcm/handoffs/\`.
|
|
35
|
-
-
|
|
35
|
+
- Gate Review Gate reports live under \`.ai/vcm/gate-reviews/\` and are VCM-managed task evidence.
|
|
36
36
|
- Runtime task records and handoffs under \`.ai/vcm/\` are temporary. Durable facts must move into code, tests, PR text, commit history, or long-term docs.
|
|
37
37
|
- Record current-task unresolved findings in \`.ai/vcm/handoffs/known-issues.md\`.
|
|
38
38
|
|