vibe-coding-master 0.4.42 → 0.5.1

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 CHANGED
@@ -655,7 +655,7 @@ For every task, `taskRepoRoot` is `<baseRepoRoot>/.claude/worktrees/<task>`.
655
655
  The npm package publishes built output, not raw TypeScript entry files. `package.json` includes:
656
656
 
657
657
  - `bin.vcm`: `dist/main.js`
658
- - `files`: `dist`, `dist-frontend`, `docs`, `scripts`, `README.md`
658
+ - `files`: `dist`, `dist-frontend`, `scripts`, `README.md`
659
659
  - `prepack`: `npm run build && npm run verify:package`
660
660
 
661
661
  Use this before publishing:
@@ -83,6 +83,13 @@ export function registerTaskRoutes(app, deps) {
83
83
  throw error;
84
84
  }
85
85
  });
86
+ app.post("/api/tasks/:taskSlug/one-click-start", async (request) => {
87
+ const project = await requireCurrentProject(deps.projectService);
88
+ return deps.taskLaunchService.startTaskRoleSessions(project.repoRoot, {
89
+ taskSlug: request.params.taskSlug,
90
+ requireFreshStart: true
91
+ });
92
+ });
86
93
  app.post("/api/tasks/:taskSlug/cleanup", async (request) => {
87
94
  const project = await requireCurrentProject(deps.projectService);
88
95
  const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
@@ -2,12 +2,14 @@ export class VcmError extends Error {
2
2
  code;
3
3
  statusCode;
4
4
  hint;
5
+ details;
5
6
  constructor(input) {
6
7
  super(input.message);
7
8
  this.name = "VcmError";
8
9
  this.code = input.code;
9
10
  this.statusCode = input.statusCode ?? 400;
10
11
  this.hint = input.hint;
12
+ this.details = input.details;
11
13
  }
12
14
  }
13
15
  export function toVcmError(error) {
@@ -1,5 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
- import { CORE_VCM_ROLE_DEFINITIONS, GATE_REVIEWER_ROLE_DEFINITION, VCM_ROLE_NAMES } from "../../shared/constants.js";
2
+ import { VCM_ROLE_NAMES } from "../../shared/constants.js";
3
3
  import { VcmError } from "../errors.js";
4
4
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
5
5
  import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
@@ -430,78 +430,48 @@ export function createGatewayService(deps) {
430
430
  taskSlug,
431
431
  title
432
432
  });
433
- const config = await deps.projectService.loadConfig(project.repoRoot);
434
- const taskRepoRoot = getTaskRuntimeRepoRoot(task);
435
433
  const preferences = await deps.appSettings.getPreferences();
436
- const template = preferences.launchTemplate;
437
- await deps.messageService.updateOrchestrationState({
438
- repoRoot: project.repoRoot,
439
- stateRepoRoot: taskRepoRoot,
440
- stateRoot: config.stateRoot,
441
- taskSlug: task.taskSlug,
442
- mode: template.autoOrchestration ? "auto" : "manual"
443
- });
444
- const gateReviewSettings = await deps.appSettings.getGateReviewSettings(project.repoRoot, task.taskSlug);
445
- const roleDefinitions = [
446
- ...CORE_VCM_ROLE_DEFINITIONS,
447
- ...(gateReviewSettings.enabled ? [GATE_REVIEWER_ROLE_DEFINITION] : [])
448
- ];
449
- const startedRoles = [];
450
- for (const definition of roleDefinitions) {
451
- const roleTemplate = template.roles[definition.name];
452
- try {
453
- const sessionInput = {
454
- cols: 100,
455
- rows: 28,
456
- permissionMode: roleTemplate.permissionMode,
457
- model: roleTemplate.model,
458
- effort: roleTemplate.effort
459
- };
460
- const existing = await deps.sessionService.getRoleSession(project.repoRoot, task.taskSlug, definition.name);
461
- if (existing?.status === "running") {
462
- startedRoles.push(definition.name);
463
- continue;
464
- }
465
- if (existing?.claudeSessionId) {
466
- await deps.sessionService.resumeRoleSession(project.repoRoot, task.taskSlug, definition.name, sessionInput);
467
- }
468
- else {
469
- await deps.sessionService.startRoleSession(project.repoRoot, task.taskSlug, definition.name, sessionInput);
470
- }
471
- startedRoles.push(definition.name);
472
- }
473
- catch (error) {
474
- const settings = await deps.settings.loadSettings();
475
- await deps.settings.saveSettings({
476
- ...settings,
477
- currentProjectId: project.repoRoot,
478
- currentTaskSlug: task.taskSlug,
479
- translationEnabled: preferences.translationEnabled,
480
- updatedAt: now()
481
- });
434
+ // Persist the gateway's current project/task selection. Done after a partial
435
+ // start (so the phone stays pointed at the new task) and on success.
436
+ const persistTaskSelection = async () => {
437
+ const settings = await deps.settings.loadSettings();
438
+ await deps.settings.saveSettings({
439
+ ...settings,
440
+ currentProjectId: project.repoRoot,
441
+ currentTaskSlug: task.taskSlug,
442
+ translationEnabled: preferences.translationEnabled,
443
+ updatedAt: now()
444
+ });
445
+ };
446
+ // Reuse the shared backend launch orchestration (roster + mode + skip/resume/
447
+ // start). A freshly created task has no sessions, so requireFreshStart is false.
448
+ let launch;
449
+ try {
450
+ launch = await deps.taskLaunchService.startTaskRoleSessions(project.repoRoot, {
451
+ taskSlug: task.taskSlug,
452
+ requireFreshStart: false
453
+ });
454
+ }
455
+ catch (error) {
456
+ if (error instanceof VcmError && error.code === "TASK_ONE_CLICK_PARTIAL_START") {
457
+ await persistTaskSelection();
482
458
  throw new VcmError({
483
459
  code: "GATEWAY_TASK_PARTIAL_START",
484
- message: `Task was created, but ${definition.name} failed to start.`,
460
+ message: `Task was created, but ${error.message}`,
485
461
  statusCode: 409,
486
- hint: errorMessage(error)
462
+ hint: error.hint
487
463
  });
488
464
  }
465
+ throw error;
489
466
  }
490
- const settings = await deps.settings.loadSettings();
491
- await deps.settings.saveSettings({
492
- ...settings,
493
- currentProjectId: project.repoRoot,
494
- currentTaskSlug: task.taskSlug,
495
- translationEnabled: preferences.translationEnabled,
496
- updatedAt: now()
497
- });
467
+ await persistTaskSelection();
498
468
  return [
499
469
  `Task created and initialized: ${task.taskSlug}`,
500
470
  `branch: ${task.branch}`,
501
471
  `worktree: ${task.worktreePath}`,
502
- `orchestration: ${template.autoOrchestration ? "auto" : "manual"}`,
472
+ `orchestration: ${launch.orchestration.mode}`,
503
473
  `translation: ${preferences.translationEnabled ? "on" : "off"}`,
504
- `sessions: ${startedRoles.join(", ")}`
474
+ `sessions: ${launch.startedRoles.join(", ")}`
505
475
  ].join("\n");
506
476
  }
507
477
  async function closeTaskPrompt() {
@@ -33,6 +33,7 @@ import { createRoundService } from "./services/round-service.js";
33
33
  import { createRuntimeCoordinatorService } from "./services/runtime-coordinator-service.js";
34
34
  import { createStatusService } from "./services/status-service.js";
35
35
  import { createTaskService } from "./services/task-service.js";
36
+ import { createTaskLaunchService } from "./services/task-launch-service.js";
36
37
  import { createTranslationService } from "./services/translation-service.js";
37
38
  import { createDiagnosticsService } from "./services/diagnostics-service.js";
38
39
  import { registerAppSettingsRoutes } from "./api/app-settings-routes.js";
@@ -109,6 +110,7 @@ export async function createServer(deps, options = {}) {
109
110
  sessionService: deps.sessionService,
110
111
  statusService: deps.statusService,
111
112
  messageService: deps.messageService,
113
+ taskLaunchService: deps.taskLaunchService,
112
114
  translationService: deps.translationService,
113
115
  roundService: deps.roundService
114
116
  });
@@ -232,6 +234,13 @@ export function createDefaultServerDeps(options = {}) {
232
234
  sessionService,
233
235
  taskService
234
236
  });
237
+ const taskLaunchService = createTaskLaunchService({
238
+ projectService,
239
+ taskService,
240
+ appSettings,
241
+ sessionService,
242
+ messageService
243
+ });
235
244
  const roundService = createRoundService({
236
245
  fs,
237
246
  sessionService,
@@ -286,7 +295,7 @@ export function createDefaultServerDeps(options = {}) {
286
295
  projectService,
287
296
  taskService,
288
297
  sessionService,
289
- messageService,
298
+ taskLaunchService,
290
299
  translationService,
291
300
  roundService,
292
301
  runtime,
@@ -337,6 +346,7 @@ export function createDefaultServerDeps(options = {}) {
337
346
  commandDispatcher,
338
347
  claudeHookService,
339
348
  messageService,
349
+ taskLaunchService,
340
350
  gateReviewService,
341
351
  translationWorkerService,
342
352
  roundService,
@@ -383,6 +383,7 @@ function toSessionRoundState(state, updatedAt) {
383
383
  totalCcActiveMs: state.totalCcActiveMs,
384
384
  currentRoundCcActiveMs: 0,
385
385
  roleRecovery: state.roleRecovery,
386
+ flowPause: computeFlowPause(undefined, state.roleRecovery),
386
387
  roles: [],
387
388
  updatedAt
388
389
  };
@@ -411,10 +412,43 @@ function toSessionRoundState(state, updatedAt) {
411
412
  totalCcActiveMs: state.totalCcActiveMs + activeDurationMs,
412
413
  currentRoundCcActiveMs,
413
414
  roleRecovery: state.roleRecovery,
415
+ flowPause: computeFlowPause(current, state.roleRecovery),
414
416
  roles: current.roles,
415
417
  updatedAt
416
418
  };
417
419
  }
420
+ /**
421
+ * Authoritative flow-pause predicate (single source of truth). Returns a paused
422
+ * VcmFlowPauseState exactly when a real round has ended and the auto flow has not
423
+ * advanced and we are not mid active-recovery — mirroring the decision the GUI
424
+ * previously derived. Reason is `role-recovery-failed` when recovery has failed,
425
+ * otherwise `stopped-no-next-turn`. Returns a non-paused state (or undefined) when
426
+ * the flow is not paused. The frontend consumes this instead of re-deriving it.
427
+ *
428
+ * Intended logic (to implement):
429
+ * const recovering = roleRecovery?.status === "waiting" || roleRecovery?.status === "retrying";
430
+ * const paused = !!current && current.status === "stopped" && !!current.id && !recovering;
431
+ * if (!paused) return undefined; // or { paused: false }
432
+ * return {
433
+ * paused: true,
434
+ * reason: roleRecovery?.status === "failed" ? "role-recovery-failed" : "stopped-no-next-turn",
435
+ * role: current.activeRole,
436
+ * since: current.stoppedAt ?? current.lastTurnEndedAt
437
+ * };
438
+ */
439
+ function computeFlowPause(current, roleRecovery) {
440
+ const recovering = roleRecovery?.status === "waiting" || roleRecovery?.status === "retrying";
441
+ const paused = Boolean(current) && current.status === "stopped" && Boolean(current.id) && !recovering;
442
+ if (!paused) {
443
+ return undefined;
444
+ }
445
+ return {
446
+ paused: true,
447
+ reason: roleRecovery?.status === "failed" ? "role-recovery-failed" : "stopped-no-next-turn",
448
+ role: current.activeRole,
449
+ since: current.stoppedAt ?? current.lastTurnEndedAt
450
+ };
451
+ }
418
452
  function normalizeRoundFile(input, taskSlug, updatedAt) {
419
453
  const legacy = input;
420
454
  return {
@@ -0,0 +1,91 @@
1
+ import { CORE_VCM_ROLE_DEFINITIONS, GATE_REVIEWER_ROLE_DEFINITION, VCM_ROLE_NAMES } from "../../shared/constants.js";
2
+ import { VcmError } from "../errors.js";
3
+ import { getTaskRuntimeRepoRoot } from "./task-service.js";
4
+ const ONE_CLICK_SESSION_COLS = 100;
5
+ const ONE_CLICK_SESSION_ROWS = 28;
6
+ export function createTaskLaunchService(deps) {
7
+ async function assertNoExistingRoleSessions(repoRoot, taskSlug) {
8
+ const sessions = await deps.sessionService.listRoleSessions(repoRoot, taskSlug);
9
+ if (sessions.some((session) => VCM_ROLE_NAMES.some((role) => role === session.role))) {
10
+ throw new VcmError({
11
+ code: "TASK_ONE_CLICK_REQUIRES_FRESH_START",
12
+ message: "One-click start is only available before any role session has started.",
13
+ statusCode: 409
14
+ });
15
+ }
16
+ }
17
+ async function applyOrchestrationMode(repoRoot, taskSlug, mode) {
18
+ const config = await deps.projectService.loadConfig(repoRoot);
19
+ const task = await deps.taskService.loadTask(repoRoot, taskSlug);
20
+ return deps.messageService.updateOrchestrationState({
21
+ repoRoot,
22
+ stateRepoRoot: getTaskRuntimeRepoRoot(task),
23
+ stateRoot: config.stateRoot,
24
+ taskSlug,
25
+ mode
26
+ });
27
+ }
28
+ function composeRoleDefinitions(gateReviewerEnabled) {
29
+ return [
30
+ ...CORE_VCM_ROLE_DEFINITIONS,
31
+ ...(gateReviewerEnabled ? [GATE_REVIEWER_ROLE_DEFINITION] : [])
32
+ ];
33
+ }
34
+ // Skip a running role, resume one that has a prior Claude session, otherwise
35
+ // start it fresh — using the launch-template entry's permission/model/effort.
36
+ async function launchRole(repoRoot, taskSlug, role, roleTemplate) {
37
+ const sessionInput = {
38
+ cols: ONE_CLICK_SESSION_COLS,
39
+ rows: ONE_CLICK_SESSION_ROWS,
40
+ permissionMode: roleTemplate.permissionMode,
41
+ model: roleTemplate.model,
42
+ effort: roleTemplate.effort
43
+ };
44
+ const existing = await deps.sessionService.getRoleSession(repoRoot, taskSlug, role);
45
+ if (existing?.status === "running") {
46
+ return;
47
+ }
48
+ if (existing?.claudeSessionId) {
49
+ await deps.sessionService.resumeRoleSession(repoRoot, taskSlug, role, sessionInput);
50
+ return;
51
+ }
52
+ await deps.sessionService.startRoleSession(repoRoot, taskSlug, role, sessionInput);
53
+ }
54
+ return {
55
+ async startTaskRoleSessions(repoRoot, input) {
56
+ const { taskSlug, requireFreshStart } = input;
57
+ if (requireFreshStart) {
58
+ await assertNoExistingRoleSessions(repoRoot, taskSlug);
59
+ }
60
+ const preferences = await deps.appSettings.getPreferences();
61
+ const template = preferences.launchTemplate;
62
+ const orchestration = await applyOrchestrationMode(repoRoot, taskSlug, template.autoOrchestration ? "auto" : "manual");
63
+ const gateReview = await deps.appSettings.getGateReviewSettings(repoRoot, taskSlug);
64
+ const roleDefinitions = composeRoleDefinitions(gateReview.enabled);
65
+ const startedRoles = [];
66
+ for (const definition of roleDefinitions) {
67
+ try {
68
+ await launchRole(repoRoot, taskSlug, definition.name, template.roles[definition.name]);
69
+ startedRoles.push(definition.name);
70
+ }
71
+ catch (cause) {
72
+ throw partialStartError(definition.name, startedRoles, cause);
73
+ }
74
+ }
75
+ const sessions = await deps.sessionService.listRoleSessions(repoRoot, taskSlug);
76
+ return { taskSlug, orchestration, startedRoles, sessions };
77
+ }
78
+ };
79
+ }
80
+ function partialStartError(failedRole, startedRoles, cause) {
81
+ return new VcmError({
82
+ code: "TASK_ONE_CLICK_PARTIAL_START",
83
+ message: `${failedRole} failed to start.`,
84
+ statusCode: 409,
85
+ hint: errorMessage(cause),
86
+ details: { startedRoles: [...startedRoles], failedRole }
87
+ });
88
+ }
89
+ function errorMessage(error) {
90
+ return error instanceof Error ? error.message : String(error);
91
+ }