vibe-coding-master 0.3.25 → 0.3.27
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 +7 -7
- 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 +17 -57
- package/dist/backend/server.js +7 -8
- package/dist/backend/services/app-settings-service.js +15 -15
- package/dist/backend/services/claude-hook-service.js +138 -28
- 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 +17 -63
- package/dist/backend/services/session-service.js +275 -49
- 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-DVy34Iwn.js} +31 -31
- package/dist-frontend/index.html +1 -1
- package/docs/codex-translation-plan.md +9 -9
- package/docs/gate-review-gates.md +133 -0
- package/docs/product-design.md +37 -40
- 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
|
@@ -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
|
},
|
|
@@ -228,16 +357,18 @@ export function createSessionService(deps) {
|
|
|
228
357
|
return undefined;
|
|
229
358
|
}
|
|
230
359
|
const timestamp = now();
|
|
231
|
-
const
|
|
360
|
+
const isTurnEnd = isTurnEndHook(input.eventName);
|
|
361
|
+
const isCompact = isCompactHook(input.eventName);
|
|
232
362
|
const updated = {
|
|
233
363
|
...current,
|
|
234
364
|
claudeSessionId: input.sessionId ?? current.claudeSessionId,
|
|
235
365
|
transcriptPath: input.transcriptPath ?? current.transcriptPath,
|
|
236
366
|
cwd: input.cwd ?? current.cwd,
|
|
237
|
-
activityStatus:
|
|
367
|
+
activityStatus: isTurnEnd ? "idle" : isCompact ? current.activityStatus : "running",
|
|
238
368
|
lastHookEventAt: timestamp,
|
|
239
|
-
lastTurnEndedAt:
|
|
240
|
-
lastTurnStartedAt:
|
|
369
|
+
lastTurnEndedAt: isTurnEnd ? timestamp : current.lastTurnEndedAt,
|
|
370
|
+
lastTurnStartedAt: isTurnEnd || isCompact ? current.lastTurnStartedAt : timestamp,
|
|
371
|
+
lastCompactAt: isCompact ? timestamp : current.lastCompactAt,
|
|
241
372
|
updatedAt: timestamp
|
|
242
373
|
};
|
|
243
374
|
deps.registry.upsert(updated);
|
|
@@ -245,6 +376,10 @@ export function createSessionService(deps) {
|
|
|
245
376
|
return updated;
|
|
246
377
|
},
|
|
247
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
|
+
}
|
|
248
383
|
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
249
384
|
void taskSlug;
|
|
250
385
|
return this.startProjectTranslatorSession(repoRoot, input);
|
|
@@ -252,6 +387,10 @@ export function createSessionService(deps) {
|
|
|
252
387
|
return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
|
|
253
388
|
},
|
|
254
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
|
+
}
|
|
255
394
|
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
256
395
|
void taskSlug;
|
|
257
396
|
return this.resumeProjectTranslatorSession(repoRoot, input);
|
|
@@ -259,6 +398,9 @@ export function createSessionService(deps) {
|
|
|
259
398
|
return launchRoleSession(repoRoot, taskSlug, role, input, "resume");
|
|
260
399
|
},
|
|
261
400
|
async stopRoleSession(repoRoot, taskSlug, role) {
|
|
401
|
+
if (role === GATE_REVIEWER_ROLE) {
|
|
402
|
+
return scopeProjectRoleSession(await this.stopProjectGateReviewerSession(repoRoot), taskSlug);
|
|
403
|
+
}
|
|
262
404
|
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
263
405
|
void taskSlug;
|
|
264
406
|
return this.stopProjectTranslatorSession(repoRoot);
|
|
@@ -287,6 +429,9 @@ export function createSessionService(deps) {
|
|
|
287
429
|
return updated;
|
|
288
430
|
},
|
|
289
431
|
async restartRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
432
|
+
if (role === GATE_REVIEWER_ROLE) {
|
|
433
|
+
return scopeProjectRoleSession(await this.restartProjectGateReviewerSession(repoRoot, input), taskSlug);
|
|
434
|
+
}
|
|
290
435
|
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
291
436
|
void taskSlug;
|
|
292
437
|
return this.restartProjectTranslatorSession(repoRoot, input);
|
|
@@ -302,6 +447,9 @@ export function createSessionService(deps) {
|
|
|
302
447
|
return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
|
|
303
448
|
},
|
|
304
449
|
async getRoleSession(repoRoot, taskSlug, role) {
|
|
450
|
+
if (role === GATE_REVIEWER_ROLE) {
|
|
451
|
+
return scopeProjectRoleSession(await this.getProjectGateReviewerSession(repoRoot), taskSlug);
|
|
452
|
+
}
|
|
305
453
|
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
306
454
|
void taskSlug;
|
|
307
455
|
return this.getProjectTranslatorSession(repoRoot);
|
|
@@ -322,7 +470,7 @@ export function createSessionService(deps) {
|
|
|
322
470
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
323
471
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
324
472
|
const persistedTaskSession = await loadPersistedTaskSessionRecord(deps.fs, taskRepoRoot, config.stateRoot, taskSlug);
|
|
325
|
-
for (const role of
|
|
473
|
+
for (const role of VCM_ROLE_NAMES) {
|
|
326
474
|
const record = deps.registry.getByRole(taskSlug, role)
|
|
327
475
|
?? normalizePersistedRoleRecord(persistedTaskSession?.roles[role]?.record);
|
|
328
476
|
const session = toRoleSessionRecordView(record, deps.runtime);
|
|
@@ -330,9 +478,38 @@ export function createSessionService(deps) {
|
|
|
330
478
|
sessions.push(session);
|
|
331
479
|
}
|
|
332
480
|
}
|
|
481
|
+
const gateReviewerSession = scopeProjectRoleSession(await this.getProjectGateReviewerSession(repoRoot), taskSlug);
|
|
482
|
+
if (gateReviewerSession) {
|
|
483
|
+
sessions.push(gateReviewerSession);
|
|
484
|
+
}
|
|
333
485
|
return sessions;
|
|
334
486
|
},
|
|
335
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
|
+
}
|
|
336
513
|
if (input.role === CODEX_TRANSLATOR_ROLE) {
|
|
337
514
|
void input.taskSlug;
|
|
338
515
|
return this.recordProjectTranslatorHookEvent(repoRoot, {
|
|
@@ -347,16 +524,18 @@ export function createSessionService(deps) {
|
|
|
347
524
|
return undefined;
|
|
348
525
|
}
|
|
349
526
|
const timestamp = now();
|
|
350
|
-
const
|
|
527
|
+
const isTurnEnd = isTurnEndHook(input.eventName);
|
|
528
|
+
const isCompact = isCompactHook(input.eventName);
|
|
351
529
|
const updated = {
|
|
352
530
|
...current,
|
|
353
531
|
claudeSessionId: input.sessionId ?? current.claudeSessionId,
|
|
354
532
|
transcriptPath: input.transcriptPath ?? current.transcriptPath,
|
|
355
533
|
cwd: input.cwd ?? current.cwd,
|
|
356
|
-
activityStatus:
|
|
534
|
+
activityStatus: isTurnEnd ? "idle" : isCompact ? current.activityStatus : "running",
|
|
357
535
|
lastHookEventAt: timestamp,
|
|
358
|
-
lastTurnEndedAt:
|
|
359
|
-
lastTurnStartedAt:
|
|
536
|
+
lastTurnEndedAt: isTurnEnd ? timestamp : current.lastTurnEndedAt,
|
|
537
|
+
lastTurnStartedAt: isTurnEnd || isCompact ? current.lastTurnStartedAt : timestamp,
|
|
538
|
+
lastCompactAt: isCompact ? timestamp : current.lastCompactAt,
|
|
360
539
|
updatedAt: timestamp
|
|
361
540
|
};
|
|
362
541
|
deps.registry.upsert(updated);
|
|
@@ -388,6 +567,15 @@ export function createSessionService(deps) {
|
|
|
388
567
|
lastHookEventAt: timestamp,
|
|
389
568
|
updatedAt: timestamp
|
|
390
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
|
+
}
|
|
391
579
|
deps.registry.upsert(updated);
|
|
392
580
|
const config = await deps.projectService.loadConfig(repoRoot);
|
|
393
581
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
@@ -406,6 +594,15 @@ export function createSessionService(deps) {
|
|
|
406
594
|
lastTurnEndedAt: timestamp,
|
|
407
595
|
updatedAt: timestamp
|
|
408
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
|
+
}
|
|
409
606
|
deps.registry.upsert(updated);
|
|
410
607
|
const config = await deps.projectService.loadConfig(repoRoot);
|
|
411
608
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
@@ -436,20 +633,27 @@ function toRoleSessionRecordView(record, runtime) {
|
|
|
436
633
|
exitCode: runtimeSession.exitCode
|
|
437
634
|
};
|
|
438
635
|
}
|
|
439
|
-
async function buildCodexStartCommand(fs, baseRepoRoot,
|
|
636
|
+
async function buildCodexStartCommand(fs, baseRepoRoot, _taskRepoRoot, role, launchMode, selectedModel, selectedEffort, sandboxMode, resumeSessionId) {
|
|
440
637
|
const isTranslator = role === CODEX_TRANSLATOR_ROLE;
|
|
441
|
-
|
|
442
|
-
|
|
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);
|
|
443
647
|
if (!(await fs.pathExists(codexDir))) {
|
|
444
648
|
throw new VcmError({
|
|
445
|
-
code:
|
|
446
|
-
message: `${
|
|
649
|
+
code: "CODEX_TRANSLATOR_CONFIG_MISSING",
|
|
650
|
+
message: `${CODEX_TRANSLATOR_DIR} does not exist.`,
|
|
447
651
|
statusCode: 409,
|
|
448
|
-
hint:
|
|
652
|
+
hint: "Apply the VCM harness before starting Codex Translator."
|
|
449
653
|
});
|
|
450
654
|
}
|
|
451
655
|
await fs.ensureDir(outputDir);
|
|
452
|
-
const config = await loadCodexSessionConfig(fs,
|
|
656
|
+
const config = await loadCodexSessionConfig(fs, baseRepoRoot, CODEX_TRANSLATOR_CONFIG_PATH);
|
|
453
657
|
const args = launchMode === "resume"
|
|
454
658
|
? resumeSessionId ? ["resume", resumeSessionId] : ["resume", "--last"]
|
|
455
659
|
: [];
|
|
@@ -458,12 +662,7 @@ async function buildCodexStartCommand(fs, baseRepoRoot, taskRepoRoot, role, laun
|
|
|
458
662
|
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
459
663
|
}
|
|
460
664
|
else {
|
|
461
|
-
|
|
462
|
-
args.push("--add-dir", baseRepoRoot);
|
|
463
|
-
}
|
|
464
|
-
else {
|
|
465
|
-
args.push("--add-dir", outputDir);
|
|
466
|
-
}
|
|
665
|
+
args.push("--add-dir", baseRepoRoot);
|
|
467
666
|
args.push("--sandbox", "workspace-write", "--ask-for-approval", "never");
|
|
468
667
|
}
|
|
469
668
|
args.push("--dangerously-bypass-hook-trust", "--search");
|
|
@@ -476,7 +675,7 @@ async function buildCodexStartCommand(fs, baseRepoRoot, taskRepoRoot, role, laun
|
|
|
476
675
|
return {
|
|
477
676
|
command: config.command,
|
|
478
677
|
args,
|
|
479
|
-
cwd:
|
|
678
|
+
cwd: baseRepoRoot,
|
|
480
679
|
display: [config.command, ...args].map(formatDisplayArg).join(" ")
|
|
481
680
|
};
|
|
482
681
|
}
|
|
@@ -504,9 +703,8 @@ async function loadCodexSessionConfig(fs, repoRoot, configRelativePath) {
|
|
|
504
703
|
};
|
|
505
704
|
}
|
|
506
705
|
const content = await fs.readText(configPath);
|
|
507
|
-
const reviewSection = extractTomlSection(content, "vcm.codex_review");
|
|
508
706
|
return {
|
|
509
|
-
command: parseTomlString(content, "command") ??
|
|
707
|
+
command: parseTomlString(content, "command") ?? "codex"
|
|
510
708
|
};
|
|
511
709
|
}
|
|
512
710
|
function matchesRoleHookSession(record, input) {
|
|
@@ -521,6 +719,12 @@ function matchesRoleHookSession(record, input) {
|
|
|
521
719
|
}
|
|
522
720
|
return false;
|
|
523
721
|
}
|
|
722
|
+
function isTurnEndHook(eventName) {
|
|
723
|
+
return eventName === "Stop" || eventName === "StopFailure";
|
|
724
|
+
}
|
|
725
|
+
function isCompactHook(eventName) {
|
|
726
|
+
return eventName === "PostCompact";
|
|
727
|
+
}
|
|
524
728
|
function getRecoverableStatus(record) {
|
|
525
729
|
if (!record.claudeSessionId) {
|
|
526
730
|
return record.status === "running" ? "missing" : record.status;
|
|
@@ -540,7 +744,7 @@ function getHandoffArtifactPath(paths, role) {
|
|
|
540
744
|
return undefined;
|
|
541
745
|
}
|
|
542
746
|
function getRegisteredRoleSession(registry, runtime, taskSlug, role) {
|
|
543
|
-
if (role !== CODEX_TRANSLATOR_ROLE) {
|
|
747
|
+
if (role !== CODEX_TRANSLATOR_ROLE && role !== GATE_REVIEWER_ROLE) {
|
|
544
748
|
return registry.getByRole(taskSlug, role);
|
|
545
749
|
}
|
|
546
750
|
const candidates = registry.list().filter((session) => session.role === role);
|
|
@@ -550,6 +754,11 @@ function getRegisteredRoleSession(registry, runtime, taskSlug, role) {
|
|
|
550
754
|
?? candidates.sort(compareSessionUpdatedAtDesc)[0];
|
|
551
755
|
return scopeProjectRoleSession(scoped, taskSlug);
|
|
552
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
|
+
}
|
|
553
762
|
function getRegisteredProjectTranslatorSession(registry, runtime) {
|
|
554
763
|
const candidates = registry.list().filter((session) => session.role === CODEX_TRANSLATOR_ROLE);
|
|
555
764
|
const live = candidates.find((session) => runtime.getSession(session.id)?.status === "running");
|
|
@@ -559,7 +768,7 @@ function compareSessionUpdatedAtDesc(left, right) {
|
|
|
559
768
|
return (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
|
|
560
769
|
}
|
|
561
770
|
function scopeProjectRoleSession(record, taskSlug) {
|
|
562
|
-
if (!record || record.role !== CODEX_TRANSLATOR_ROLE) {
|
|
771
|
+
if (!record || (record.role !== CODEX_TRANSLATOR_ROLE && record.role !== GATE_REVIEWER_ROLE)) {
|
|
563
772
|
return record;
|
|
564
773
|
}
|
|
565
774
|
return {
|
|
@@ -568,12 +777,31 @@ function scopeProjectRoleSession(record, taskSlug) {
|
|
|
568
777
|
};
|
|
569
778
|
}
|
|
570
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
|
+
}
|
|
571
784
|
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
572
785
|
void taskSlug;
|
|
573
786
|
return loadPersistedCodexTranslatorSession(fs, baseRepoRoot);
|
|
574
787
|
}
|
|
575
788
|
return loadPersistedRoleRecord(fs, taskRepoRoot, stateRoot, taskSlug, role);
|
|
576
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
|
+
}
|
|
577
805
|
async function loadPersistedCodexTranslatorSession(fs, repoRoot) {
|
|
578
806
|
const sessionPath = resolveRepoPath(repoRoot, CODEX_TRANSLATOR_SESSION_PATH);
|
|
579
807
|
if (!(await fs.pathExists(sessionPath))) {
|
|
@@ -647,12 +875,27 @@ async function persistTaskSession(fs, repoRoot, stateRoot, session) {
|
|
|
647
875
|
});
|
|
648
876
|
}
|
|
649
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
|
+
}
|
|
650
882
|
if (session.role === CODEX_TRANSLATOR_ROLE) {
|
|
651
883
|
await persistCodexTranslatorSession(fs, baseRepoRoot, session);
|
|
652
884
|
return;
|
|
653
885
|
}
|
|
654
886
|
await persistTaskSession(fs, taskRepoRoot, stateRoot, session);
|
|
655
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
|
+
}
|
|
656
899
|
async function persistCodexTranslatorSession(fs, repoRoot, session) {
|
|
657
900
|
await fs.writeJsonAtomic(resolveRepoPath(repoRoot, CODEX_TRANSLATOR_SESSION_PATH), {
|
|
658
901
|
version: 1,
|
|
@@ -726,23 +969,6 @@ function normalizeCodexEffort(value) {
|
|
|
726
969
|
}
|
|
727
970
|
return "default";
|
|
728
971
|
}
|
|
729
|
-
function extractTomlSection(content, sectionName) {
|
|
730
|
-
const lines = content.split(/\r?\n/);
|
|
731
|
-
const header = `[${sectionName}]`;
|
|
732
|
-
const start = lines.findIndex((line) => line.trim() === header);
|
|
733
|
-
if (start < 0) {
|
|
734
|
-
return "";
|
|
735
|
-
}
|
|
736
|
-
const section = [];
|
|
737
|
-
for (let index = start + 1; index < lines.length; index += 1) {
|
|
738
|
-
const line = lines[index];
|
|
739
|
-
if (/^\s*\[[^\]]+\]\s*$/.test(line)) {
|
|
740
|
-
break;
|
|
741
|
-
}
|
|
742
|
-
section.push(line);
|
|
743
|
-
}
|
|
744
|
-
return section.join("\n");
|
|
745
|
-
}
|
|
746
972
|
function parseTomlString(content, key) {
|
|
747
973
|
const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"([^"]*)"\\s*$`, "m");
|
|
748
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
|
|