vibe-coding-master 0.3.2 → 0.3.4
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/dist/backend/adapters/claude-adapter.js +4 -1
- package/dist/backend/api/round-routes.js +1 -0
- package/dist/backend/runtime/node-pty-runtime.js +45 -2
- 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 +45 -107
- package/dist/backend/services/round-service.js +78 -3
- package/dist/backend/services/session-service.js +43 -20
- package/dist/backend/templates/harness/codex-review.js +7 -30
- package/dist/shared/types/session.js +10 -1
- package/dist-frontend/assets/{index-BavJjWQY.js → index-OPkFuHRf.js} +29 -29
- package/dist-frontend/index.html +1 -1
- package/docs/codex-file-translation-plan.md +618 -0
- package/docs/codex-review-gates.md +30 -16
- package/package.json +1 -1
|
@@ -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
|
|
@@ -33,16 +33,22 @@ export function createNodePtyTerminalRuntime(deps) {
|
|
|
33
33
|
startedAt: now(),
|
|
34
34
|
exitCode: null
|
|
35
35
|
};
|
|
36
|
+
const logWriter = createTerminalLogWriter(deps.fs, input.logPath, deps.onLogWriteError ?? defaultLogWriteErrorHandler);
|
|
36
37
|
const entry = {
|
|
37
38
|
input,
|
|
38
39
|
session,
|
|
39
40
|
process: child,
|
|
40
|
-
listeners: new Set()
|
|
41
|
+
listeners: new Set(),
|
|
42
|
+
logWriter,
|
|
43
|
+
disposed: false
|
|
41
44
|
};
|
|
42
45
|
entries.set(session.id, entry);
|
|
43
46
|
child.onData((data) => {
|
|
47
|
+
if (entry.disposed) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
44
50
|
entry.session.lastOutputAt = now();
|
|
45
|
-
|
|
51
|
+
entry.logWriter.append(data);
|
|
46
52
|
emit(entry, {
|
|
47
53
|
sessionId: session.id,
|
|
48
54
|
taskSlug: input.taskSlug,
|
|
@@ -52,8 +58,13 @@ export function createNodePtyTerminalRuntime(deps) {
|
|
|
52
58
|
});
|
|
53
59
|
});
|
|
54
60
|
child.onExit(({ exitCode }) => {
|
|
61
|
+
if (entry.disposed) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
entry.disposed = true;
|
|
55
65
|
entry.session.status = exitCode === 0 ? "exited" : "crashed";
|
|
56
66
|
entry.session.exitCode = exitCode;
|
|
67
|
+
void entry.logWriter.close();
|
|
57
68
|
emit(entry, {
|
|
58
69
|
sessionId: session.id,
|
|
59
70
|
taskSlug: input.taskSlug,
|
|
@@ -102,12 +113,16 @@ export function createNodePtyTerminalRuntime(deps) {
|
|
|
102
113
|
},
|
|
103
114
|
async stop(sessionId) {
|
|
104
115
|
const entry = getEntry(entries, sessionId);
|
|
116
|
+
entry.disposed = true;
|
|
105
117
|
entry.session.status = "exited";
|
|
106
118
|
entry.process.kill();
|
|
119
|
+
await entry.logWriter.close();
|
|
107
120
|
},
|
|
108
121
|
async restart(sessionId) {
|
|
109
122
|
const entry = getEntry(entries, sessionId);
|
|
123
|
+
entry.disposed = true;
|
|
110
124
|
entry.process.kill();
|
|
125
|
+
await entry.logWriter.close();
|
|
111
126
|
return create(entry.input, sessionId);
|
|
112
127
|
},
|
|
113
128
|
subscribe(sessionId, listener, options = {}) {
|
|
@@ -139,12 +154,40 @@ export function createNodePtyTerminalRuntime(deps) {
|
|
|
139
154
|
}
|
|
140
155
|
};
|
|
141
156
|
}
|
|
157
|
+
export function createTerminalLogWriter(fs, logPath, onError) {
|
|
158
|
+
let closed = false;
|
|
159
|
+
let pending = Promise.resolve();
|
|
160
|
+
return {
|
|
161
|
+
append(data) {
|
|
162
|
+
if (closed || !data) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
pending = pending
|
|
166
|
+
.then(() => fs.appendText(logPath, data))
|
|
167
|
+
.catch((error) => {
|
|
168
|
+
try {
|
|
169
|
+
onError(error, logPath);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// Logging must never break terminal output delivery or future log writes.
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
async close() {
|
|
177
|
+
closed = true;
|
|
178
|
+
await pending.catch(() => undefined);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
142
182
|
async function readTerminalReplayText(fs, logPath) {
|
|
143
183
|
const data = fs.readTextTail
|
|
144
184
|
? await fs.readTextTail(logPath, TERMINAL_REPLAY_TAIL_LIMIT_BYTES)
|
|
145
185
|
: tailTerminalReplay(await fs.readText(logPath));
|
|
146
186
|
return tailTerminalReplay(data);
|
|
147
187
|
}
|
|
188
|
+
function defaultLogWriteErrorHandler(error, logPath) {
|
|
189
|
+
console.warn(`[VCM] Failed to append terminal log: ${logPath}`, error);
|
|
190
|
+
}
|
|
148
191
|
export function tailTerminalReplay(data, limitBytes = TERMINAL_REPLAY_TAIL_LIMIT_BYTES) {
|
|
149
192
|
if (limitBytes <= 0 || Buffer.byteLength(data, "utf8") <= limitBytes) {
|
|
150
193
|
return data;
|
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);
|
|
@@ -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);
|