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
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { isCodexRoleName } from "../../shared/constants.js";
|
|
2
2
|
import { VcmError } from "../errors.js";
|
|
3
|
-
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
4
3
|
export function createCodexHookService(deps) {
|
|
5
4
|
async function getHookContext(input) {
|
|
6
5
|
if (!isCodexRoleName(input.role)) {
|
|
@@ -18,20 +17,8 @@ export function createCodexHookService(deps) {
|
|
|
18
17
|
statusCode: 409
|
|
19
18
|
});
|
|
20
19
|
}
|
|
21
|
-
if (input.role === "codex-translator") {
|
|
22
|
-
return {
|
|
23
|
-
project,
|
|
24
|
-
config: undefined,
|
|
25
|
-
taskRepoRoot: undefined
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
const config = await deps.projectService.loadConfig(project.repoRoot);
|
|
29
|
-
const task = await deps.taskService.loadTask(project.repoRoot, input.taskSlug);
|
|
30
|
-
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
31
20
|
return {
|
|
32
|
-
project
|
|
33
|
-
config,
|
|
34
|
-
taskRepoRoot
|
|
21
|
+
project
|
|
35
22
|
};
|
|
36
23
|
}
|
|
37
24
|
async function processHook(input, expectedEventName) {
|
|
@@ -44,42 +31,13 @@ export function createCodexHookService(deps) {
|
|
|
44
31
|
});
|
|
45
32
|
}
|
|
46
33
|
const context = await getHookContext(input);
|
|
47
|
-
const session =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
: await deps.sessionService.recordRoleHookEvent(context.project.repoRoot, {
|
|
55
|
-
taskSlug: input.taskSlug,
|
|
56
|
-
role: input.role,
|
|
57
|
-
eventName,
|
|
58
|
-
sessionId: stringOrUndefined(input.event.session_id),
|
|
59
|
-
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
60
|
-
cwd: stringOrUndefined(input.event.cwd),
|
|
61
|
-
allowSessionMismatch: true
|
|
62
|
-
});
|
|
63
|
-
if (input.role === "codex-reviewer") {
|
|
64
|
-
if (!context.config || !context.taskRepoRoot) {
|
|
65
|
-
throw new VcmError({
|
|
66
|
-
code: "CODEX_HOOK_TASK_CONTEXT_MISSING",
|
|
67
|
-
message: "Codex Reviewer hook is missing task context.",
|
|
68
|
-
statusCode: 500
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
await deps.roundService.recordRoleTurnEvent({
|
|
72
|
-
repoRoot: context.project.repoRoot,
|
|
73
|
-
stateRepoRoot: context.taskRepoRoot,
|
|
74
|
-
stateRoot: context.config.stateRoot,
|
|
75
|
-
taskSlug: input.taskSlug,
|
|
76
|
-
role: input.role,
|
|
77
|
-
eventName
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
if (input.role === "codex-translator") {
|
|
81
|
-
await deps.codexTranslationService?.handleCodexHook(context.project.repoRoot, eventName, input.taskSlug);
|
|
82
|
-
}
|
|
34
|
+
const session = await deps.sessionService.recordProjectTranslatorHookEvent(context.project.repoRoot, {
|
|
35
|
+
eventName,
|
|
36
|
+
sessionId: stringOrUndefined(input.event.session_id),
|
|
37
|
+
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
38
|
+
cwd: stringOrUndefined(input.event.cwd)
|
|
39
|
+
});
|
|
40
|
+
await deps.codexTranslationService?.handleCodexHook(context.project.repoRoot, eventName, input.taskSlug);
|
|
83
41
|
return {
|
|
84
42
|
ok: true,
|
|
85
43
|
eventName,
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { GATE_REVIEW_GATES } from "../../shared/types/gate-review.js";
|
|
4
4
|
import { VcmError } from "../errors.js";
|
|
5
5
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
6
6
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
7
7
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const CODEX_REVIEWER_ROLE = "codex-reviewer";
|
|
8
|
+
const GATE_REVIEW_AGENT_PATH = ".claude/agents/gate-reviewer.md";
|
|
9
|
+
const GATE_REVIEW_DIR = ".ai/vcm/gate-reviews";
|
|
10
|
+
const REQUESTS_DIR = ".ai/vcm/gate-reviews/requests";
|
|
11
|
+
const GATE_REVIEW_VERSION = 1;
|
|
12
|
+
const GATE_REVIEWER_ROLE = "gate-reviewer";
|
|
14
13
|
const DEFAULT_REPORT_POLL_INTERVAL_MS = 1000;
|
|
15
14
|
const DEFAULT_REPORT_TIMEOUT_MS = 30 * 60 * 1000;
|
|
16
15
|
const activeRuns = new Set();
|
|
@@ -30,7 +29,7 @@ const SOURCE_ARTIFACTS = {
|
|
|
30
29
|
]
|
|
31
30
|
};
|
|
32
31
|
const VALID_SEVERITIES = new Set(["critical", "high", "medium", "low"]);
|
|
33
|
-
export function
|
|
32
|
+
export function createGateReviewService(deps) {
|
|
34
33
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
35
34
|
const reportPollIntervalMs = deps.reportPollIntervalMs ?? DEFAULT_REPORT_POLL_INTERVAL_MS;
|
|
36
35
|
const reportTimeoutMs = deps.reportTimeoutMs ?? DEFAULT_REPORT_TIMEOUT_MS;
|
|
@@ -38,7 +37,7 @@ export function createCodexReviewService(deps) {
|
|
|
38
37
|
const projectConfig = await deps.projectService.loadConfig(repoRoot);
|
|
39
38
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
40
39
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
41
|
-
const reviewSettings = await deps.appSettings.
|
|
40
|
+
const reviewSettings = await deps.appSettings.getGateReviewSettings(repoRoot, taskSlug);
|
|
42
41
|
return {
|
|
43
42
|
repoRoot,
|
|
44
43
|
taskSlug,
|
|
@@ -59,7 +58,7 @@ export function createCodexReviewService(deps) {
|
|
|
59
58
|
error: undefined
|
|
60
59
|
}, now());
|
|
61
60
|
await saveIndex(deps.fs, context.taskRepoRoot, index);
|
|
62
|
-
return { status: "disabled", gate, record: index.gates[gate], message: "
|
|
61
|
+
return { status: "disabled", gate, record: index.gates[gate], message: "Gate review is disabled." };
|
|
63
62
|
}
|
|
64
63
|
if (!record.required) {
|
|
65
64
|
index = applyGateState(index, gate, {
|
|
@@ -75,11 +74,11 @@ export function createCodexReviewService(deps) {
|
|
|
75
74
|
status: "running",
|
|
76
75
|
gate,
|
|
77
76
|
record,
|
|
78
|
-
message: `
|
|
77
|
+
message: `Gate review is already running for ${index.activeGate}.`
|
|
79
78
|
};
|
|
80
79
|
}
|
|
81
80
|
if (record.status === "running" && !options.force) {
|
|
82
|
-
return { status: "running", gate, record, message: "
|
|
81
|
+
return { status: "running", gate, record, message: "Gate review is already running." };
|
|
83
82
|
}
|
|
84
83
|
const inputHash = await computeInputHash(deps, context.taskRepoRoot, gate);
|
|
85
84
|
if (!options.force
|
|
@@ -90,12 +89,13 @@ export function createCodexReviewService(deps) {
|
|
|
90
89
|
status: "already_approved",
|
|
91
90
|
gate,
|
|
92
91
|
record,
|
|
93
|
-
message: "
|
|
92
|
+
message: "Gate review already approved the current inputs."
|
|
94
93
|
};
|
|
95
94
|
}
|
|
96
95
|
const timestamp = now();
|
|
97
96
|
const requestId = createRequestId(gate);
|
|
98
97
|
const requestPath = path.posix.join(REQUESTS_DIR, `${requestId}.json`);
|
|
98
|
+
const promptPath = path.posix.join(REQUESTS_DIR, `${requestId}.prompt.md`);
|
|
99
99
|
const nextRecord = {
|
|
100
100
|
...record,
|
|
101
101
|
status: "running",
|
|
@@ -104,6 +104,7 @@ export function createCodexReviewService(deps) {
|
|
|
104
104
|
exceptionReason: undefined,
|
|
105
105
|
requestId,
|
|
106
106
|
requestPath,
|
|
107
|
+
promptPath,
|
|
107
108
|
inputHash,
|
|
108
109
|
requestedAt: timestamp,
|
|
109
110
|
startedAt: undefined,
|
|
@@ -122,7 +123,7 @@ export function createCodexReviewService(deps) {
|
|
|
122
123
|
updatedAt: timestamp
|
|
123
124
|
};
|
|
124
125
|
await deps.fs.writeJsonAtomic(resolveRepoPath(context.taskRepoRoot, requestPath), {
|
|
125
|
-
version:
|
|
126
|
+
version: GATE_REVIEW_VERSION,
|
|
126
127
|
requestId,
|
|
127
128
|
gate,
|
|
128
129
|
status: "requested",
|
|
@@ -132,23 +133,23 @@ export function createCodexReviewService(deps) {
|
|
|
132
133
|
promptPath: nextRecord.promptPath
|
|
133
134
|
});
|
|
134
135
|
await saveIndex(deps.fs, context.taskRepoRoot, index);
|
|
135
|
-
void
|
|
136
|
-
//
|
|
136
|
+
void runGateReview(context, gate, requestId).catch(() => {
|
|
137
|
+
// runGateReview records failures in the persisted gate state.
|
|
137
138
|
});
|
|
138
139
|
return {
|
|
139
140
|
status: "started",
|
|
140
141
|
gate,
|
|
141
142
|
record: nextRecord,
|
|
142
|
-
message: "
|
|
143
|
+
message: "Gate review started."
|
|
143
144
|
};
|
|
144
145
|
}
|
|
145
|
-
async function
|
|
146
|
+
async function runGateReview(context, gate, requestId) {
|
|
146
147
|
const runKey = `${context.taskRepoRoot}:${context.taskSlug}:${gate}`;
|
|
147
148
|
if (activeRuns.has(runKey)) {
|
|
148
149
|
return;
|
|
149
150
|
}
|
|
150
151
|
activeRuns.add(runKey);
|
|
151
|
-
let
|
|
152
|
+
let gateTurnStarted = false;
|
|
152
153
|
try {
|
|
153
154
|
const timestamp = now();
|
|
154
155
|
await updateGateRecord(context, gate, {
|
|
@@ -157,37 +158,39 @@ export function createCodexReviewService(deps) {
|
|
|
157
158
|
updatedAt: timestamp
|
|
158
159
|
});
|
|
159
160
|
await updateRequestStatus(deps.fs, context, requestId, "running", { startedAt: timestamp });
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
const prompt =
|
|
161
|
+
const reviewDir = resolveRepoPath(context.taskRepoRoot, GATE_REVIEW_DIR);
|
|
162
|
+
const agentPath = resolveRepoPath(context.repoRoot, GATE_REVIEW_AGENT_PATH);
|
|
163
|
+
const prompt = buildGatePrompt(context, gate, requestId);
|
|
163
164
|
await deps.fs.ensureDir(reviewDir);
|
|
164
|
-
|
|
165
|
+
await deps.fs.ensureDir(resolveRepoPath(context.taskRepoRoot, REQUESTS_DIR));
|
|
166
|
+
await deps.fs.writeText(resolveRepoPath(context.taskRepoRoot, promptPathForRequest(requestId)), prompt);
|
|
167
|
+
if (!(await deps.fs.pathExists(agentPath))) {
|
|
165
168
|
throw new VcmError({
|
|
166
|
-
code: "
|
|
167
|
-
message: `${
|
|
169
|
+
code: "GATE_REVIEW_AGENT_MISSING",
|
|
170
|
+
message: `${GATE_REVIEW_AGENT_PATH} does not exist.`,
|
|
168
171
|
statusCode: 409,
|
|
169
|
-
hint: "Apply the VCM harness before requesting
|
|
172
|
+
hint: "Apply the VCM harness before requesting Gate Review Gates."
|
|
170
173
|
});
|
|
171
174
|
}
|
|
172
|
-
const session = await
|
|
175
|
+
const session = await ensureGateReviewerSession(context);
|
|
173
176
|
await submitTerminalInput(deps.runtime, session.id, prompt);
|
|
174
|
-
await deps.sessionService.markRoleActivityRunning(context.repoRoot, context.taskSlug,
|
|
177
|
+
await deps.sessionService.markRoleActivityRunning(context.repoRoot, context.taskSlug, GATE_REVIEWER_ROLE);
|
|
175
178
|
await deps.roundService.recordRoleTurnEvent({
|
|
176
179
|
repoRoot: context.repoRoot,
|
|
177
180
|
stateRepoRoot: context.taskRepoRoot,
|
|
178
181
|
stateRoot: context.stateRoot,
|
|
179
182
|
taskSlug: context.taskSlug,
|
|
180
|
-
role:
|
|
183
|
+
role: GATE_REVIEWER_ROLE,
|
|
181
184
|
eventName: "UserPromptSubmit"
|
|
182
185
|
});
|
|
183
|
-
|
|
186
|
+
gateTurnStarted = true;
|
|
184
187
|
const parsed = await waitForGateReport(deps.fs, context.taskRepoRoot, gate, requestId, now(), {
|
|
185
188
|
intervalMs: reportPollIntervalMs,
|
|
186
189
|
timeoutMs: reportTimeoutMs
|
|
187
190
|
});
|
|
188
191
|
const completedAt = now();
|
|
189
|
-
await
|
|
190
|
-
|
|
192
|
+
await recordGateReviewerTurnStop(context, gateTurnStarted);
|
|
193
|
+
gateTurnStarted = false;
|
|
191
194
|
await updateGateRecord(context, gate, {
|
|
192
195
|
status: "completed",
|
|
193
196
|
decision: parsed.decision,
|
|
@@ -209,8 +212,8 @@ export function createCodexReviewService(deps) {
|
|
|
209
212
|
catch (error) {
|
|
210
213
|
const timestamp = now();
|
|
211
214
|
const message = errorMessage(error);
|
|
212
|
-
await
|
|
213
|
-
|
|
215
|
+
await recordGateReviewerTurnStop(context, gateTurnStarted);
|
|
216
|
+
gateTurnStarted = false;
|
|
214
217
|
await updateGateRecord(context, gate, {
|
|
215
218
|
status: "failed",
|
|
216
219
|
error: message,
|
|
@@ -229,38 +232,38 @@ export function createCodexReviewService(deps) {
|
|
|
229
232
|
activeRuns.delete(runKey);
|
|
230
233
|
}
|
|
231
234
|
}
|
|
232
|
-
async function
|
|
235
|
+
async function recordGateReviewerTurnStop(context, shouldRecord) {
|
|
233
236
|
if (!shouldRecord) {
|
|
234
237
|
return;
|
|
235
238
|
}
|
|
236
|
-
await deps.sessionService.markRoleActivityIdle(context.repoRoot, context.taskSlug,
|
|
239
|
+
await deps.sessionService.markRoleActivityIdle(context.repoRoot, context.taskSlug, GATE_REVIEWER_ROLE);
|
|
237
240
|
await deps.roundService.recordRoleTurnEvent({
|
|
238
241
|
repoRoot: context.repoRoot,
|
|
239
242
|
stateRepoRoot: context.taskRepoRoot,
|
|
240
243
|
stateRoot: context.stateRoot,
|
|
241
244
|
taskSlug: context.taskSlug,
|
|
242
|
-
role:
|
|
245
|
+
role: GATE_REVIEWER_ROLE,
|
|
243
246
|
eventName: "Stop"
|
|
244
247
|
});
|
|
245
248
|
}
|
|
246
|
-
async function
|
|
247
|
-
const existing = await deps.sessionService.getRoleSession(context.repoRoot, context.taskSlug,
|
|
249
|
+
async function ensureGateReviewerSession(context) {
|
|
250
|
+
const existing = await deps.sessionService.getRoleSession(context.repoRoot, context.taskSlug, GATE_REVIEWER_ROLE);
|
|
248
251
|
if (existing?.status === "running" && deps.runtime.getSession(existing.id)) {
|
|
249
252
|
return existing;
|
|
250
253
|
}
|
|
251
254
|
if (existing?.claudeSessionId) {
|
|
252
255
|
try {
|
|
253
|
-
return await deps.sessionService.resumeRoleSession(context.repoRoot, context.taskSlug,
|
|
256
|
+
return await deps.sessionService.resumeRoleSession(context.repoRoot, context.taskSlug, GATE_REVIEWER_ROLE, {
|
|
254
257
|
cols: 100,
|
|
255
258
|
rows: 28,
|
|
256
259
|
model: "default"
|
|
257
260
|
});
|
|
258
261
|
}
|
|
259
262
|
catch {
|
|
260
|
-
// Fall through to a fresh
|
|
263
|
+
// Fall through to a fresh Gate Reviewer terminal if the saved session cannot be resumed.
|
|
261
264
|
}
|
|
262
265
|
}
|
|
263
|
-
return deps.sessionService.startRoleSession(context.repoRoot, context.taskSlug,
|
|
266
|
+
return deps.sessionService.startRoleSession(context.repoRoot, context.taskSlug, GATE_REVIEWER_ROLE, {
|
|
264
267
|
cols: 100,
|
|
265
268
|
rows: 28,
|
|
266
269
|
model: "default"
|
|
@@ -321,10 +324,10 @@ export function createCodexReviewService(deps) {
|
|
|
321
324
|
return loadIndex(deps.fs, context, now());
|
|
322
325
|
},
|
|
323
326
|
async updateSettings(repoRoot, taskSlug, input) {
|
|
324
|
-
const currentSettings = await deps.appSettings.
|
|
327
|
+
const currentSettings = await deps.appSettings.getGateReviewSettings(repoRoot, taskSlug);
|
|
325
328
|
const requiredGates = new Set(currentSettings.requiredGates);
|
|
326
329
|
for (const [gate, enabled] of Object.entries(input?.gates ?? {})) {
|
|
327
|
-
if (!
|
|
330
|
+
if (!isGateReviewGate(gate)) {
|
|
328
331
|
continue;
|
|
329
332
|
}
|
|
330
333
|
if (enabled) {
|
|
@@ -334,7 +337,7 @@ export function createCodexReviewService(deps) {
|
|
|
334
337
|
requiredGates.delete(gate);
|
|
335
338
|
}
|
|
336
339
|
}
|
|
337
|
-
await deps.appSettings.
|
|
340
|
+
await deps.appSettings.updateGateReviewSettings(repoRoot, taskSlug, [...requiredGates]);
|
|
338
341
|
const nextContext = await getContext(repoRoot, taskSlug);
|
|
339
342
|
const index = await loadIndex(deps.fs, nextContext, now());
|
|
340
343
|
await saveIndex(deps.fs, nextContext.taskRepoRoot, {
|
|
@@ -355,10 +358,10 @@ export function createCodexReviewService(deps) {
|
|
|
355
358
|
const current = await loadIndex(deps.fs, context, now());
|
|
356
359
|
if (current.gates[gate].status === "running") {
|
|
357
360
|
throw new VcmError({
|
|
358
|
-
code: "
|
|
359
|
-
message: "Cannot skip a running
|
|
361
|
+
code: "GATE_REVIEW_RUNNING",
|
|
362
|
+
message: "Cannot skip a running Gate review gate.",
|
|
360
363
|
statusCode: 409,
|
|
361
|
-
hint: "Wait for the
|
|
364
|
+
hint: "Wait for the gate review to finish, then choose retry, skip, or override."
|
|
362
365
|
});
|
|
363
366
|
}
|
|
364
367
|
const index = await updateGateRecord(context, gate, {
|
|
@@ -380,10 +383,10 @@ export function createCodexReviewService(deps) {
|
|
|
380
383
|
const current = await loadIndex(deps.fs, context, now());
|
|
381
384
|
if (current.gates[gate].status === "running") {
|
|
382
385
|
throw new VcmError({
|
|
383
|
-
code: "
|
|
384
|
-
message: "Cannot override a running
|
|
386
|
+
code: "GATE_REVIEW_RUNNING",
|
|
387
|
+
message: "Cannot override a running Gate review gate.",
|
|
385
388
|
statusCode: 409,
|
|
386
|
-
hint: "Wait for the
|
|
389
|
+
hint: "Wait for the gate review to finish, then choose retry, skip, or override."
|
|
387
390
|
});
|
|
388
391
|
}
|
|
389
392
|
const index = await updateGateRecord(context, gate, {
|
|
@@ -405,8 +408,8 @@ export function createCodexReviewService(deps) {
|
|
|
405
408
|
}
|
|
406
409
|
};
|
|
407
410
|
}
|
|
408
|
-
export function
|
|
409
|
-
return
|
|
411
|
+
export function isGateReviewGate(value) {
|
|
412
|
+
return GATE_REVIEW_GATES.includes(value);
|
|
410
413
|
}
|
|
411
414
|
function loadRuntimeConfig(reviewSettings) {
|
|
412
415
|
return {
|
|
@@ -425,7 +428,7 @@ function normalizeIndex(raw, config, timestamp) {
|
|
|
425
428
|
: {};
|
|
426
429
|
const gates = {};
|
|
427
430
|
const requiredSet = new Set(config.enabled ? config.requiredGates : []);
|
|
428
|
-
for (const gate of
|
|
431
|
+
for (const gate of GATE_REVIEW_GATES) {
|
|
429
432
|
const existing = existingGates[gate];
|
|
430
433
|
const required = requiredSet.has(gate);
|
|
431
434
|
const fallbackStatus = config.enabled
|
|
@@ -457,11 +460,11 @@ function normalizeIndex(raw, config, timestamp) {
|
|
|
457
460
|
callbackError: typeof existing?.callbackError === "string" ? existing.callbackError : undefined
|
|
458
461
|
};
|
|
459
462
|
}
|
|
460
|
-
const activeGate =
|
|
463
|
+
const activeGate = isGateReviewGate(String(raw?.activeGate)) && gates[raw?.activeGate].status === "running"
|
|
461
464
|
? raw?.activeGate
|
|
462
465
|
: null;
|
|
463
466
|
return {
|
|
464
|
-
version:
|
|
467
|
+
version: GATE_REVIEW_VERSION,
|
|
465
468
|
enabled: config.enabled,
|
|
466
469
|
activeGate,
|
|
467
470
|
gates,
|
|
@@ -494,11 +497,9 @@ async function computeInputHash(deps, taskRepoRoot, gate) {
|
|
|
494
497
|
const digest = createHash("sha256");
|
|
495
498
|
const common = [
|
|
496
499
|
"CLAUDE.md",
|
|
497
|
-
".
|
|
498
|
-
".
|
|
499
|
-
".ai/
|
|
500
|
-
".ai/codex/.codex/hooks.json",
|
|
501
|
-
promptPathForGate(gate)
|
|
500
|
+
".claude/agents/gate-reviewer.md",
|
|
501
|
+
".claude/skills/vcm-gate-review/SKILL.md",
|
|
502
|
+
".ai/tools/request-gate-review"
|
|
502
503
|
];
|
|
503
504
|
for (const relativePath of [...common, ...SOURCE_ARTIFACTS[gate]]) {
|
|
504
505
|
digest.update(relativePath);
|
|
@@ -521,35 +522,31 @@ async function commandStdout(runner, cwd, args) {
|
|
|
521
522
|
const result = await runner.run("git", args, { cwd });
|
|
522
523
|
return result.exitCode === 0 ? result.stdout : "";
|
|
523
524
|
}
|
|
524
|
-
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
- Gate: ${gate}
|
|
541
|
-
- Request: ${requestId}
|
|
542
|
-
- Report path from this working directory: ${reportPath}
|
|
525
|
+
function buildGatePrompt(context, gate, requestId) {
|
|
526
|
+
const reportPath = reportPathForGate(gate);
|
|
527
|
+
const absoluteReportPath = resolveRepoPath(context.taskRepoRoot, reportPath);
|
|
528
|
+
const evidence = SOURCE_ARTIFACTS[gate]
|
|
529
|
+
.map((relativePath) => `- ${relativePath}`)
|
|
530
|
+
.join("\n");
|
|
531
|
+
const gitLine = gate === "architecture-plan" || gate === "final-diff"
|
|
532
|
+
? "\nDiff: inspect git status/diff in Worktree."
|
|
533
|
+
: "";
|
|
534
|
+
return `[VCM GATE REVIEW]
|
|
535
|
+
Task: ${context.taskSlug}
|
|
536
|
+
Worktree: ${context.taskRepoRoot}
|
|
537
|
+
Gate: ${gate}
|
|
538
|
+
Request: ${requestId}
|
|
539
|
+
Report: ${absoluteReportPath}
|
|
543
540
|
|
|
544
|
-
|
|
541
|
+
Evidence:
|
|
542
|
+
${evidence}${gitLine}
|
|
545
543
|
|
|
546
|
-
|
|
544
|
+
Write only Report. Start exactly:
|
|
547
545
|
Gate: ${gate}
|
|
548
546
|
Request: ${requestId}
|
|
549
547
|
Decision: approve|request_changes
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
Write only that report file. Do not edit any other file.`;
|
|
548
|
+
Summary: <one or two sentences>
|
|
549
|
+
[/VCM GATE REVIEW]`;
|
|
553
550
|
}
|
|
554
551
|
async function waitForGateReport(fs, taskRepoRoot, gate, requestId, timestamp, options) {
|
|
555
552
|
const startedAt = Date.now();
|
|
@@ -568,8 +565,8 @@ async function waitForGateReport(fs, taskRepoRoot, gate, requestId, timestamp, o
|
|
|
568
565
|
}
|
|
569
566
|
const detail = errorMessage(lastError);
|
|
570
567
|
throw new VcmError({
|
|
571
|
-
code: "
|
|
572
|
-
message: `
|
|
568
|
+
code: "GATE_REVIEW_REPORT_TIMEOUT",
|
|
569
|
+
message: `Gate Reviewer did not produce a valid ${gate} report within ${Math.round(options.timeoutMs / 1000)}s.`,
|
|
573
570
|
statusCode: 504,
|
|
574
571
|
hint: detail
|
|
575
572
|
});
|
|
@@ -579,8 +576,8 @@ async function parseGateReport(fs, taskRepoRoot, gate, requestId, timestamp) {
|
|
|
579
576
|
const absolutePath = resolveRepoPath(taskRepoRoot, reportPath);
|
|
580
577
|
if (!(await fs.pathExists(absolutePath))) {
|
|
581
578
|
throw new VcmError({
|
|
582
|
-
code: "
|
|
583
|
-
message: `
|
|
579
|
+
code: "GATE_REVIEW_REPORT_MISSING",
|
|
580
|
+
message: `Gate review report was not written: ${reportPath}`,
|
|
584
581
|
statusCode: 500
|
|
585
582
|
});
|
|
586
583
|
}
|
|
@@ -588,24 +585,24 @@ async function parseGateReport(fs, taskRepoRoot, gate, requestId, timestamp) {
|
|
|
588
585
|
const parsedGate = matchField(content, "Gate");
|
|
589
586
|
if (parsedGate && parsedGate !== gate) {
|
|
590
587
|
throw new VcmError({
|
|
591
|
-
code: "
|
|
592
|
-
message: `
|
|
588
|
+
code: "GATE_REVIEW_REPORT_GATE_MISMATCH",
|
|
589
|
+
message: `Gate review report gate is ${parsedGate}, expected ${gate}.`,
|
|
593
590
|
statusCode: 500
|
|
594
591
|
});
|
|
595
592
|
}
|
|
596
593
|
const parsedRequest = matchField(content, "Request");
|
|
597
594
|
if (requestId && parsedRequest !== requestId) {
|
|
598
595
|
throw new VcmError({
|
|
599
|
-
code: "
|
|
600
|
-
message: `
|
|
596
|
+
code: "GATE_REVIEW_REPORT_STALE",
|
|
597
|
+
message: `Gate review report request is ${parsedRequest ?? "missing"}, expected ${requestId}.`,
|
|
601
598
|
statusCode: 500
|
|
602
599
|
});
|
|
603
600
|
}
|
|
604
601
|
const decision = normalizeDecision(matchField(content, "Decision"));
|
|
605
602
|
if (!decision) {
|
|
606
603
|
throw new VcmError({
|
|
607
|
-
code: "
|
|
608
|
-
message: `
|
|
604
|
+
code: "GATE_REVIEW_DECISION_MISSING",
|
|
605
|
+
message: `Gate review report must contain Decision: approve or Decision: request_changes.`,
|
|
609
606
|
statusCode: 500
|
|
610
607
|
});
|
|
611
608
|
}
|
|
@@ -623,7 +620,7 @@ async function parseGateReport(fs, taskRepoRoot, gate, requestId, timestamp) {
|
|
|
623
620
|
async function updateRequestStatus(fs, context, requestId, status, patch) {
|
|
624
621
|
const requestPath = resolveRepoPath(context.taskRepoRoot, path.posix.join(REQUESTS_DIR, `${requestId}.json`));
|
|
625
622
|
const current = await readJsonOrNull(fs, requestPath) ?? {
|
|
626
|
-
version:
|
|
623
|
+
version: GATE_REVIEW_VERSION,
|
|
627
624
|
requestId
|
|
628
625
|
};
|
|
629
626
|
await fs.writeJsonAtomic(requestPath, {
|
|
@@ -634,13 +631,16 @@ async function updateRequestStatus(fs, context, requestId, status, patch) {
|
|
|
634
631
|
});
|
|
635
632
|
}
|
|
636
633
|
function reportPathForGate(gate) {
|
|
637
|
-
return path.posix.join(
|
|
634
|
+
return path.posix.join(GATE_REVIEW_DIR, `${gate}-review.md`);
|
|
635
|
+
}
|
|
636
|
+
function promptPathForRequest(requestId) {
|
|
637
|
+
return path.posix.join(REQUESTS_DIR, `${requestId}.prompt.md`);
|
|
638
638
|
}
|
|
639
639
|
function promptPathForGate(gate) {
|
|
640
|
-
return path.posix.join(
|
|
640
|
+
return path.posix.join(GATE_REVIEW_DIR, "prompts", `${gate}-gate.md`);
|
|
641
641
|
}
|
|
642
642
|
function getIndexPath(taskRepoRoot) {
|
|
643
|
-
return resolveRepoPath(taskRepoRoot, path.posix.join(
|
|
643
|
+
return resolveRepoPath(taskRepoRoot, path.posix.join(GATE_REVIEW_DIR, "index.json"));
|
|
644
644
|
}
|
|
645
645
|
function createRequestId(gate) {
|
|
646
646
|
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
@@ -736,7 +736,7 @@ function parsePositiveInteger(value) {
|
|
|
736
736
|
function assertExceptionReason(reason) {
|
|
737
737
|
if (!reason?.trim()) {
|
|
738
738
|
throw new VcmError({
|
|
739
|
-
code: "
|
|
739
|
+
code: "GATE_REVIEW_REASON_REQUIRED",
|
|
740
740
|
message: "A reason is required.",
|
|
741
741
|
statusCode: 400
|
|
742
742
|
});
|
|
@@ -744,7 +744,7 @@ function assertExceptionReason(reason) {
|
|
|
744
744
|
}
|
|
745
745
|
function renderProjectManagerCallback(input) {
|
|
746
746
|
const lines = [
|
|
747
|
-
"[VCM
|
|
747
|
+
"[VCM GATE REVIEW CALLBACK]",
|
|
748
748
|
`task: ${input.taskSlug}`,
|
|
749
749
|
`gate: ${input.gate}`,
|
|
750
750
|
`status: ${input.status}`,
|
|
@@ -752,11 +752,11 @@ function renderProjectManagerCallback(input) {
|
|
|
752
752
|
`report: ${input.reportPath}`,
|
|
753
753
|
...(input.error ? [`error: ${input.error}`] : []),
|
|
754
754
|
"",
|
|
755
|
-
"Use the vcm-
|
|
755
|
+
"Use the vcm-gate-review skill to handle this callback.",
|
|
756
756
|
"If status is completed and decision is approve, continue the VCM flow.",
|
|
757
757
|
"If decision is request_changes, analyze the report and route follow-up through the normal VCM roles.",
|
|
758
758
|
"If status is failed, stop and ask the user to retry, skip, or override in VCM.",
|
|
759
|
-
"[/VCM
|
|
759
|
+
"[/VCM GATE REVIEW CALLBACK]"
|
|
760
760
|
];
|
|
761
761
|
return lines.join("\n");
|
|
762
762
|
}
|
|
@@ -767,14 +767,14 @@ function errorMessage(error) {
|
|
|
767
767
|
if (error instanceof Error) {
|
|
768
768
|
return error.message;
|
|
769
769
|
}
|
|
770
|
-
return "Unknown
|
|
770
|
+
return "Unknown Gate review error.";
|
|
771
771
|
}
|
|
772
772
|
function isPendingReportError(error) {
|
|
773
773
|
return error instanceof VcmError && [
|
|
774
|
-
"
|
|
775
|
-
"
|
|
776
|
-
"
|
|
777
|
-
"
|
|
774
|
+
"GATE_REVIEW_DECISION_MISSING",
|
|
775
|
+
"GATE_REVIEW_REPORT_GATE_MISMATCH",
|
|
776
|
+
"GATE_REVIEW_REPORT_MISSING",
|
|
777
|
+
"GATE_REVIEW_REPORT_STALE"
|
|
778
778
|
].includes(error.code);
|
|
779
779
|
}
|
|
780
780
|
function delay(ms) {
|