vibe-coding-master 0.3.1 → 0.3.3
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 +1 -1
- package/dist/backend/adapters/claude-adapter.js +4 -1
- package/dist/backend/api/round-routes.js +1 -0
- package/dist/backend/server.js +10 -7
- package/dist/backend/services/app-settings-service.js +56 -1
- package/dist/backend/services/codex-review-service.js +46 -108
- package/dist/backend/services/round-service.js +78 -3
- package/dist/backend/services/session-service.js +43 -20
- package/dist/backend/templates/handoff.js +10 -0
- package/dist/backend/templates/harness/architect-agent.js +8 -4
- package/dist/backend/templates/harness/claude-root.js +1 -1
- package/dist/backend/templates/harness/coder-agent.js +1 -1
- package/dist/backend/templates/harness/codex-review.js +11 -30
- package/dist/backend/templates/harness/reviewer-agent.js +1 -0
- package/dist/shared/types/session.js +10 -1
- package/dist/shared/validation/artifact-check.js +1 -0
- package/dist-frontend/assets/{index-BavJjWQY.js → index-OPkFuHRf.js} +29 -29
- package/dist-frontend/index.html +1 -1
- package/docs/cc-best-practices.md +1 -0
- package/docs/codex-file-translation-plan.md +618 -0
- package/docs/codex-review-gates.md +35 -16
- package/docs/full-harness-baseline.md +1 -1
- package/docs/product-design.md +2 -1
- package/docs/vcm-cc-best-practices.md +15 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -163,7 +163,7 @@ The recommended flow is:
|
|
|
163
163
|
|
|
164
164
|
```text
|
|
165
165
|
project-manager
|
|
166
|
-
-> architect architecture plan and code scaffolding
|
|
166
|
+
-> architect architecture plan, Scaffold Manifest, and code scaffolding
|
|
167
167
|
-> coder implementation and baseline unit checks
|
|
168
168
|
-> reviewer independent validation
|
|
169
169
|
-> architect docs sync / architecture drift check
|
|
@@ -23,7 +23,10 @@ export function createClaudeAdapter(runner) {
|
|
|
23
23
|
args.push(resume ? "--resume" : "--session-id", claudeSessionId);
|
|
24
24
|
}
|
|
25
25
|
args.push("--model", model);
|
|
26
|
-
if (effort
|
|
26
|
+
if (effort === "ultracode") {
|
|
27
|
+
args.push("--settings", JSON.stringify({ ultracode: true }));
|
|
28
|
+
}
|
|
29
|
+
else if (effort !== "default") {
|
|
27
30
|
args.push("--effort", effort);
|
|
28
31
|
}
|
|
29
32
|
if (permissionMode === "bypassPermissions") {
|
|
@@ -7,6 +7,7 @@ export function registerRoundRoutes(app, deps) {
|
|
|
7
7
|
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
8
8
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
9
9
|
return deps.roundService.getSessionRoundState({
|
|
10
|
+
repoRoot: project.repoRoot,
|
|
10
11
|
stateRepoRoot: taskRepoRoot,
|
|
11
12
|
stateRoot: config.stateRoot,
|
|
12
13
|
taskSlug: request.params.taskSlug
|
package/dist/backend/server.js
CHANGED
|
@@ -184,19 +184,22 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
184
184
|
sessionService,
|
|
185
185
|
taskService
|
|
186
186
|
});
|
|
187
|
+
const roundService = createRoundService({
|
|
188
|
+
fs,
|
|
189
|
+
sessionService,
|
|
190
|
+
onSessionStatusChange: async ({ repoRoot, taskSlug, status }) => {
|
|
191
|
+
await taskService.updateTaskStatus(repoRoot, taskSlug, status);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
187
194
|
const codexReviewService = createCodexReviewService({
|
|
188
195
|
fs,
|
|
189
196
|
runner,
|
|
190
197
|
runtime,
|
|
191
198
|
projectService,
|
|
192
199
|
taskService,
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
fs,
|
|
197
|
-
onSessionStatusChange: async ({ repoRoot, taskSlug, status }) => {
|
|
198
|
-
await taskService.updateTaskStatus(repoRoot, taskSlug, status);
|
|
199
|
-
}
|
|
200
|
+
appSettings,
|
|
201
|
+
sessionService,
|
|
202
|
+
roundService
|
|
200
203
|
});
|
|
201
204
|
const transcripts = createClaudeTranscriptService();
|
|
202
205
|
const translationService = createTranslationService({
|
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { VCM_ROLE_NAMES } from "../../shared/constants.js";
|
|
5
|
+
import { CODEX_REVIEW_GATES } from "../../shared/types/codex-review.js";
|
|
5
6
|
import { createDefaultLaunchTemplate } from "../../shared/types/app-settings.js";
|
|
6
7
|
import { CLAUDE_MODEL_OPTIONS, SESSION_EFFORT_OPTIONS } from "../../shared/types/session.js";
|
|
7
8
|
const MAX_RECENT_REPOSITORIES = 5;
|
|
@@ -129,6 +130,32 @@ export function createAppSettingsService(deps) {
|
|
|
129
130
|
});
|
|
130
131
|
return config;
|
|
131
132
|
},
|
|
133
|
+
async getCodexReviewSettings() {
|
|
134
|
+
const settings = await loadSettings();
|
|
135
|
+
const requiredGates = normalizeCodexReviewGates(settings.codexReview?.requiredGates);
|
|
136
|
+
return {
|
|
137
|
+
enabled: requiredGates.length > 0,
|
|
138
|
+
requiredGates
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
async updateCodexReviewSettings(_repoRoot, _taskSlug, requiredGates) {
|
|
142
|
+
const current = await loadSettings();
|
|
143
|
+
const normalizedRequiredGates = normalizeCodexReviewGates(requiredGates);
|
|
144
|
+
const timestamp = new Date().toISOString();
|
|
145
|
+
const nextCodexReview = {
|
|
146
|
+
version: 1,
|
|
147
|
+
requiredGates: normalizedRequiredGates,
|
|
148
|
+
updatedAt: timestamp
|
|
149
|
+
};
|
|
150
|
+
await saveSettings({
|
|
151
|
+
...current,
|
|
152
|
+
codexReview: normalizeCodexReviewSettingsState(nextCodexReview)
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
enabled: normalizedRequiredGates.length > 0,
|
|
156
|
+
requiredGates: normalizedRequiredGates
|
|
157
|
+
};
|
|
158
|
+
},
|
|
132
159
|
getSettingsPath() {
|
|
133
160
|
return settingsPath;
|
|
134
161
|
},
|
|
@@ -172,13 +199,41 @@ function normalizeProjectIndexFile(input) {
|
|
|
172
199
|
projects
|
|
173
200
|
};
|
|
174
201
|
}
|
|
175
|
-
function
|
|
202
|
+
function normalizeCodexReviewSettingsState(input) {
|
|
203
|
+
if (!isObject(input)) {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
176
206
|
return {
|
|
207
|
+
version: 1,
|
|
208
|
+
requiredGates: normalizeCodexReviewGates(input.requiredGates),
|
|
209
|
+
updatedAt: typeof input.updatedAt === "string" ? input.updatedAt : new Date(0).toISOString()
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function normalizeCodexReviewGates(input) {
|
|
213
|
+
if (!Array.isArray(input)) {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
const gates = [];
|
|
217
|
+
for (const value of input) {
|
|
218
|
+
if (!CODEX_REVIEW_GATES.includes(value) || gates.includes(value)) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
gates.push(value);
|
|
222
|
+
}
|
|
223
|
+
return CODEX_REVIEW_GATES.filter((gate) => gates.includes(gate));
|
|
224
|
+
}
|
|
225
|
+
function normalizeSettingsFile(input) {
|
|
226
|
+
const settings = {
|
|
177
227
|
version: 1,
|
|
178
228
|
preferences: normalizePreferences(input.preferences),
|
|
179
229
|
translation: normalizeTranslationConfig(input.translation),
|
|
180
230
|
recentRepositoryPaths: normalizeRecentRepositoryPaths(input.recentRepositoryPaths)
|
|
181
231
|
};
|
|
232
|
+
const codexReview = normalizeCodexReviewSettingsState(input.codexReview);
|
|
233
|
+
if (codexReview) {
|
|
234
|
+
settings.codexReview = codexReview;
|
|
235
|
+
}
|
|
236
|
+
return settings;
|
|
182
237
|
}
|
|
183
238
|
function normalizePreferences(input) {
|
|
184
239
|
const candidate = isObject(input) ? input : {};
|
|
@@ -4,7 +4,6 @@ import { CODEX_REVIEW_GATES } from "../../shared/types/codex-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
|
-
import { renderCodexConfigHarnessRules } from "../templates/harness/codex-review.js";
|
|
8
7
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
9
8
|
const CODEX_DIR = ".ai/codex";
|
|
10
9
|
const CODEX_CONFIG_PATH = ".ai/codex/config.toml";
|
|
@@ -39,12 +38,13 @@ export function createCodexReviewService(deps) {
|
|
|
39
38
|
const projectConfig = await deps.projectService.loadConfig(repoRoot);
|
|
40
39
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
41
40
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
41
|
+
const reviewSettings = await deps.appSettings.getCodexReviewSettings(repoRoot, taskSlug);
|
|
42
42
|
return {
|
|
43
43
|
repoRoot,
|
|
44
44
|
taskSlug,
|
|
45
45
|
taskRepoRoot,
|
|
46
46
|
stateRoot: projectConfig.stateRoot,
|
|
47
|
-
config:
|
|
47
|
+
config: loadRuntimeConfig(reviewSettings)
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
async function requestReviewGateInternal(repoRoot, taskSlug, gate, options = {}) {
|
|
@@ -148,6 +148,7 @@ export function createCodexReviewService(deps) {
|
|
|
148
148
|
return;
|
|
149
149
|
}
|
|
150
150
|
activeRuns.add(runKey);
|
|
151
|
+
let codexTurnStarted = false;
|
|
151
152
|
try {
|
|
152
153
|
const timestamp = now();
|
|
153
154
|
await updateGateRecord(context, gate, {
|
|
@@ -169,13 +170,24 @@ export function createCodexReviewService(deps) {
|
|
|
169
170
|
});
|
|
170
171
|
}
|
|
171
172
|
const session = await ensureCodexReviewerSession(context);
|
|
172
|
-
await deps.sessionService.markRoleActivityRunning(context.repoRoot, context.taskSlug, CODEX_REVIEWER_ROLE);
|
|
173
173
|
await submitTerminalInput(deps.runtime, session.id, prompt);
|
|
174
|
+
await deps.sessionService.markRoleActivityRunning(context.repoRoot, context.taskSlug, CODEX_REVIEWER_ROLE);
|
|
175
|
+
await deps.roundService.recordRoleTurnEvent({
|
|
176
|
+
repoRoot: context.repoRoot,
|
|
177
|
+
stateRepoRoot: context.taskRepoRoot,
|
|
178
|
+
stateRoot: context.stateRoot,
|
|
179
|
+
taskSlug: context.taskSlug,
|
|
180
|
+
role: CODEX_REVIEWER_ROLE,
|
|
181
|
+
eventName: "UserPromptSubmit"
|
|
182
|
+
});
|
|
183
|
+
codexTurnStarted = true;
|
|
174
184
|
const parsed = await waitForGateReport(deps.fs, context.taskRepoRoot, gate, requestId, now(), {
|
|
175
185
|
intervalMs: reportPollIntervalMs,
|
|
176
186
|
timeoutMs: reportTimeoutMs
|
|
177
187
|
});
|
|
178
188
|
const completedAt = now();
|
|
189
|
+
await recordCodexReviewerTurnStop(context, codexTurnStarted);
|
|
190
|
+
codexTurnStarted = false;
|
|
179
191
|
await updateGateRecord(context, gate, {
|
|
180
192
|
status: "completed",
|
|
181
193
|
decision: parsed.decision,
|
|
@@ -197,6 +209,8 @@ export function createCodexReviewService(deps) {
|
|
|
197
209
|
catch (error) {
|
|
198
210
|
const timestamp = now();
|
|
199
211
|
const message = errorMessage(error);
|
|
212
|
+
await recordCodexReviewerTurnStop(context, codexTurnStarted);
|
|
213
|
+
codexTurnStarted = false;
|
|
200
214
|
await updateGateRecord(context, gate, {
|
|
201
215
|
status: "failed",
|
|
202
216
|
error: message,
|
|
@@ -215,6 +229,20 @@ export function createCodexReviewService(deps) {
|
|
|
215
229
|
activeRuns.delete(runKey);
|
|
216
230
|
}
|
|
217
231
|
}
|
|
232
|
+
async function recordCodexReviewerTurnStop(context, shouldRecord) {
|
|
233
|
+
if (!shouldRecord) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
await deps.sessionService.markRoleActivityIdle(context.repoRoot, context.taskSlug, CODEX_REVIEWER_ROLE);
|
|
237
|
+
await deps.roundService.recordRoleTurnEvent({
|
|
238
|
+
repoRoot: context.repoRoot,
|
|
239
|
+
stateRepoRoot: context.taskRepoRoot,
|
|
240
|
+
stateRoot: context.stateRoot,
|
|
241
|
+
taskSlug: context.taskSlug,
|
|
242
|
+
role: CODEX_REVIEWER_ROLE,
|
|
243
|
+
eventName: "Stop"
|
|
244
|
+
});
|
|
245
|
+
}
|
|
218
246
|
async function ensureCodexReviewerSession(context) {
|
|
219
247
|
const existing = await deps.sessionService.getRoleSession(context.repoRoot, context.taskSlug, CODEX_REVIEWER_ROLE);
|
|
220
248
|
if (existing?.status === "running" && deps.runtime.getSession(existing.id)) {
|
|
@@ -265,6 +293,14 @@ export function createCodexReviewService(deps) {
|
|
|
265
293
|
try {
|
|
266
294
|
await submitTerminalInput(deps.runtime, session.id, prompt);
|
|
267
295
|
await deps.sessionService.markRoleActivityRunning(context.repoRoot, context.taskSlug, "project-manager");
|
|
296
|
+
await deps.roundService.recordRoleTurnEvent({
|
|
297
|
+
repoRoot: context.repoRoot,
|
|
298
|
+
stateRepoRoot: context.taskRepoRoot,
|
|
299
|
+
stateRoot: context.stateRoot,
|
|
300
|
+
taskSlug: context.taskSlug,
|
|
301
|
+
role: "project-manager",
|
|
302
|
+
eventName: "UserPromptSubmit"
|
|
303
|
+
});
|
|
268
304
|
await updateGateRecord(context, gate, {
|
|
269
305
|
callbackStatus: "sent",
|
|
270
306
|
callbackError: undefined,
|
|
@@ -285,8 +321,8 @@ export function createCodexReviewService(deps) {
|
|
|
285
321
|
return loadIndex(deps.fs, context, now());
|
|
286
322
|
},
|
|
287
323
|
async updateSettings(repoRoot, taskSlug, input) {
|
|
288
|
-
const
|
|
289
|
-
const requiredGates = new Set(
|
|
324
|
+
const currentSettings = await deps.appSettings.getCodexReviewSettings(repoRoot, taskSlug);
|
|
325
|
+
const requiredGates = new Set(currentSettings.requiredGates);
|
|
290
326
|
for (const [gate, enabled] of Object.entries(input?.gates ?? {})) {
|
|
291
327
|
if (!isCodexReviewGate(gate)) {
|
|
292
328
|
continue;
|
|
@@ -298,7 +334,7 @@ export function createCodexReviewService(deps) {
|
|
|
298
334
|
requiredGates.delete(gate);
|
|
299
335
|
}
|
|
300
336
|
}
|
|
301
|
-
await
|
|
337
|
+
await deps.appSettings.updateCodexReviewSettings(repoRoot, taskSlug, [...requiredGates]);
|
|
302
338
|
const nextContext = await getContext(repoRoot, taskSlug);
|
|
303
339
|
const index = await loadIndex(deps.fs, nextContext, now());
|
|
304
340
|
await saveIndex(deps.fs, nextContext.taskRepoRoot, {
|
|
@@ -372,75 +408,12 @@ export function createCodexReviewService(deps) {
|
|
|
372
408
|
export function isCodexReviewGate(value) {
|
|
373
409
|
return CODEX_REVIEW_GATES.includes(value);
|
|
374
410
|
}
|
|
375
|
-
|
|
376
|
-
const configPath = resolveRepoPath(taskRepoRoot, CODEX_CONFIG_PATH);
|
|
377
|
-
if (!(await fs.pathExists(configPath))) {
|
|
378
|
-
return {
|
|
379
|
-
enabled: false,
|
|
380
|
-
requiredGates: [],
|
|
381
|
-
command: "codex"
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
const content = await fs.readText(configPath);
|
|
385
|
-
const section = extractTomlSection(content, "vcm.codex_review");
|
|
386
|
-
const enabled = parseTomlBoolean(section, "enabled") ?? false;
|
|
387
|
-
const parsedGates = parseTomlStringArray(section, "required_gates").filter(isCodexReviewGate);
|
|
411
|
+
function loadRuntimeConfig(reviewSettings) {
|
|
388
412
|
return {
|
|
389
|
-
enabled,
|
|
390
|
-
requiredGates:
|
|
391
|
-
model: parseTomlString(content, "model"),
|
|
392
|
-
modelReasoningEffort: parseTomlString(content, "model_reasoning_effort"),
|
|
393
|
-
command: parseTomlString(section, "command") ?? "codex"
|
|
413
|
+
enabled: reviewSettings.enabled,
|
|
414
|
+
requiredGates: reviewSettings.requiredGates
|
|
394
415
|
};
|
|
395
416
|
}
|
|
396
|
-
async function writeRuntimeConfigSettings(fs, taskRepoRoot, requiredGates) {
|
|
397
|
-
const configPath = resolveRepoPath(taskRepoRoot, CODEX_CONFIG_PATH);
|
|
398
|
-
const current = await fs.pathExists(configPath)
|
|
399
|
-
? await fs.readText(configPath)
|
|
400
|
-
: renderCodexConfigHarnessRules();
|
|
401
|
-
const section = renderCodexReviewSettingsSection(requiredGates);
|
|
402
|
-
const next = replaceTomlSection(current, "vcm.codex_review", section);
|
|
403
|
-
await fs.writeText(configPath, next);
|
|
404
|
-
}
|
|
405
|
-
function renderCodexReviewSettingsSection(requiredGates) {
|
|
406
|
-
if (requiredGates.length === 0) {
|
|
407
|
-
return [
|
|
408
|
-
"[vcm.codex_review]",
|
|
409
|
-
"enabled = false",
|
|
410
|
-
"required_gates = []"
|
|
411
|
-
].join("\n");
|
|
412
|
-
}
|
|
413
|
-
const lines = [
|
|
414
|
-
"[vcm.codex_review]",
|
|
415
|
-
"enabled = true",
|
|
416
|
-
"required_gates = [",
|
|
417
|
-
...CODEX_REVIEW_GATES
|
|
418
|
-
.filter((gate) => requiredGates.includes(gate))
|
|
419
|
-
.map((gate) => ` "${gate}",`),
|
|
420
|
-
"]"
|
|
421
|
-
];
|
|
422
|
-
return lines.join("\n");
|
|
423
|
-
}
|
|
424
|
-
function replaceTomlSection(content, sectionName, nextSection) {
|
|
425
|
-
const lines = content.replace(/\s+$/g, "").split(/\r?\n/);
|
|
426
|
-
const header = `[${sectionName}]`;
|
|
427
|
-
const start = lines.findIndex((line) => line.trim() === header);
|
|
428
|
-
if (start < 0) {
|
|
429
|
-
return `${lines.join("\n").trimEnd()}\n\n${nextSection}\n`;
|
|
430
|
-
}
|
|
431
|
-
let end = lines.length;
|
|
432
|
-
for (let index = start + 1; index < lines.length; index += 1) {
|
|
433
|
-
if (/^\s*\[[^\]]+\]\s*$/.test(lines[index])) {
|
|
434
|
-
end = index;
|
|
435
|
-
break;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
return [
|
|
439
|
-
...lines.slice(0, start),
|
|
440
|
-
nextSection,
|
|
441
|
-
...lines.slice(end)
|
|
442
|
-
].join("\n").trimEnd() + "\n";
|
|
443
|
-
}
|
|
444
417
|
async function loadIndex(fs, context, timestamp) {
|
|
445
418
|
const indexPath = getIndexPath(context.taskRepoRoot);
|
|
446
419
|
const raw = await readJsonOrNull(fs, indexPath);
|
|
@@ -537,7 +510,7 @@ async function computeInputHash(deps, taskRepoRoot, gate) {
|
|
|
537
510
|
digest.update("<missing>");
|
|
538
511
|
}
|
|
539
512
|
}
|
|
540
|
-
if (gate === "final-diff") {
|
|
513
|
+
if (gate === "architecture-plan" || gate === "final-diff") {
|
|
541
514
|
digest.update(await commandStdout(deps.runner, taskRepoRoot, ["status", "--porcelain=v1"]));
|
|
542
515
|
digest.update(await commandStdout(deps.runner, taskRepoRoot, ["diff", "--binary"]));
|
|
543
516
|
digest.update(await commandStdout(deps.runner, taskRepoRoot, ["diff", "--cached", "--binary"]));
|
|
@@ -684,41 +657,6 @@ async function readJsonOrNull(fs, targetPath) {
|
|
|
684
657
|
return null;
|
|
685
658
|
}
|
|
686
659
|
}
|
|
687
|
-
function extractTomlSection(content, sectionName) {
|
|
688
|
-
const lines = content.split(/\r?\n/);
|
|
689
|
-
const sectionHeader = `[${sectionName}]`;
|
|
690
|
-
const collected = [];
|
|
691
|
-
let inSection = false;
|
|
692
|
-
for (const line of lines) {
|
|
693
|
-
const trimmed = line.trim();
|
|
694
|
-
if (/^\[[^\]]+\]$/.test(trimmed)) {
|
|
695
|
-
if (inSection) {
|
|
696
|
-
break;
|
|
697
|
-
}
|
|
698
|
-
inSection = trimmed === sectionHeader;
|
|
699
|
-
continue;
|
|
700
|
-
}
|
|
701
|
-
if (inSection) {
|
|
702
|
-
collected.push(line);
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
return collected.join("\n");
|
|
706
|
-
}
|
|
707
|
-
function parseTomlBoolean(section, key) {
|
|
708
|
-
const match = section.match(new RegExp(`^\\s*${escapeRegex(key)}\\s*=\\s*(true|false)\\s*(?:#.*)?$`, "mi"));
|
|
709
|
-
return match ? match[1] === "true" : undefined;
|
|
710
|
-
}
|
|
711
|
-
function parseTomlString(content, key) {
|
|
712
|
-
const match = content.match(new RegExp(`^\\s*${escapeRegex(key)}\\s*=\\s*"([^"]*)"\\s*(?:#.*)?$`, "mi"));
|
|
713
|
-
return match?.[1];
|
|
714
|
-
}
|
|
715
|
-
function parseTomlStringArray(section, key) {
|
|
716
|
-
const match = section.match(new RegExp(`${escapeRegex(key)}\\s*=\\s*\\[([\\s\\S]*?)\\]`, "m"));
|
|
717
|
-
if (!match) {
|
|
718
|
-
return [];
|
|
719
|
-
}
|
|
720
|
-
return [...match[1].matchAll(/"([^"]+)"/g)].map((candidate) => candidate[1]);
|
|
721
|
-
}
|
|
722
660
|
function matchField(content, field) {
|
|
723
661
|
const match = content.match(new RegExp(`^\\s*${escapeRegex(field)}\\s*:\\s*(.+?)\\s*$`, "mi"));
|
|
724
662
|
return match?.[1]?.trim();
|
|
@@ -39,6 +39,51 @@ export function createRoundService(deps) {
|
|
|
39
39
|
status
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
|
+
async function findActiveRoleSession(input, timestamp) {
|
|
43
|
+
if (!input.repoRoot || !deps.sessionService) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
let sessions;
|
|
47
|
+
try {
|
|
48
|
+
sessions = await deps.sessionService.listRoleSessions(input.repoRoot, input.taskSlug);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const active = sessions
|
|
54
|
+
.filter((session) => session.status === "running" && session.activityStatus === "running")
|
|
55
|
+
.sort((left, right) => timestampMs(roleActivityTimestamp(right, timestamp)) - timestampMs(roleActivityTimestamp(left, timestamp)))[0];
|
|
56
|
+
if (!active) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
role: active.role,
|
|
61
|
+
startedAt: roleActivityTimestamp(active, timestamp)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function syncRoundFromActiveRoleSession(input, state, timestamp) {
|
|
65
|
+
const current = state.currentRound;
|
|
66
|
+
if (current?.status === "running" && current.activeTurnStartedAt) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
const active = await findActiveRoleSession(input, timestamp);
|
|
70
|
+
if (!active) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const shouldStartNewRound = !current || current.status === "stopped";
|
|
74
|
+
const next = applyPromptSubmitted({
|
|
75
|
+
state,
|
|
76
|
+
taskSlug: input.taskSlug,
|
|
77
|
+
role: active.role,
|
|
78
|
+
timestamp: active.startedAt,
|
|
79
|
+
updatedAt: timestamp,
|
|
80
|
+
roundId: shouldStartNewRound ? id() : current.id
|
|
81
|
+
});
|
|
82
|
+
await save(input, next);
|
|
83
|
+
clearSettleTimer(input);
|
|
84
|
+
await updateSessionStatus(input, "running");
|
|
85
|
+
return next;
|
|
86
|
+
}
|
|
42
87
|
async function settleIfNeeded(input, state, timestamp) {
|
|
43
88
|
const current = state.currentRound;
|
|
44
89
|
if (current?.status !== "running" ||
|
|
@@ -49,6 +94,10 @@ export function createRoundService(deps) {
|
|
|
49
94
|
if (timestamp < current.settleDeadlineAt) {
|
|
50
95
|
return state;
|
|
51
96
|
}
|
|
97
|
+
const activeSessionState = await syncRoundFromActiveRoleSession(input, state, timestamp);
|
|
98
|
+
if (activeSessionState) {
|
|
99
|
+
return activeSessionState;
|
|
100
|
+
}
|
|
52
101
|
const stopped = {
|
|
53
102
|
...current,
|
|
54
103
|
status: "stopped",
|
|
@@ -76,6 +125,10 @@ export function createRoundService(deps) {
|
|
|
76
125
|
return;
|
|
77
126
|
}
|
|
78
127
|
const timestamp = maxIsoTimestamp(now(), settleDeadlineAt);
|
|
128
|
+
const activeSessionState = await syncRoundFromActiveRoleSession(input, state, timestamp);
|
|
129
|
+
if (activeSessionState) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
79
132
|
if (settleGuard) {
|
|
80
133
|
const decision = await runSettleGuard(settleGuard, {
|
|
81
134
|
...input,
|
|
@@ -178,7 +231,10 @@ export function createRoundService(deps) {
|
|
|
178
231
|
async getSessionRoundState(input) {
|
|
179
232
|
return withTaskLock(input, async () => {
|
|
180
233
|
const timestamp = now();
|
|
181
|
-
|
|
234
|
+
const loaded = await load(input);
|
|
235
|
+
const synced = await syncRoundFromActiveRoleSession(input, loaded, timestamp);
|
|
236
|
+
const settled = synced ?? await settleIfNeeded(input, loaded, timestamp);
|
|
237
|
+
return toSessionRoundState(settled, timestamp);
|
|
182
238
|
});
|
|
183
239
|
},
|
|
184
240
|
recordRoleTurnEvent,
|
|
@@ -198,7 +254,19 @@ export function applyRoundHookEvent(input) {
|
|
|
198
254
|
return applyStop(input);
|
|
199
255
|
}
|
|
200
256
|
function applyPromptSubmitted(input) {
|
|
257
|
+
const updatedAt = input.updatedAt ?? input.timestamp;
|
|
201
258
|
const current = input.state.currentRound;
|
|
259
|
+
if (current?.status === "running" && current.activeTurnStartedAt && current.activeRole === input.role) {
|
|
260
|
+
return {
|
|
261
|
+
...input.state,
|
|
262
|
+
taskSlug: input.taskSlug,
|
|
263
|
+
currentRound: {
|
|
264
|
+
...current,
|
|
265
|
+
roles: appendUniqueRole(current.roles, input.role)
|
|
266
|
+
},
|
|
267
|
+
updatedAt
|
|
268
|
+
};
|
|
269
|
+
}
|
|
202
270
|
const shouldStartNewRound = !current || current.status === "stopped";
|
|
203
271
|
const totalRoundCount = shouldStartNewRound
|
|
204
272
|
? input.state.totalRoundCount + 1
|
|
@@ -236,12 +304,12 @@ function applyPromptSubmitted(input) {
|
|
|
236
304
|
totalTurnCount: input.state.totalTurnCount + 1,
|
|
237
305
|
totalCompletedTurnCount: input.state.totalCompletedTurnCount,
|
|
238
306
|
totalCcActiveMs: input.state.totalCcActiveMs,
|
|
239
|
-
updatedAt
|
|
307
|
+
updatedAt
|
|
240
308
|
};
|
|
241
309
|
}
|
|
242
310
|
function applyStop(input) {
|
|
243
311
|
const current = input.state.currentRound;
|
|
244
|
-
if (!current || current.status === "stopped" || !current.activeTurnStartedAt) {
|
|
312
|
+
if (!current || current.status === "stopped" || !current.activeTurnStartedAt || current.activeRole !== input.role) {
|
|
245
313
|
return {
|
|
246
314
|
...input.state,
|
|
247
315
|
taskSlug: input.taskSlug,
|
|
@@ -389,6 +457,13 @@ function addMilliseconds(value, milliseconds) {
|
|
|
389
457
|
function maxIsoTimestamp(left, right) {
|
|
390
458
|
return Date.parse(left) >= Date.parse(right) ? left : right;
|
|
391
459
|
}
|
|
460
|
+
function roleActivityTimestamp(session, fallback) {
|
|
461
|
+
return session.lastTurnStartedAt ?? session.lastHookEventAt ?? session.updatedAt ?? fallback;
|
|
462
|
+
}
|
|
463
|
+
function timestampMs(value) {
|
|
464
|
+
const parsed = Date.parse(value);
|
|
465
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
466
|
+
}
|
|
392
467
|
async function runSettleGuard(settleGuard, input) {
|
|
393
468
|
try {
|
|
394
469
|
return await settleGuard(input);
|
|
@@ -26,7 +26,9 @@ export function createSessionService(deps) {
|
|
|
26
26
|
const model = isCodexReviewer
|
|
27
27
|
? normalizeCodexModel(input.model ?? persisted?.model)
|
|
28
28
|
: normalizeClaudeModel(input.model ?? persisted?.model);
|
|
29
|
-
const effort =
|
|
29
|
+
const effort = isCodexReviewer
|
|
30
|
+
? normalizeCodexEffort(input.effort ?? persisted?.effort)
|
|
31
|
+
: normalizeClaudeEffort(input.effort ?? persisted?.effort);
|
|
30
32
|
const claudeSessionId = launchMode === "resume"
|
|
31
33
|
? persisted?.claudeSessionId
|
|
32
34
|
: randomUUID();
|
|
@@ -225,6 +227,24 @@ export function createSessionService(deps) {
|
|
|
225
227
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
226
228
|
await persistTaskSession(deps.fs, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
|
|
227
229
|
return updated;
|
|
230
|
+
},
|
|
231
|
+
async markRoleActivityIdle(repoRoot, taskSlug, role) {
|
|
232
|
+
const current = await this.getRoleSession(repoRoot, taskSlug, role);
|
|
233
|
+
if (!current) {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
const timestamp = now();
|
|
237
|
+
const updated = {
|
|
238
|
+
...current,
|
|
239
|
+
activityStatus: "idle",
|
|
240
|
+
lastTurnEndedAt: timestamp,
|
|
241
|
+
updatedAt: timestamp
|
|
242
|
+
};
|
|
243
|
+
deps.registry.upsert(updated);
|
|
244
|
+
const config = await deps.projectService.loadConfig(repoRoot);
|
|
245
|
+
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
246
|
+
await persistTaskSession(deps.fs, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
|
|
247
|
+
return updated;
|
|
228
248
|
}
|
|
229
249
|
};
|
|
230
250
|
}
|
|
@@ -241,21 +261,15 @@ async function buildCodexReviewerStartCommand(fs, taskRepoRoot, launchMode, sele
|
|
|
241
261
|
}
|
|
242
262
|
await fs.ensureDir(reviewDir);
|
|
243
263
|
const config = await loadCodexSessionConfig(fs, taskRepoRoot);
|
|
244
|
-
const model = selectedModel === "default"
|
|
245
|
-
? config.model
|
|
246
|
-
: selectedModel;
|
|
247
|
-
const modelReasoningEffort = selectedEffort === "default"
|
|
248
|
-
? config.modelReasoningEffort
|
|
249
|
-
: selectedEffort;
|
|
250
264
|
const args = launchMode === "resume"
|
|
251
265
|
? ["resume", "--last"]
|
|
252
266
|
: [];
|
|
253
|
-
args.push("--cd", codexDir, "--add-dir", reviewDir, "--sandbox", "workspace-write", "--ask-for-approval", "never", "--dangerously-bypass-hook-trust");
|
|
254
|
-
if (
|
|
255
|
-
args.push("--model",
|
|
267
|
+
args.push("--cd", codexDir, "--add-dir", reviewDir, "--sandbox", "workspace-write", "--ask-for-approval", "never", "--dangerously-bypass-hook-trust", "--search");
|
|
268
|
+
if (selectedModel !== "default") {
|
|
269
|
+
args.push("--model", selectedModel);
|
|
256
270
|
}
|
|
257
|
-
if (
|
|
258
|
-
args.push("--config", `model_reasoning_effort="${
|
|
271
|
+
if (selectedEffort !== "default") {
|
|
272
|
+
args.push("--config", `model_reasoning_effort="${selectedEffort}"`);
|
|
259
273
|
}
|
|
260
274
|
return {
|
|
261
275
|
command: config.command,
|
|
@@ -268,17 +282,13 @@ async function loadCodexSessionConfig(fs, taskRepoRoot) {
|
|
|
268
282
|
const configPath = resolveRepoPath(taskRepoRoot, CODEX_CONFIG_PATH);
|
|
269
283
|
if (!(await fs.pathExists(configPath))) {
|
|
270
284
|
return {
|
|
271
|
-
command: "codex"
|
|
272
|
-
model: "gpt-5.5",
|
|
273
|
-
modelReasoningEffort: "xhigh"
|
|
285
|
+
command: "codex"
|
|
274
286
|
};
|
|
275
287
|
}
|
|
276
288
|
const content = await fs.readText(configPath);
|
|
277
289
|
const reviewSection = extractTomlSection(content, "vcm.codex_review");
|
|
278
290
|
return {
|
|
279
|
-
command: parseTomlString(reviewSection, "command") ?? "codex"
|
|
280
|
-
model: parseTomlString(content, "model") ?? "gpt-5.5",
|
|
281
|
-
modelReasoningEffort: parseTomlString(content, "model_reasoning_effort") ?? "xhigh"
|
|
291
|
+
command: parseTomlString(content, "command") ?? parseTomlString(reviewSection, "command") ?? "codex"
|
|
282
292
|
};
|
|
283
293
|
}
|
|
284
294
|
function matchesRoleHookSession(record, input) {
|
|
@@ -332,7 +342,9 @@ async function loadPersistedRoleRecord(fs, repoRoot, stateRoot, taskSlug, role)
|
|
|
332
342
|
model: record.role === CODEX_REVIEWER_ROLE
|
|
333
343
|
? normalizeCodexModel(record.model)
|
|
334
344
|
: normalizeClaudeModel(record.model),
|
|
335
|
-
effort:
|
|
345
|
+
effort: record.role === CODEX_REVIEWER_ROLE
|
|
346
|
+
? normalizeCodexEffort(record.effort)
|
|
347
|
+
: normalizeClaudeEffort(record.effort)
|
|
336
348
|
}
|
|
337
349
|
: undefined;
|
|
338
350
|
}
|
|
@@ -400,7 +412,18 @@ function normalizeCodexModel(value) {
|
|
|
400
412
|
}
|
|
401
413
|
return "gpt-5.5";
|
|
402
414
|
}
|
|
403
|
-
function
|
|
415
|
+
function normalizeClaudeEffort(value) {
|
|
416
|
+
if (value === "low"
|
|
417
|
+
|| value === "medium"
|
|
418
|
+
|| value === "high"
|
|
419
|
+
|| value === "xhigh"
|
|
420
|
+
|| value === "max"
|
|
421
|
+
|| value === "ultracode") {
|
|
422
|
+
return value;
|
|
423
|
+
}
|
|
424
|
+
return "default";
|
|
425
|
+
}
|
|
426
|
+
function normalizeCodexEffort(value) {
|
|
404
427
|
if (value === "low"
|
|
405
428
|
|| value === "medium"
|
|
406
429
|
|| value === "high"
|
|
@@ -9,6 +9,16 @@ TBD
|
|
|
9
9
|
|
|
10
10
|
TBD
|
|
11
11
|
|
|
12
|
+
## Scaffold Manifest
|
|
13
|
+
|
|
14
|
+
Task-specific context and coder guidance go here, not in source-code comments.
|
|
15
|
+
Source-code comments should only describe durable behavior, contracts, invariants,
|
|
16
|
+
error boundaries, or non-obvious logic that should remain useful after this task.
|
|
17
|
+
|
|
18
|
+
| File | Action | Task Context | Durable Code Comment Needed | Coder Work | Allowed Freedom | VCM:CODE | Proof Point | Replan Trigger |
|
|
19
|
+
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
20
|
+
| TBD | TBD | TBD | TBD | TBD | TBD | TBD | TBD | TBD |
|
|
21
|
+
|
|
12
22
|
## Implementation Plan
|
|
13
23
|
|
|
14
24
|
TBD
|