vibe-coding-master 0.2.1 → 0.2.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 +15 -8
- package/dist/backend/adapters/claude-adapter.js +0 -3
- package/dist/backend/adapters/filesystem.js +14 -0
- package/dist/backend/api/round-routes.js +1 -10
- package/dist/backend/api/translation-routes.js +8 -0
- package/dist/backend/runtime/node-pty-runtime.js +21 -1
- package/dist/backend/server.js +9 -9
- package/dist/backend/services/app-settings-service.js +4 -1
- package/dist/backend/services/claude-hook-service.js +36 -0
- package/dist/backend/services/harness-service.js +0 -3
- package/dist/backend/services/round-service.js +309 -57
- package/dist/backend/services/session-service.js +14 -2
- package/dist/backend/services/translation-service.js +280 -13
- package/dist/backend/templates/harness/reviewer-agent.js +2 -0
- package/dist/shared/types/translation.js +1 -0
- package/dist-frontend/assets/index-CfqduxVB.css +32 -0
- package/dist-frontend/assets/index-DaVjpyA9.js +90 -0
- package/dist-frontend/index.html +2 -2
- package/docs/product-design.md +35 -16
- package/package.json +1 -1
- package/scripts/install-vcm-harness.mjs +2 -0
- package/dist-frontend/assets/index-BmpJxnNx.css +0 -32
- package/dist-frontend/assets/index-CGUkhVAP.js +0 -90
|
@@ -1,79 +1,331 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
const DEFAULT_SETTLE_MS = 10_000;
|
|
4
|
+
const setGlobalTimeout = globalThis.setTimeout.bind(globalThis);
|
|
5
|
+
const clearGlobalTimeout = globalThis.clearTimeout.bind(globalThis);
|
|
6
|
+
export function createRoundService(deps) {
|
|
3
7
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
const id = deps.id ?? (() => `round_${randomUUID()}`);
|
|
9
|
+
const settleMs = deps.settleMs ?? DEFAULT_SETTLE_MS;
|
|
10
|
+
const setTimer = deps.setTimeout ?? setGlobalTimeout;
|
|
11
|
+
const clearTimer = deps.clearTimeout ?? clearGlobalTimeout;
|
|
12
|
+
const taskLocks = new Map();
|
|
13
|
+
const settleTimers = new Map();
|
|
14
|
+
async function load(input) {
|
|
15
|
+
const statePath = getRoundStatePath(input);
|
|
16
|
+
if (!(await deps.fs.pathExists(statePath))) {
|
|
17
|
+
return {
|
|
18
|
+
version: 1,
|
|
8
19
|
taskSlug: input.taskSlug,
|
|
9
|
-
|
|
10
|
-
|
|
20
|
+
totalRoundCount: 0,
|
|
21
|
+
totalPromptSubmitCount: 0,
|
|
22
|
+
totalStopCount: 0,
|
|
23
|
+
totalCcActiveMs: 0,
|
|
11
24
|
updatedAt: now()
|
|
12
|
-
});
|
|
13
|
-
return {
|
|
14
|
-
...state,
|
|
15
|
-
pendingRouteCount: input.pendingRouteCount
|
|
16
25
|
};
|
|
26
|
+
}
|
|
27
|
+
return normalizeRoundFile(await deps.fs.readJson(statePath), input.taskSlug, now());
|
|
28
|
+
}
|
|
29
|
+
async function save(input, state) {
|
|
30
|
+
await deps.fs.writeJsonAtomic(getRoundStatePath(input), state);
|
|
31
|
+
}
|
|
32
|
+
async function settleIfNeeded(input, state, timestamp) {
|
|
33
|
+
const current = state.currentRound;
|
|
34
|
+
if (current?.status !== "settling" || !current.settleDeadlineAt) {
|
|
35
|
+
return state;
|
|
36
|
+
}
|
|
37
|
+
if (timestamp < current.settleDeadlineAt) {
|
|
38
|
+
return state;
|
|
39
|
+
}
|
|
40
|
+
const paused = {
|
|
41
|
+
...current,
|
|
42
|
+
status: "paused",
|
|
43
|
+
pausedAt: current.settleDeadlineAt
|
|
44
|
+
};
|
|
45
|
+
const next = {
|
|
46
|
+
...state,
|
|
47
|
+
currentRound: paused,
|
|
48
|
+
lastPausedRound: paused,
|
|
49
|
+
updatedAt: timestamp
|
|
50
|
+
};
|
|
51
|
+
await save(input, next);
|
|
52
|
+
return next;
|
|
53
|
+
}
|
|
54
|
+
async function settleIfStillCurrent(input, roundId, settleDeadlineAt) {
|
|
55
|
+
await withTaskLock(input, async () => {
|
|
56
|
+
const state = await load(input);
|
|
57
|
+
const current = state.currentRound;
|
|
58
|
+
if (current?.id !== roundId ||
|
|
59
|
+
current.status !== "settling" ||
|
|
60
|
+
current.settleDeadlineAt !== settleDeadlineAt) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
await settleIfNeeded(input, state, settleDeadlineAt);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function clearSettleTimer(input) {
|
|
67
|
+
const key = getRoundStatePath(input);
|
|
68
|
+
const timer = settleTimers.get(key);
|
|
69
|
+
if (timer === undefined) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
clearTimer(timer);
|
|
73
|
+
settleTimers.delete(key);
|
|
74
|
+
}
|
|
75
|
+
function clearSettleTimersForTask(taskSlug) {
|
|
76
|
+
for (const [key, timer] of settleTimers) {
|
|
77
|
+
if (!key.endsWith(path.join("rounds", `${taskSlug}.json`))) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
clearTimer(timer);
|
|
81
|
+
settleTimers.delete(key);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function scheduleSettleTimer(input, round, timestamp) {
|
|
85
|
+
if (round.status !== "settling" || !round.settleDeadlineAt) {
|
|
86
|
+
clearSettleTimer(input);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
clearSettleTimer(input);
|
|
90
|
+
const delayMs = Math.max(0, new Date(round.settleDeadlineAt).getTime() - new Date(timestamp).getTime());
|
|
91
|
+
const timer = setTimer(() => {
|
|
92
|
+
settleTimers.delete(getRoundStatePath(input));
|
|
93
|
+
void settleIfStillCurrent(input, round.id, round.settleDeadlineAt ?? "").catch(() => undefined);
|
|
94
|
+
}, delayMs);
|
|
95
|
+
settleTimers.set(getRoundStatePath(input), timer);
|
|
96
|
+
}
|
|
97
|
+
async function withTaskLock(input, run) {
|
|
98
|
+
const key = getRoundStatePath(input);
|
|
99
|
+
const previous = taskLocks.get(key) ?? Promise.resolve();
|
|
100
|
+
const next = previous.catch(() => undefined).then(run);
|
|
101
|
+
taskLocks.set(key, next);
|
|
102
|
+
try {
|
|
103
|
+
return await next;
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
if (taskLocks.get(key) === next) {
|
|
107
|
+
taskLocks.delete(key);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
async getTaskRoundState(input) {
|
|
113
|
+
return withTaskLock(input, async () => {
|
|
114
|
+
const timestamp = now();
|
|
115
|
+
const state = await settleIfNeeded(input, await load(input), timestamp);
|
|
116
|
+
return toTaskRoundState(state, timestamp);
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
async recordClaudeHookEvent(input) {
|
|
120
|
+
return withTaskLock(input, async () => {
|
|
121
|
+
const timestamp = now();
|
|
122
|
+
const settled = await settleIfNeeded(input, await load(input), timestamp);
|
|
123
|
+
const current = settled.currentRound;
|
|
124
|
+
const shouldStartNewRound = input.eventName === "UserPromptSubmit" && (!current || current.status === "paused");
|
|
125
|
+
const next = applyRoundHookEvent({
|
|
126
|
+
state: settled,
|
|
127
|
+
taskSlug: input.taskSlug,
|
|
128
|
+
role: input.role,
|
|
129
|
+
eventName: input.eventName,
|
|
130
|
+
timestamp,
|
|
131
|
+
roundId: shouldStartNewRound ? id() : current?.id ?? "",
|
|
132
|
+
settleMs
|
|
133
|
+
});
|
|
134
|
+
await save(input, next);
|
|
135
|
+
if (input.eventName === "UserPromptSubmit") {
|
|
136
|
+
clearSettleTimer(input);
|
|
137
|
+
}
|
|
138
|
+
else if (next.currentRound) {
|
|
139
|
+
scheduleSettleTimer(input, next.currentRound, timestamp);
|
|
140
|
+
}
|
|
141
|
+
return toTaskRoundState(next, timestamp);
|
|
142
|
+
});
|
|
17
143
|
},
|
|
18
144
|
stopSession() { },
|
|
19
|
-
stopTask() {
|
|
145
|
+
stopTask(taskSlug) {
|
|
146
|
+
clearSettleTimersForTask(taskSlug);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
export function applyRoundHookEvent(input) {
|
|
151
|
+
if (input.eventName === "UserPromptSubmit") {
|
|
152
|
+
return applyPromptSubmitted(input);
|
|
153
|
+
}
|
|
154
|
+
return applyStop(input);
|
|
155
|
+
}
|
|
156
|
+
function applyPromptSubmitted(input) {
|
|
157
|
+
const current = input.state.currentRound;
|
|
158
|
+
const shouldStartNewRound = !current || current.status === "paused";
|
|
159
|
+
const totalRoundCount = shouldStartNewRound
|
|
160
|
+
? input.state.totalRoundCount + 1
|
|
161
|
+
: input.state.totalRoundCount;
|
|
162
|
+
const nextRound = shouldStartNewRound
|
|
163
|
+
? {
|
|
164
|
+
id: input.roundId,
|
|
165
|
+
sequence: totalRoundCount,
|
|
166
|
+
status: "active",
|
|
167
|
+
activeRole: input.role,
|
|
168
|
+
startedAt: input.timestamp,
|
|
169
|
+
lastPromptSubmittedAt: input.timestamp,
|
|
170
|
+
runningSince: input.timestamp,
|
|
171
|
+
ccActiveMs: 0,
|
|
172
|
+
promptSubmitCount: 1,
|
|
173
|
+
stopCount: 0,
|
|
174
|
+
roles: [input.role]
|
|
175
|
+
}
|
|
176
|
+
: {
|
|
177
|
+
...current,
|
|
178
|
+
status: "active",
|
|
179
|
+
activeRole: input.role,
|
|
180
|
+
lastPromptSubmittedAt: input.timestamp,
|
|
181
|
+
settleDeadlineAt: undefined,
|
|
182
|
+
pausedAt: undefined,
|
|
183
|
+
runningSince: current.runningSince ?? input.timestamp,
|
|
184
|
+
promptSubmitCount: current.promptSubmitCount + 1,
|
|
185
|
+
roles: appendUniqueRole(current.roles, input.role)
|
|
186
|
+
};
|
|
187
|
+
return {
|
|
188
|
+
...input.state,
|
|
189
|
+
taskSlug: input.taskSlug,
|
|
190
|
+
currentRound: nextRound,
|
|
191
|
+
totalRoundCount,
|
|
192
|
+
totalPromptSubmitCount: input.state.totalPromptSubmitCount + 1,
|
|
193
|
+
totalStopCount: input.state.totalStopCount,
|
|
194
|
+
totalCcActiveMs: input.state.totalCcActiveMs,
|
|
195
|
+
updatedAt: input.timestamp
|
|
20
196
|
};
|
|
21
197
|
}
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
roleState.status === "waiting_user" ||
|
|
26
|
-
roleState.status === "abnormal");
|
|
27
|
-
if (activeRole) {
|
|
198
|
+
function applyStop(input) {
|
|
199
|
+
const current = input.state.currentRound;
|
|
200
|
+
if (!current || current.status === "paused") {
|
|
28
201
|
return {
|
|
202
|
+
...input.state,
|
|
29
203
|
taskSlug: input.taskSlug,
|
|
30
|
-
|
|
31
|
-
activeRole: activeRole.role,
|
|
32
|
-
roles: input.roleStates,
|
|
33
|
-
updatedAt: input.updatedAt
|
|
204
|
+
updatedAt: input.timestamp
|
|
34
205
|
};
|
|
35
206
|
}
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
207
|
+
const activeDurationMs = current.runningSince
|
|
208
|
+
? getDurationMs(current.runningSince, input.timestamp)
|
|
209
|
+
: 0;
|
|
210
|
+
const nextRound = {
|
|
211
|
+
...current,
|
|
212
|
+
status: "settling",
|
|
213
|
+
activeRole: input.role,
|
|
214
|
+
lastStopAt: input.timestamp,
|
|
215
|
+
settleDeadlineAt: addMilliseconds(input.timestamp, input.settleMs),
|
|
216
|
+
runningSince: undefined,
|
|
217
|
+
ccActiveMs: current.ccActiveMs + activeDurationMs,
|
|
218
|
+
stopCount: current.stopCount + 1,
|
|
219
|
+
roles: appendUniqueRole(current.roles, input.role)
|
|
220
|
+
};
|
|
221
|
+
return {
|
|
222
|
+
...input.state,
|
|
223
|
+
taskSlug: input.taskSlug,
|
|
224
|
+
currentRound: nextRound,
|
|
225
|
+
totalStopCount: input.state.totalStopCount + 1,
|
|
226
|
+
totalCcActiveMs: input.state.totalCcActiveMs + activeDurationMs,
|
|
227
|
+
updatedAt: input.timestamp
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function toTaskRoundState(state, updatedAt) {
|
|
231
|
+
const current = state.currentRound;
|
|
232
|
+
if (!current) {
|
|
39
233
|
return {
|
|
40
|
-
taskSlug:
|
|
41
|
-
status: "
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
234
|
+
taskSlug: state.taskSlug,
|
|
235
|
+
status: "idle",
|
|
236
|
+
promptSubmitCount: 0,
|
|
237
|
+
stopCount: 0,
|
|
238
|
+
totalRoundCount: state.totalRoundCount,
|
|
239
|
+
totalPromptSubmitCount: state.totalPromptSubmitCount,
|
|
240
|
+
totalStopCount: state.totalStopCount,
|
|
241
|
+
totalCcActiveMs: state.totalCcActiveMs,
|
|
242
|
+
currentRoundCcActiveMs: 0,
|
|
243
|
+
roles: [],
|
|
244
|
+
updatedAt
|
|
47
245
|
};
|
|
48
246
|
}
|
|
247
|
+
const activeDurationMs = current.runningSince
|
|
248
|
+
? getDurationMs(current.runningSince, updatedAt)
|
|
249
|
+
: 0;
|
|
250
|
+
const currentRoundCcActiveMs = current.ccActiveMs + activeDurationMs;
|
|
49
251
|
return {
|
|
50
|
-
taskSlug:
|
|
51
|
-
status:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
252
|
+
taskSlug: state.taskSlug,
|
|
253
|
+
status: current.status,
|
|
254
|
+
roundId: current.id,
|
|
255
|
+
pauseId: current.status === "paused" ? `${current.id}:${current.pausedAt ?? current.lastStopAt ?? ""}` : undefined,
|
|
256
|
+
activeRole: current.activeRole,
|
|
257
|
+
startedAt: current.startedAt,
|
|
258
|
+
lastPromptSubmittedAt: current.lastPromptSubmittedAt,
|
|
259
|
+
lastStopAt: current.lastStopAt,
|
|
260
|
+
settleDeadlineAt: current.settleDeadlineAt,
|
|
261
|
+
pausedAt: current.pausedAt,
|
|
262
|
+
runningSince: current.runningSince,
|
|
263
|
+
roundSequence: current.sequence,
|
|
264
|
+
promptSubmitCount: current.promptSubmitCount,
|
|
265
|
+
stopCount: current.stopCount,
|
|
266
|
+
totalRoundCount: state.totalRoundCount,
|
|
267
|
+
totalPromptSubmitCount: state.totalPromptSubmitCount,
|
|
268
|
+
totalStopCount: state.totalStopCount,
|
|
269
|
+
totalCcActiveMs: state.totalCcActiveMs + activeDurationMs,
|
|
270
|
+
currentRoundCcActiveMs,
|
|
271
|
+
roles: current.roles,
|
|
272
|
+
updatedAt
|
|
55
273
|
};
|
|
56
274
|
}
|
|
57
|
-
function
|
|
58
|
-
const session = sessions.find((candidate) => candidate.role === role && candidate.status === "running");
|
|
59
|
-
const activityStatus = session?.activityStatus ?? "idle";
|
|
275
|
+
function normalizeRoundFile(input, taskSlug, updatedAt) {
|
|
60
276
|
return {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
: "Claude Code emitted Stop or has not started a prompt in this process."
|
|
71
|
-
: undefined
|
|
277
|
+
version: 1,
|
|
278
|
+
taskSlug,
|
|
279
|
+
currentRound: normalizeRound(input.currentRound),
|
|
280
|
+
lastPausedRound: normalizeRound(input.lastPausedRound),
|
|
281
|
+
totalRoundCount: normalizeNumber(input.totalRoundCount),
|
|
282
|
+
totalPromptSubmitCount: normalizeNumber(input.totalPromptSubmitCount),
|
|
283
|
+
totalStopCount: normalizeNumber(input.totalStopCount),
|
|
284
|
+
totalCcActiveMs: normalizeNumber(input.totalCcActiveMs),
|
|
285
|
+
updatedAt: typeof input.updatedAt === "string" ? input.updatedAt : updatedAt
|
|
72
286
|
};
|
|
73
287
|
}
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
288
|
+
function normalizeRound(input) {
|
|
289
|
+
if (!input || typeof input.id !== "string") {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
if (input.status !== "active" && input.status !== "settling" && input.status !== "paused") {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
id: input.id,
|
|
297
|
+
sequence: Number.isFinite(input.sequence) ? input.sequence : 1,
|
|
298
|
+
status: input.status,
|
|
299
|
+
activeRole: input.activeRole,
|
|
300
|
+
startedAt: input.startedAt,
|
|
301
|
+
lastPromptSubmittedAt: input.lastPromptSubmittedAt,
|
|
302
|
+
lastStopAt: input.lastStopAt,
|
|
303
|
+
settleDeadlineAt: input.settleDeadlineAt,
|
|
304
|
+
pausedAt: input.pausedAt,
|
|
305
|
+
runningSince: input.runningSince,
|
|
306
|
+
ccActiveMs: Number.isFinite(input.ccActiveMs) ? input.ccActiveMs : 0,
|
|
307
|
+
promptSubmitCount: Number.isFinite(input.promptSubmitCount) ? input.promptSubmitCount : 0,
|
|
308
|
+
stopCount: Number.isFinite(input.stopCount) ? input.stopCount : 0,
|
|
309
|
+
roles: Array.isArray(input.roles) ? input.roles : []
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function appendUniqueRole(roles, role) {
|
|
313
|
+
return roles.includes(role) ? roles : [...roles, role];
|
|
314
|
+
}
|
|
315
|
+
function addMilliseconds(value, milliseconds) {
|
|
316
|
+
return new Date(new Date(value).getTime() + milliseconds).toISOString();
|
|
317
|
+
}
|
|
318
|
+
function getDurationMs(start, end) {
|
|
319
|
+
const startMs = Date.parse(start);
|
|
320
|
+
const endMs = Date.parse(end);
|
|
321
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) {
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
|
324
|
+
return Math.max(0, endMs - startMs);
|
|
325
|
+
}
|
|
326
|
+
function normalizeNumber(value) {
|
|
327
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
328
|
+
}
|
|
329
|
+
function getRoundStatePath(input) {
|
|
330
|
+
return path.join(input.stateRepoRoot, input.stateRoot, "rounds", `${input.taskSlug}.json`);
|
|
79
331
|
}
|
|
@@ -17,7 +17,7 @@ export function createSessionService(deps) {
|
|
|
17
17
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
18
18
|
const paths = deps.artifactService.getHandoffPaths(taskRepoRoot, task.handoffDir);
|
|
19
19
|
const persisted = await loadPersistedRoleRecord(deps.fs, taskRepoRoot, config.stateRoot, taskSlug, role);
|
|
20
|
-
const permissionMode = input.permissionMode ?? persisted?.permissionMode
|
|
20
|
+
const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
|
|
21
21
|
const claudeSessionId = launchMode === "resume"
|
|
22
22
|
? persisted?.claudeSessionId
|
|
23
23
|
: randomUUID();
|
|
@@ -236,7 +236,13 @@ async function loadPersistedRoleRecord(fs, repoRoot, stateRoot, taskSlug, role)
|
|
|
236
236
|
return undefined;
|
|
237
237
|
}
|
|
238
238
|
const current = await fs.readJson(sessionPath);
|
|
239
|
-
|
|
239
|
+
const record = current.roles[role]?.record;
|
|
240
|
+
return record
|
|
241
|
+
? {
|
|
242
|
+
...record,
|
|
243
|
+
permissionMode: normalizeClaudePermissionMode(record.permissionMode)
|
|
244
|
+
}
|
|
245
|
+
: undefined;
|
|
240
246
|
}
|
|
241
247
|
async function persistTaskSession(fs, repoRoot, stateRoot, session) {
|
|
242
248
|
const sessionPath = getTaskSessionPath(repoRoot, stateRoot, session.taskSlug);
|
|
@@ -279,3 +285,9 @@ function createEmptyTaskSessionRecord(taskSlug, updatedAt) {
|
|
|
279
285
|
function getTaskSessionPath(repoRoot, stateRoot, taskSlug) {
|
|
280
286
|
return path.join(repoRoot, stateRoot, "sessions", `${taskSlug}.json`);
|
|
281
287
|
}
|
|
288
|
+
function normalizeClaudePermissionMode(value) {
|
|
289
|
+
if (value === "bypassPermissions" || value === "dangerously-skip-permissions") {
|
|
290
|
+
return "bypassPermissions";
|
|
291
|
+
}
|
|
292
|
+
return "default";
|
|
293
|
+
}
|