vibe-coding-master 0.2.0 → 0.2.2

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.
@@ -1,79 +1,331 @@
1
- import { ROLE_NAMES } from "../../shared/constants.js";
2
- export function createRoundService(deps = {}) {
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
- return {
5
- getTaskRoundState(input) {
6
- const roleStates = ROLE_NAMES.map((role) => toRoleTurnState(role, input.sessions));
7
- const state = evaluateTaskRoundState({
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
- pendingRouteCount: input.pendingRouteCount,
10
- roleStates,
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
- export function evaluateTaskRoundState(input) {
23
- const activeRole = input.roleStates.find((roleState) => roleState.status === "answering" ||
24
- roleState.status === "using_tools" ||
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
- status: activeRole.status === "waiting_user" ? "waiting_user" : "active",
31
- activeRole: activeRole.role,
32
- roles: input.roleStates,
33
- updatedAt: input.updatedAt
204
+ updatedAt: input.timestamp
34
205
  };
35
206
  }
36
- const latestIdleRole = getLatestIdleRole(input.roleStates);
37
- const hasPendingRoutes = input.pendingRouteCount > 0;
38
- if (latestIdleRole?.lastAnswerEndedAt && !hasPendingRoutes) {
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: input.taskSlug,
41
- status: "completed",
42
- activeRole: latestIdleRole.role,
43
- completionId: `direct:${latestIdleRole.role}:${latestIdleRole.lastAnswerEndedAt}`,
44
- completedAt: latestIdleRole.lastAnswerEndedAt,
45
- roles: input.roleStates,
46
- updatedAt: input.updatedAt
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: input.taskSlug,
51
- status: hasPendingRoutes ? "active" : "idle",
52
- activeRole: latestIdleRole?.role,
53
- roles: input.roleStates,
54
- updatedAt: input.updatedAt
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 toRoleTurnState(role, sessions) {
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
- role,
62
- sessionId: session?.id,
63
- status: session ? activityStatus === "running" ? "answering" : "idle" : "unknown",
64
- pendingToolUseCount: 0,
65
- lastActivityAt: session?.lastPromptSubmittedAt ?? session?.lastHookEventAt,
66
- lastAnswerEndedAt: session?.lastStopAt,
67
- reason: session
68
- ? activityStatus === "running"
69
- ? "Claude Code accepted a prompt and has not emitted Stop yet."
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 getLatestIdleRole(roleStates) {
75
- return roleStates
76
- .filter((roleState) => roleState.status === "idle" && roleState.lastAnswerEndedAt)
77
- .sort((left, right) => (left.lastAnswerEndedAt ?? "").localeCompare(right.lastAnswerEndedAt ?? ""))
78
- .at(-1);
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 ?? "default";
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
- return current.roles[role]?.record;
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
+ }