vibe-coding-master 0.7.47 → 0.7.48

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.
@@ -297,6 +297,7 @@ export function createDefaultServerDeps(options = {}) {
297
297
  const architectRestartService = createArchitectRestartService({
298
298
  fs,
299
299
  taskService,
300
+ projectService,
300
301
  sessionService,
301
302
  appSettings
302
303
  });
@@ -399,7 +400,8 @@ export function createDefaultServerDeps(options = {}) {
399
400
  runtime,
400
401
  projectService,
401
402
  taskService,
402
- translationWorkerService
403
+ translationWorkerService,
404
+ architectRestartService
403
405
  });
404
406
  const claudeHookService = createClaudeHookService({
405
407
  projectService,
@@ -17,55 +17,61 @@ Before performing any assigned work, read:
17
17
  - the latest Gate Review report when present
18
18
 
19
19
  Treat the current artifacts and worktree as the source of truth. The architecture-plan Gate has accepted the current planning artifacts or VCM recorded an explicit Gate exception. Do not repeat the completed interview or planning work unless a later route explicitly reopens it.`;
20
+ const ARCHITECT_RESTART_STATE_FILE = "architect-restart.json";
20
21
  export function createArchitectRestartService(deps) {
21
22
  const pendingByTask = new Map();
23
+ const operationsByTask = new Map();
22
24
  return {
23
25
  async schedule(repoRoot, taskSlug) {
24
- const session = await requireRunningArchitect(repoRoot, taskSlug);
25
- await requireCompletePlan(repoRoot, taskSlug);
26
26
  const key = taskKey(repoRoot, taskSlug);
27
- const existing = pendingByTask.get(key);
28
- if (existing?.sessionId === session.id) {
29
- if (existing.status === "blocked") {
30
- existing.status = "pending";
31
- existing.blocker = undefined;
32
- await tryRestart(existing);
27
+ return withTaskLock(key, async () => {
28
+ const session = await requireRunningArchitect(repoRoot, taskSlug);
29
+ await requireCompletePlan(repoRoot, taskSlug);
30
+ const existing = pendingByTask.get(key);
31
+ if (existing?.status === "executing") {
33
32
  return {
34
33
  taskSlug,
35
- sessionId: session.id,
36
- status: "scheduled",
34
+ sessionId: existing.replacementSessionId ?? existing.sourceSessionId,
35
+ status: "already_scheduled",
37
36
  ...(existing.memoryCandidatePath
38
37
  ? { memoryCandidatePath: existing.memoryCandidatePath }
39
38
  : {})
40
39
  };
41
40
  }
42
- return {
41
+ if (existing && isSourceSession(existing, session)) {
42
+ if (existing.status === "blocked") {
43
+ existing.status = "pending";
44
+ existing.blocker = undefined;
45
+ existing.sourceSessionId = session.id;
46
+ existing.updatedAt = timestamp();
47
+ await persistPending(existing);
48
+ await tryRestart(existing);
49
+ return scheduleResult(existing, "scheduled");
50
+ }
51
+ return scheduleResult(existing, "already_scheduled");
52
+ }
53
+ const memoryCandidatePath = (await deps.appSettings.getPreferences()).autoMemoryEnabled
54
+ ? ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH
55
+ : undefined;
56
+ const pending = {
57
+ version: 1,
58
+ repoRoot,
43
59
  taskSlug,
44
- sessionId: session.id,
45
- status: "already_scheduled",
46
- ...(existing.memoryCandidatePath
47
- ? { memoryCandidatePath: existing.memoryCandidatePath }
48
- : {})
60
+ sourceSessionId: session.id,
61
+ ...(session.claudeSessionId ? { sourceClaudeSessionId: session.claudeSessionId } : {}),
62
+ stopped: false,
63
+ gateAccepted: false,
64
+ status: "pending",
65
+ permissionMode: session.permissionMode,
66
+ model: session.model,
67
+ effort: session.effort,
68
+ memoryCandidatePath: existing?.memoryCandidatePath ?? memoryCandidatePath,
69
+ updatedAt: timestamp()
49
70
  };
50
- }
51
- const memoryCandidatePath = (await deps.appSettings.getPreferences()).autoMemoryEnabled
52
- ? ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH
53
- : undefined;
54
- pendingByTask.set(key, {
55
- repoRoot,
56
- taskSlug,
57
- sessionId: session.id,
58
- stopped: false,
59
- gateAccepted: false,
60
- status: "pending",
61
- memoryCandidatePath
71
+ pendingByTask.set(key, pending);
72
+ await persistPending(pending);
73
+ return scheduleResult(pending, "scheduled");
62
74
  });
63
- return {
64
- taskSlug,
65
- sessionId: session.id,
66
- status: "scheduled",
67
- ...(memoryCandidatePath ? { memoryCandidatePath } : {})
68
- };
69
75
  },
70
76
  getState(repoRoot, taskSlug) {
71
77
  const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
@@ -74,7 +80,7 @@ export function createArchitectRestartService(deps) {
74
80
  }
75
81
  return {
76
82
  taskSlug: pending.taskSlug,
77
- sessionId: pending.sessionId,
83
+ sessionId: pending.replacementSessionId ?? pending.sourceSessionId,
78
84
  status: pending.status,
79
85
  ...(pending.memoryCandidatePath
80
86
  ? { memoryCandidatePath: pending.memoryCandidatePath }
@@ -82,46 +88,128 @@ export function createArchitectRestartService(deps) {
82
88
  ...(pending.blocker ? { blocker: { ...pending.blocker } } : {})
83
89
  };
84
90
  },
91
+ async recoverTask(repoRoot, taskSlug) {
92
+ const key = taskKey(repoRoot, taskSlug);
93
+ await withTaskLock(key, async () => {
94
+ const stored = await loadPending(repoRoot, taskSlug);
95
+ if (!stored) {
96
+ pendingByTask.delete(key);
97
+ return;
98
+ }
99
+ const pending = { ...stored, repoRoot };
100
+ pendingByTask.set(key, pending);
101
+ const session = await deps.sessionService.getRoleSession(repoRoot, taskSlug, ARCHITECT_ROLE);
102
+ if (pending.status === "executing") {
103
+ if (isConfirmedReplacement(pending, session)) {
104
+ await removePending(pending);
105
+ return;
106
+ }
107
+ if (session?.status === "running"
108
+ && pending.replacementSessionId === session.id) {
109
+ return;
110
+ }
111
+ await launchReplacement(pending);
112
+ return;
113
+ }
114
+ if (pending.status === "pending" && restartPrerequisitesMet(pending)) {
115
+ if (session?.status === "running") {
116
+ await tryRestart(pending);
117
+ }
118
+ else {
119
+ await launchReplacement(pending);
120
+ }
121
+ }
122
+ });
123
+ },
85
124
  async recordArchitectStop(repoRoot, taskSlug, sessionId) {
86
- const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
87
- if (!pending || pending.sessionId !== sessionId || pending.status === "blocked") {
88
- return;
89
- }
90
- pending.stopped = true;
91
- await tryRestart(pending);
125
+ const key = taskKey(repoRoot, taskSlug);
126
+ await withTaskLock(key, async () => {
127
+ const pending = pendingByTask.get(key);
128
+ if (!pending || pending.status !== "pending") {
129
+ return;
130
+ }
131
+ const session = await deps.sessionService.getRoleSession(repoRoot, taskSlug, ARCHITECT_ROLE);
132
+ if (!session || session.id !== sessionId || !isSourceSession(pending, session)) {
133
+ return;
134
+ }
135
+ pending.sourceSessionId = session.id;
136
+ pending.stopped = true;
137
+ pending.updatedAt = timestamp();
138
+ await persistPending(pending);
139
+ await tryRestart(pending);
140
+ });
92
141
  },
93
142
  async recordRouteDelivered(repoRoot, taskSlug, message) {
94
143
  if (!isArchitectToPm(message)) {
95
144
  return;
96
145
  }
97
- const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
98
- if (!pending || pending.status === "blocked") {
99
- return;
100
- }
101
- pending.deliveredMessageId = message.id;
102
- await tryRestart(pending);
146
+ const key = taskKey(repoRoot, taskSlug);
147
+ await withTaskLock(key, async () => {
148
+ const pending = pendingByTask.get(key);
149
+ if (!pending || pending.status !== "pending") {
150
+ return;
151
+ }
152
+ pending.deliveredMessageId = message.id;
153
+ pending.updatedAt = timestamp();
154
+ await persistPending(pending);
155
+ await tryRestart(pending);
156
+ });
103
157
  },
104
158
  async recordRouteAccepted(repoRoot, taskSlug, message) {
105
159
  if (!isArchitectToPm(message)) {
106
160
  return;
107
161
  }
108
- const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
109
- if (!pending || pending.status === "blocked") {
110
- return;
111
- }
112
- pending.acceptedMessageId = message.id;
113
- await tryRestart(pending);
162
+ const key = taskKey(repoRoot, taskSlug);
163
+ await withTaskLock(key, async () => {
164
+ const pending = pendingByTask.get(key);
165
+ if (!pending || pending.status !== "pending") {
166
+ return;
167
+ }
168
+ pending.acceptedMessageId = message.id;
169
+ pending.updatedAt = timestamp();
170
+ await persistPending(pending);
171
+ await tryRestart(pending);
172
+ });
114
173
  },
115
174
  async recordArchitectureGateDisposition(repoRoot, taskSlug, accepted) {
116
- const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
117
- if (!pending || pending.status === "blocked") {
118
- return;
119
- }
120
- pending.gateAccepted = accepted;
121
- await tryRestart(pending);
175
+ const key = taskKey(repoRoot, taskSlug);
176
+ await withTaskLock(key, async () => {
177
+ const pending = pendingByTask.get(key);
178
+ if (!pending || pending.status !== "pending") {
179
+ return;
180
+ }
181
+ pending.gateAccepted = accepted;
182
+ pending.updatedAt = timestamp();
183
+ await persistPending(pending);
184
+ await tryRestart(pending);
185
+ });
186
+ },
187
+ async recordReplacementPromptSubmitted(repoRoot, taskSlug, sessionId) {
188
+ const key = taskKey(repoRoot, taskSlug);
189
+ await withTaskLock(key, async () => {
190
+ const pending = pendingByTask.get(key);
191
+ if (!pending || pending.status !== "executing") {
192
+ return;
193
+ }
194
+ const session = await deps.sessionService.getRoleSession(repoRoot, taskSlug, ARCHITECT_ROLE);
195
+ if (!session
196
+ || session.id !== sessionId
197
+ || !session.claudeSessionId
198
+ || (pending.replacementSessionId && pending.replacementSessionId !== session.id)) {
199
+ return;
200
+ }
201
+ await removePending(pending);
202
+ });
122
203
  },
123
- clear(repoRoot, taskSlug) {
124
- pendingByTask.delete(taskKey(repoRoot, taskSlug));
204
+ async clear(repoRoot, taskSlug) {
205
+ const key = taskKey(repoRoot, taskSlug);
206
+ await withTaskLock(key, async () => {
207
+ const pending = pendingByTask.get(key) ?? await loadPending(repoRoot, taskSlug);
208
+ pendingByTask.delete(key);
209
+ if (pending) {
210
+ await removePending({ ...pending, repoRoot });
211
+ }
212
+ });
125
213
  }
126
214
  };
127
215
  async function requireRunningArchitect(repoRoot, taskSlug) {
@@ -147,17 +235,12 @@ export function createArchitectRestartService(deps) {
147
235
  }
148
236
  }
149
237
  async function tryRestart(pending) {
150
- if (pending.status !== "pending"
151
- || !pending.stopped
152
- || !pending.deliveredMessageId
153
- || pending.deliveredMessageId !== pending.acceptedMessageId
154
- || !pending.gateAccepted) {
238
+ if (pending.status !== "pending" || !restartPrerequisitesMet(pending)) {
155
239
  return;
156
240
  }
157
241
  const session = await deps.sessionService.getRoleSession(pending.repoRoot, pending.taskSlug, ARCHITECT_ROLE);
158
- if (!session
159
- || session.id !== pending.sessionId) {
160
- blockPending(pending, new VcmError({
242
+ if (!session || !isSourceSession(pending, session)) {
243
+ await blockPending(pending, new VcmError({
161
244
  code: "ARCHITECT_RESTART_SESSION_UNAVAILABLE",
162
245
  message: "Architect restart is blocked because the scheduled Architect session no longer exists.",
163
246
  statusCode: 409
@@ -165,7 +248,7 @@ export function createArchitectRestartService(deps) {
165
248
  return;
166
249
  }
167
250
  if (session.status !== "running") {
168
- blockPending(pending, new VcmError({
251
+ await blockPending(pending, new VcmError({
169
252
  code: "ARCHITECT_RESTART_SESSION_NOT_RUNNING",
170
253
  message: "Architect restart is blocked because the scheduled Architect session is not running.",
171
254
  statusCode: 409
@@ -175,31 +258,121 @@ export function createArchitectRestartService(deps) {
175
258
  if (session.activityStatus !== "idle") {
176
259
  return;
177
260
  }
261
+ pending.sourceSessionId = session.id;
262
+ await launchReplacement(pending);
263
+ }
264
+ async function launchReplacement(pending) {
178
265
  pending.status = "executing";
266
+ pending.blocker = undefined;
267
+ pending.updatedAt = timestamp();
268
+ await persistPending(pending);
179
269
  try {
180
270
  await requireCompletePlan(pending.repoRoot, pending.taskSlug);
181
271
  await requirePlanningMemoryCandidate(pending);
182
- await deps.sessionService.restartRoleSession(pending.repoRoot, pending.taskSlug, ARCHITECT_ROLE, {
183
- permissionMode: session.permissionMode,
184
- model: session.model,
185
- effort: session.effort,
272
+ const replacement = await deps.sessionService.restartRoleSession(pending.repoRoot, pending.taskSlug, ARCHITECT_ROLE, {
273
+ permissionMode: pending.permissionMode,
274
+ model: pending.model,
275
+ effort: pending.effort,
186
276
  appendSystemPrompt: ARCHITECT_RESTORE_PROMPT
187
277
  });
188
- pendingByTask.delete(taskKey(pending.repoRoot, pending.taskSlug));
278
+ pending.replacementSessionId = replacement.id;
279
+ pending.updatedAt = timestamp();
280
+ await persistPending(pending);
189
281
  }
190
282
  catch (error) {
191
- blockPending(pending, error);
283
+ await blockPending(pending, error);
192
284
  }
193
285
  }
194
- function blockPending(pending, error) {
286
+ async function blockPending(pending, error) {
195
287
  const normalized = toVcmError(error);
196
288
  pending.status = "blocked";
197
289
  pending.blocker = {
198
290
  code: normalized.code,
199
291
  message: normalized.message,
200
- blockedAt: new Date().toISOString()
292
+ blockedAt: timestamp()
293
+ };
294
+ pending.updatedAt = timestamp();
295
+ await persistPending(pending);
296
+ }
297
+ function restartPrerequisitesMet(pending) {
298
+ return Boolean(pending.stopped
299
+ && pending.deliveredMessageId
300
+ && pending.deliveredMessageId === pending.acceptedMessageId
301
+ && pending.gateAccepted);
302
+ }
303
+ function isSourceSession(pending, session) {
304
+ return session.id === pending.sourceSessionId
305
+ || Boolean(pending.sourceClaudeSessionId
306
+ && session.claudeSessionId
307
+ && pending.sourceClaudeSessionId === session.claudeSessionId);
308
+ }
309
+ function isConfirmedReplacement(pending, session) {
310
+ return Boolean(session?.claudeSessionId
311
+ && ((pending.replacementSessionId && session.id === pending.replacementSessionId)
312
+ || !pending.sourceClaudeSessionId
313
+ || session.claudeSessionId !== pending.sourceClaudeSessionId));
314
+ }
315
+ function scheduleResult(pending, status) {
316
+ return {
317
+ taskSlug: pending.taskSlug,
318
+ sessionId: pending.replacementSessionId ?? pending.sourceSessionId,
319
+ status,
320
+ ...(pending.memoryCandidatePath ? { memoryCandidatePath: pending.memoryCandidatePath } : {})
201
321
  };
202
322
  }
323
+ async function persistPending(pending) {
324
+ const { repoRoot: _repoRoot, ...stored } = pending;
325
+ await deps.fs.writeJsonAtomic(await statePath(pending.repoRoot, pending.taskSlug), stored);
326
+ pendingByTask.set(taskKey(pending.repoRoot, pending.taskSlug), pending);
327
+ }
328
+ async function loadPending(repoRoot, taskSlug) {
329
+ const target = await statePath(repoRoot, taskSlug);
330
+ if (!(await deps.fs.pathExists(target))) {
331
+ return undefined;
332
+ }
333
+ const stored = await deps.fs.readJson(target);
334
+ if (stored.version !== 1
335
+ || stored.taskSlug !== taskSlug
336
+ || !stored.sourceSessionId
337
+ || !["pending", "executing", "blocked"].includes(stored.status)) {
338
+ throw new VcmError({
339
+ code: "ARCHITECT_RESTART_STATE_INVALID",
340
+ message: `Architect restart state is invalid for task ${taskSlug}.`,
341
+ statusCode: 500
342
+ });
343
+ }
344
+ return stored;
345
+ }
346
+ async function removePending(pending) {
347
+ pendingByTask.delete(taskKey(pending.repoRoot, pending.taskSlug));
348
+ const target = await statePath(pending.repoRoot, pending.taskSlug);
349
+ if (await deps.fs.pathExists(target)) {
350
+ await deps.fs.removePath?.(target, { force: true });
351
+ }
352
+ }
353
+ async function statePath(repoRoot, taskSlug) {
354
+ const [config, task] = await Promise.all([
355
+ deps.projectService.loadConfig(repoRoot),
356
+ deps.taskService.loadTask(repoRoot, taskSlug)
357
+ ]);
358
+ return resolveRepoPath(getTaskRuntimeRepoRoot(task), path.posix.join(config.stateRoot, ARCHITECT_RESTART_STATE_FILE));
359
+ }
360
+ async function withTaskLock(key, run) {
361
+ const previous = operationsByTask.get(key) ?? Promise.resolve();
362
+ const current = previous.catch(() => undefined).then(run);
363
+ operationsByTask.set(key, current);
364
+ try {
365
+ return await current;
366
+ }
367
+ finally {
368
+ if (operationsByTask.get(key) === current) {
369
+ operationsByTask.delete(key);
370
+ }
371
+ }
372
+ }
373
+ function timestamp() {
374
+ return new Date().toISOString();
375
+ }
203
376
  async function requirePlanningMemoryCandidate(pending) {
204
377
  if (!pending.memoryCandidatePath) {
205
378
  return;