vibe-coding-master 0.3.22 → 0.3.24

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.
Files changed (35) hide show
  1. package/README.md +2 -2
  2. package/dist/backend/adapters/git-adapter.js +53 -0
  3. package/dist/backend/api/artifact-routes.js +1 -28
  4. package/dist/backend/api/codex-translation-routes.js +24 -0
  5. package/dist/backend/api/harness-routes.js +10 -0
  6. package/dist/backend/api/task-routes.js +2 -6
  7. package/dist/backend/gateway/gateway-service.js +3 -16
  8. package/dist/backend/server.js +5 -2
  9. package/dist/backend/services/artifact-service.js +1 -30
  10. package/dist/backend/services/codex-hook-service.js +30 -9
  11. package/dist/backend/services/codex-translation-service.js +17 -40
  12. package/dist/backend/services/harness-service.js +106 -0
  13. package/dist/backend/services/session-service.js +189 -15
  14. package/dist/backend/services/status-service.js +1 -6
  15. package/dist/backend/services/task-service.js +42 -110
  16. package/dist/backend/services/translation-service.js +15 -4
  17. package/dist/backend/templates/handoff.js +3 -3
  18. package/dist/backend/templates/harness/architect-agent.js +3 -2
  19. package/dist/backend/templates/harness/coder-agent.js +6 -1
  20. package/dist/backend/templates/harness/project-manager-agent.js +10 -1
  21. package/dist/backend/templates/harness/vcm-route-message-skill.js +8 -2
  22. package/dist/shared/types/app-settings.js +3 -2
  23. package/dist-frontend/assets/index-CKWy15WL.js +94 -0
  24. package/dist-frontend/assets/index-Cfum1Prr.css +32 -0
  25. package/dist-frontend/index.html +2 -2
  26. package/docs/cc-best-practices.md +1 -1
  27. package/docs/codex-translation-plan.md +41 -37
  28. package/docs/full-harness-baseline.md +0 -1
  29. package/docs/gateway-design.md +9 -13
  30. package/docs/product-design.md +23 -44
  31. package/docs/v0.4-custom-workflow-plan.md +785 -0
  32. package/docs/vcm-cc-best-practices.md +15 -13
  33. package/package.json +1 -1
  34. package/dist-frontend/assets/index-CEB6Bssn.js +0 -95
  35. package/dist-frontend/assets/index-Dmefx9m7.css +0 -32
@@ -246,6 +246,73 @@ export function createHarnessService(deps) {
246
246
  : "VCM Harness updated. Review these files and commit the harness changes before starting long-running work."
247
247
  };
248
248
  },
249
+ async commitAndRebaseTask(repoRoot, input) {
250
+ if (!deps.git) {
251
+ throw new VcmError({
252
+ code: "HARNESS_GIT_UNAVAILABLE",
253
+ message: "Git-backed harness sync is not available in this VCM runtime.",
254
+ statusCode: 501
255
+ });
256
+ }
257
+ const changedFiles = dedupeHarnessChanges(input.changedFiles);
258
+ const changedPaths = changedFiles.map((change) => change.path);
259
+ const taskBranch = await deps.git.getCurrentBranch(input.worktreePath);
260
+ if (taskBranch !== input.branch) {
261
+ throw new VcmError({
262
+ code: "HARNESS_TASK_BRANCH_MISMATCH",
263
+ message: "The selected task worktree is not on its recorded task branch.",
264
+ statusCode: 409,
265
+ hint: `Expected ${input.branch}, found ${taskBranch}.`
266
+ });
267
+ }
268
+ const taskStatus = await deps.git.getStatusPorcelain(input.worktreePath);
269
+ if (taskStatus.trim()) {
270
+ throw new VcmError({
271
+ code: "HARNESS_TASK_DIRTY",
272
+ message: "The selected task worktree has uncommitted changes.",
273
+ statusCode: 409,
274
+ hint: "Commit or clean the task worktree before rebasing it onto the harness commit."
275
+ });
276
+ }
277
+ const preExistingStaged = await deps.git.getStagedStatus(repoRoot);
278
+ if (preExistingStaged.trim()) {
279
+ throw new VcmError({
280
+ code: "HARNESS_BASE_STAGED_CHANGES",
281
+ message: "The connected repository already has staged changes.",
282
+ statusCode: 409,
283
+ hint: "Commit, unstage, or clean existing staged changes before using Commit & rebase task."
284
+ });
285
+ }
286
+ const baseBranch = await deps.git.getCurrentBranch(repoRoot);
287
+ const baseCommitBefore = await deps.git.getHeadCommit(repoRoot);
288
+ let harnessCommit;
289
+ let committed = false;
290
+ if (changedPaths.length > 0) {
291
+ await deps.git.addPaths(repoRoot, changedPaths);
292
+ const stagedHarnessChanges = await deps.git.getStagedStatus(repoRoot);
293
+ if (stagedHarnessChanges.trim()) {
294
+ harnessCommit = await deps.git.commit(repoRoot, "chore: update VCM harness");
295
+ committed = true;
296
+ }
297
+ }
298
+ const baseCommitAfter = await deps.git.getHeadCommit(repoRoot);
299
+ await deps.git.rebase(input.worktreePath, baseCommitAfter);
300
+ return {
301
+ taskSlug: input.taskSlug,
302
+ branch: input.branch,
303
+ worktreePath: input.worktreePath,
304
+ baseBranch,
305
+ baseCommitBefore,
306
+ baseCommitAfter,
307
+ harnessCommit,
308
+ committed,
309
+ rebased: true,
310
+ changedFiles,
311
+ message: committed
312
+ ? `Committed VCM harness update ${shortCommit(baseCommitAfter)} on ${baseBranch} and rebased ${input.branch}.`
313
+ : `No new harness commit was needed; rebased ${input.branch} onto ${shortCommit(baseCommitAfter)}.`
314
+ };
315
+ },
249
316
  async getBootstrapStatus(repoRoot) {
250
317
  return getHarnessBootstrapStatus(deps, repoRoot, now);
251
318
  },
@@ -320,6 +387,45 @@ export function createHarnessService(deps) {
320
387
  }
321
388
  };
322
389
  }
390
+ function dedupeHarnessChanges(changedFiles) {
391
+ const seen = new Set();
392
+ const changes = [];
393
+ for (const change of changedFiles) {
394
+ const normalizedPath = normalizeHarnessGitPath(change.path);
395
+ if (seen.has(normalizedPath)) {
396
+ continue;
397
+ }
398
+ seen.add(normalizedPath);
399
+ changes.push({
400
+ ...change,
401
+ path: normalizedPath
402
+ });
403
+ }
404
+ return changes;
405
+ }
406
+ function normalizeHarnessGitPath(value) {
407
+ if (!value || value.includes("\0") || path.posix.isAbsolute(value)) {
408
+ throw new VcmError({
409
+ code: "HARNESS_CHANGED_PATH_INVALID",
410
+ message: "Harness changed file path is invalid.",
411
+ statusCode: 400,
412
+ hint: value
413
+ });
414
+ }
415
+ const normalized = path.posix.normalize(value).replace(/^\.\//, "");
416
+ if (normalized === "." || normalized.startsWith("../")) {
417
+ throw new VcmError({
418
+ code: "HARNESS_CHANGED_PATH_INVALID",
419
+ message: "Harness changed file path must stay inside the repository.",
420
+ statusCode: 400,
421
+ hint: value
422
+ });
423
+ }
424
+ return normalized;
425
+ }
426
+ function shortCommit(commit) {
427
+ return commit.slice(0, 7);
428
+ }
323
429
  async function analyzeHarnessFiles(fs, repoRoot) {
324
430
  const analyses = [];
325
431
  for (const definition of HARNESS_FILES) {
@@ -12,8 +12,9 @@ const CODEX_REVIEW_DIR = ".ai/vcm/codex-reviews";
12
12
  const CODEX_CONFIG_PATH = ".ai/codex/config.toml";
13
13
  const CODEX_TRANSLATOR_DIR = ".ai/codex-translator";
14
14
  const CODEX_TRANSLATION_DIR = ".ai/vcm/translations";
15
- const CODEX_TRANSLATOR_SESSION_PATH = ".ai/vcm/translations/runtime/session.json";
15
+ const CODEX_TRANSLATOR_SESSION_PATH = ".ai/vcm/translations/session.json";
16
16
  const CODEX_TRANSLATOR_CONFIG_PATH = ".ai/codex-translator/config.toml";
17
+ const PROJECT_TRANSLATOR_SCOPE = "__project__";
17
18
  export function createSessionService(deps) {
18
19
  const now = deps.now ?? (() => new Date().toISOString());
19
20
  async function launchRoleSession(repoRoot, taskSlug, role, input, launchMode) {
@@ -29,7 +30,6 @@ export function createSessionService(deps) {
29
30
  const isCodexRole = isCodexRoleName(role);
30
31
  const isTranslator = role === CODEX_TRANSLATOR_ROLE;
31
32
  const sessionRepoRoot = isTranslator ? repoRoot : taskRepoRoot;
32
- const sessionLogPath = isTranslator ? undefined : paths.roleLogPaths[role];
33
33
  const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
34
34
  const model = isCodexRole
35
35
  ? normalizeCodexModel(input.model ?? persisted?.model)
@@ -71,8 +71,7 @@ export function createSessionService(deps) {
71
71
  VCM_SESSION_ID: claudeSessionId
72
72
  },
73
73
  cols: input.cols,
74
- rows: input.rows,
75
- ...(sessionLogPath ? { logPath: resolveRepoPath(sessionRepoRoot, sessionLogPath) } : {})
74
+ rows: input.rows
76
75
  });
77
76
  const timestamp = now();
78
77
  const record = {
@@ -90,7 +89,6 @@ export function createSessionService(deps) {
90
89
  cwd: startCommand.cwd,
91
90
  terminalBackend: "node-pty",
92
91
  pid: runtimeSession.pid,
93
- ...(sessionLogPath ? { logPath: sessionLogPath } : {}),
94
92
  roleCommandPath: isDispatchableRole(role)
95
93
  ? paths.roleCommandPaths[role]
96
94
  : undefined,
@@ -104,14 +102,167 @@ export function createSessionService(deps) {
104
102
  await persistRoleSessionRecord(deps.fs, repoRoot, taskRepoRoot, config.stateRoot, record);
105
103
  return record;
106
104
  }
105
+ async function launchProjectTranslatorSession(repoRoot, input, launchMode) {
106
+ const live = toRoleSessionRecordView(getRegisteredProjectTranslatorSession(deps.registry, deps.runtime), deps.runtime);
107
+ if (live && live.status === "running") {
108
+ return live;
109
+ }
110
+ const persisted = await loadPersistedCodexTranslatorSession(deps.fs, repoRoot);
111
+ const model = normalizeCodexModel(input.model ?? persisted?.model);
112
+ const effort = normalizeCodexEffort(input.effort ?? persisted?.effort);
113
+ const claudeSessionId = launchMode === "resume"
114
+ ? persisted?.claudeSessionId
115
+ : randomUUID();
116
+ if (!claudeSessionId) {
117
+ throw new VcmError({
118
+ code: "CODEX_TRANSLATOR_SESSION_MISSING",
119
+ message: "Codex Translator does not have a session id to resume.",
120
+ statusCode: 409,
121
+ hint: "Start the translator once before using Resume."
122
+ });
123
+ }
124
+ const startCommand = await buildCodexStartCommand(deps.fs, repoRoot, repoRoot, CODEX_TRANSLATOR_ROLE, launchMode, model, effort, deps.sandboxMode, launchMode === "resume" && hasCapturedCodexSession(persisted) ? claudeSessionId : undefined);
125
+ const runtimeSession = await deps.runtime.createSession({
126
+ taskSlug: PROJECT_TRANSLATOR_SCOPE,
127
+ role: CODEX_TRANSLATOR_ROLE,
128
+ command: startCommand.command,
129
+ args: startCommand.args,
130
+ cwd: startCommand.cwd,
131
+ env: {
132
+ VCM_API_URL: deps.apiUrl,
133
+ VCM_TASK_REPO_ROOT: repoRoot,
134
+ VCM_TASK_SLUG: PROJECT_TRANSLATOR_SCOPE,
135
+ VCM_ROLE: CODEX_TRANSLATOR_ROLE,
136
+ VCM_SESSION_ID: claudeSessionId
137
+ },
138
+ cols: input.cols,
139
+ rows: input.rows
140
+ });
141
+ const timestamp = now();
142
+ const record = {
143
+ id: runtimeSession.id,
144
+ claudeSessionId,
145
+ taskSlug: PROJECT_TRANSLATOR_SCOPE,
146
+ role: CODEX_TRANSLATOR_ROLE,
147
+ status: runtimeSession.status,
148
+ activityStatus: "idle",
149
+ command: startCommand.display,
150
+ permissionMode: "default",
151
+ model,
152
+ effort,
153
+ cwd: startCommand.cwd,
154
+ terminalBackend: "node-pty",
155
+ pid: runtimeSession.pid,
156
+ startedAt: runtimeSession.startedAt,
157
+ updatedAt: timestamp,
158
+ lastOutputAt: runtimeSession.lastOutputAt,
159
+ exitCode: runtimeSession.exitCode
160
+ };
161
+ deps.registry.upsert(record);
162
+ await persistCodexTranslatorSession(deps.fs, repoRoot, record);
163
+ return record;
164
+ }
107
165
  return {
166
+ startProjectTranslatorSession(repoRoot, input = {}) {
167
+ return launchProjectTranslatorSession(repoRoot, input, "fresh");
168
+ },
169
+ resumeProjectTranslatorSession(repoRoot, input = {}) {
170
+ return launchProjectTranslatorSession(repoRoot, input, "resume");
171
+ },
172
+ async stopProjectTranslatorSession(repoRoot) {
173
+ const existing = await this.getProjectTranslatorSession(repoRoot);
174
+ if (!existing) {
175
+ throw new VcmError({
176
+ code: "SESSION_MISSING",
177
+ message: "Codex Translator session has not been started.",
178
+ statusCode: 404
179
+ });
180
+ }
181
+ if (deps.runtime.getSession(existing.id)) {
182
+ await deps.runtime.stop(existing.id);
183
+ }
184
+ const updated = {
185
+ ...existing,
186
+ status: "exited",
187
+ activityStatus: "idle",
188
+ updatedAt: now()
189
+ };
190
+ deps.registry.upsert(updated);
191
+ await persistCodexTranslatorSession(deps.fs, repoRoot, updated);
192
+ return updated;
193
+ },
194
+ async restartProjectTranslatorSession(repoRoot, input = {}) {
195
+ const existing = await this.getProjectTranslatorSession(repoRoot);
196
+ if (!existing) {
197
+ return launchProjectTranslatorSession(repoRoot, input, "fresh");
198
+ }
199
+ if (deps.runtime.getSession(existing.id)) {
200
+ await deps.runtime.stop(existing.id);
201
+ }
202
+ deps.registry.remove(existing.id);
203
+ return launchProjectTranslatorSession(repoRoot, input, "fresh");
204
+ },
205
+ async getProjectTranslatorSession(repoRoot) {
206
+ const record = getRegisteredProjectTranslatorSession(deps.registry, deps.runtime)
207
+ ?? await loadPersistedCodexTranslatorSession(deps.fs, repoRoot);
208
+ return toRoleSessionRecordView(record, deps.runtime);
209
+ },
210
+ async ensureProjectTranslatorSession(repoRoot, input = {}) {
211
+ const existing = await this.getProjectTranslatorSession(repoRoot);
212
+ if (existing?.status === "running") {
213
+ return existing;
214
+ }
215
+ if (existing?.claudeSessionId) {
216
+ return this.resumeProjectTranslatorSession(repoRoot, {
217
+ model: input.model ?? existing.model,
218
+ effort: input.effort ?? existing.effort,
219
+ cols: input.cols,
220
+ rows: input.rows
221
+ });
222
+ }
223
+ return this.startProjectTranslatorSession(repoRoot, input);
224
+ },
225
+ async recordProjectTranslatorHookEvent(repoRoot, input) {
226
+ const current = await this.getProjectTranslatorSession(repoRoot);
227
+ if (!current) {
228
+ return undefined;
229
+ }
230
+ const timestamp = now();
231
+ const isStop = input.eventName === "Stop";
232
+ const updated = {
233
+ ...current,
234
+ claudeSessionId: input.sessionId ?? current.claudeSessionId,
235
+ transcriptPath: input.transcriptPath ?? current.transcriptPath,
236
+ cwd: input.cwd ?? current.cwd,
237
+ activityStatus: isStop ? "idle" : "running",
238
+ lastHookEventAt: timestamp,
239
+ lastTurnEndedAt: isStop ? timestamp : current.lastTurnEndedAt,
240
+ lastTurnStartedAt: isStop ? current.lastTurnStartedAt : timestamp,
241
+ updatedAt: timestamp
242
+ };
243
+ deps.registry.upsert(updated);
244
+ await persistCodexTranslatorSession(deps.fs, repoRoot, updated);
245
+ return updated;
246
+ },
108
247
  startRoleSession(repoRoot, taskSlug, role, input = {}) {
248
+ if (role === CODEX_TRANSLATOR_ROLE) {
249
+ void taskSlug;
250
+ return this.startProjectTranslatorSession(repoRoot, input);
251
+ }
109
252
  return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
110
253
  },
111
254
  resumeRoleSession(repoRoot, taskSlug, role, input = {}) {
255
+ if (role === CODEX_TRANSLATOR_ROLE) {
256
+ void taskSlug;
257
+ return this.resumeProjectTranslatorSession(repoRoot, input);
258
+ }
112
259
  return launchRoleSession(repoRoot, taskSlug, role, input, "resume");
113
260
  },
114
261
  async stopRoleSession(repoRoot, taskSlug, role) {
262
+ if (role === CODEX_TRANSLATOR_ROLE) {
263
+ void taskSlug;
264
+ return this.stopProjectTranslatorSession(repoRoot);
265
+ }
115
266
  const existing = await this.getRoleSession(repoRoot, taskSlug, role);
116
267
  if (!existing) {
117
268
  throw new VcmError({
@@ -136,6 +287,10 @@ export function createSessionService(deps) {
136
287
  return updated;
137
288
  },
138
289
  async restartRoleSession(repoRoot, taskSlug, role, input = {}) {
290
+ if (role === CODEX_TRANSLATOR_ROLE) {
291
+ void taskSlug;
292
+ return this.restartProjectTranslatorSession(repoRoot, input);
293
+ }
139
294
  const existing = await this.getRoleSession(repoRoot, taskSlug, role);
140
295
  if (!existing) {
141
296
  return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
@@ -147,6 +302,10 @@ export function createSessionService(deps) {
147
302
  return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
148
303
  },
149
304
  async getRoleSession(repoRoot, taskSlug, role) {
305
+ if (role === CODEX_TRANSLATOR_ROLE) {
306
+ void taskSlug;
307
+ return this.getProjectTranslatorSession(repoRoot);
308
+ }
150
309
  const config = await deps.projectService.loadConfig(repoRoot);
151
310
  const task = await deps.taskService.loadTask(repoRoot, taskSlug);
152
311
  const taskRepoRoot = getTaskRuntimeRepoRoot(task);
@@ -163,12 +322,9 @@ export function createSessionService(deps) {
163
322
  const task = await deps.taskService.loadTask(repoRoot, taskSlug);
164
323
  const taskRepoRoot = getTaskRuntimeRepoRoot(task);
165
324
  const persistedTaskSession = await loadPersistedTaskSessionRecord(deps.fs, taskRepoRoot, config.stateRoot, taskSlug);
166
- for (const role of ROLE_NAMES) {
167
- const record = role === CODEX_TRANSLATOR_ROLE
168
- ? getRegisteredRoleSession(deps.registry, deps.runtime, taskSlug, role)
169
- ?? await loadPersistedCodexTranslatorSession(deps.fs, repoRoot, taskSlug)
170
- : deps.registry.getByRole(taskSlug, role)
171
- ?? normalizePersistedRoleRecord(persistedTaskSession?.roles[role]?.record);
325
+ for (const role of ROLE_NAMES.filter((candidate) => candidate !== CODEX_TRANSLATOR_ROLE)) {
326
+ const record = deps.registry.getByRole(taskSlug, role)
327
+ ?? normalizePersistedRoleRecord(persistedTaskSession?.roles[role]?.record);
172
328
  const session = toRoleSessionRecordView(record, deps.runtime);
173
329
  if (session) {
174
330
  sessions.push(session);
@@ -177,6 +333,15 @@ export function createSessionService(deps) {
177
333
  return sessions;
178
334
  },
179
335
  async recordRoleHookEvent(repoRoot, input) {
336
+ if (input.role === CODEX_TRANSLATOR_ROLE) {
337
+ void input.taskSlug;
338
+ return this.recordProjectTranslatorHookEvent(repoRoot, {
339
+ eventName: input.eventName,
340
+ sessionId: input.sessionId,
341
+ transcriptPath: input.transcriptPath,
342
+ cwd: input.cwd
343
+ });
344
+ }
180
345
  const current = await this.getRoleSession(repoRoot, input.taskSlug, input.role);
181
346
  if (!current || (!input.allowSessionMismatch && !matchesRoleHookSession(current, input))) {
182
347
  return undefined;
@@ -385,6 +550,11 @@ function getRegisteredRoleSession(registry, runtime, taskSlug, role) {
385
550
  ?? candidates.sort(compareSessionUpdatedAtDesc)[0];
386
551
  return scopeProjectRoleSession(scoped, taskSlug);
387
552
  }
553
+ function getRegisteredProjectTranslatorSession(registry, runtime) {
554
+ const candidates = registry.list().filter((session) => session.role === CODEX_TRANSLATOR_ROLE);
555
+ const live = candidates.find((session) => runtime.getSession(session.id)?.status === "running");
556
+ return live ?? candidates.sort(compareSessionUpdatedAtDesc)[0];
557
+ }
388
558
  function compareSessionUpdatedAtDesc(left, right) {
389
559
  return (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
390
560
  }
@@ -399,11 +569,12 @@ function scopeProjectRoleSession(record, taskSlug) {
399
569
  }
400
570
  async function loadPersistedRoleRecordForRole(fs, baseRepoRoot, taskRepoRoot, stateRoot, taskSlug, role) {
401
571
  if (role === CODEX_TRANSLATOR_ROLE) {
402
- return loadPersistedCodexTranslatorSession(fs, baseRepoRoot, taskSlug);
572
+ void taskSlug;
573
+ return loadPersistedCodexTranslatorSession(fs, baseRepoRoot);
403
574
  }
404
575
  return loadPersistedRoleRecord(fs, taskRepoRoot, stateRoot, taskSlug, role);
405
576
  }
406
- async function loadPersistedCodexTranslatorSession(fs, repoRoot, taskSlug) {
577
+ async function loadPersistedCodexTranslatorSession(fs, repoRoot) {
407
578
  const sessionPath = resolveRepoPath(repoRoot, CODEX_TRANSLATOR_SESSION_PATH);
408
579
  if (!(await fs.pathExists(sessionPath))) {
409
580
  return undefined;
@@ -413,7 +584,10 @@ async function loadPersistedCodexTranslatorSession(fs, repoRoot, taskSlug) {
413
584
  if (!record) {
414
585
  return undefined;
415
586
  }
416
- return scopeProjectRoleSession(record, taskSlug);
587
+ return {
588
+ ...record,
589
+ taskSlug: PROJECT_TRANSLATOR_SCOPE
590
+ };
417
591
  }
418
592
  async function loadPersistedRoleRecord(fs, repoRoot, stateRoot, taskSlug, role) {
419
593
  const current = await loadPersistedTaskSessionRecord(fs, repoRoot, stateRoot, taskSlug);
@@ -486,7 +660,7 @@ async function persistCodexTranslatorSession(fs, repoRoot, session) {
486
660
  updatedAt: session.updatedAt,
487
661
  record: {
488
662
  ...session,
489
- taskSlug: session.taskSlug
663
+ taskSlug: PROJECT_TRANSLATOR_SCOPE
490
664
  }
491
665
  });
492
666
  }
@@ -1,4 +1,4 @@
1
- import { DISPATCHABLE_ROLES, ROLE_NAMES } from "../../shared/constants.js";
1
+ import { DISPATCHABLE_ROLES } from "../../shared/constants.js";
2
2
  import { isOpenFileLimitError } from "../errors.js";
3
3
  import { getTaskRuntimeRepoRoot } from "./task-service.js";
4
4
  export function createStatusService(deps) {
@@ -45,18 +45,13 @@ export function createStatusService(deps) {
45
45
  }
46
46
  function degradedArtifactSummary(handoffDir) {
47
47
  const roleCommandsDir = `${handoffDir}/role-commands`;
48
- const logsDir = `${handoffDir}/logs`;
49
48
  const messagesDir = `${handoffDir}/messages`;
50
49
  return {
51
50
  paths: {
52
51
  handoffDir,
53
52
  roleCommandsDir,
54
- logsDir,
55
53
  messagesDir,
56
54
  roleCommandPaths: Object.fromEntries(DISPATCHABLE_ROLES.map((role) => [role, `${roleCommandsDir}/${role}.md`])),
57
- roleLogPaths: Object.fromEntries(ROLE_NAMES
58
- .filter((role) => role !== "codex-translator")
59
- .map((role) => [role, `${logsDir}/${role}.log`])),
60
55
  messageRoutePaths: {},
61
56
  architecturePlanPath: `${handoffDir}/architecture-plan.md`,
62
57
  knownIssuesPath: `${handoffDir}/known-issues.md`,
@@ -9,13 +9,8 @@ export function createTaskService(deps) {
9
9
  const config = await deps.projectService.loadConfig(repoRoot);
10
10
  const taskStoreRoot = deps.projectService.getProjectDataRoot(repoRoot);
11
11
  const taskPath = getTaskPath(taskStoreRoot, input.taskSlug);
12
- const shouldCreateWorktree = input.createWorktree !== false;
13
- const taskBranch = shouldCreateWorktree
14
- ? `feature/${input.taskSlug}`
15
- : await deps.git.getCurrentBranch(repoRoot);
16
- const worktreePath = shouldCreateWorktree
17
- ? getTaskWorktreePath(repoRoot, input.taskSlug)
18
- : undefined;
12
+ const taskBranch = `feature/${input.taskSlug}`;
13
+ const worktreePath = getTaskWorktreePath(repoRoot, input.taskSlug);
19
14
  if (await deps.fs.pathExists(taskPath)) {
20
15
  throw new VcmError({
21
16
  code: "TASK_EXISTS",
@@ -31,7 +26,7 @@ export function createTaskService(deps) {
31
26
  hint: "Apply VCM Harness first so .gitignore contains the VCM managed block."
32
27
  });
33
28
  }
34
- if (shouldCreateWorktree && !(await deps.git.isIgnored(repoRoot, ".claude/worktrees/.probe"))) {
29
+ if (!(await deps.git.isIgnored(repoRoot, ".claude/worktrees/.probe"))) {
35
30
  throw new VcmError({
36
31
  code: "VCM_WORKTREES_NOT_IGNORED",
37
32
  message: ".claude/worktrees/ is not ignored by Git.",
@@ -39,53 +34,39 @@ export function createTaskService(deps) {
39
34
  hint: "Apply VCM Harness first so .gitignore ignores Claude-compatible task worktrees."
40
35
  });
41
36
  }
42
- if (!shouldCreateWorktree) {
43
- const activeInlineTask = await findActiveInlineTask(deps.fs, taskStoreRoot);
44
- if (activeInlineTask) {
45
- throw new VcmError({
46
- code: "INLINE_TASK_EXISTS",
47
- message: `An inline task already exists: ${activeInlineTask.taskSlug}`,
48
- statusCode: 409,
49
- hint: "Close the existing inline task first, or enable Create worktree and branch for this task."
50
- });
51
- }
37
+ if (await deps.git.branchExists(repoRoot, taskBranch)) {
38
+ throw new VcmError({
39
+ code: "TASK_BRANCH_EXISTS",
40
+ message: `Task branch already exists: ${taskBranch}`,
41
+ statusCode: 409,
42
+ hint: "Choose a different task name or clean up the existing branch."
43
+ });
44
+ }
45
+ if (await deps.fs.pathExists(worktreePath)) {
46
+ throw new VcmError({
47
+ code: "TASK_WORKTREE_EXISTS",
48
+ message: `Task worktree already exists: ${worktreePath}`,
49
+ statusCode: 409,
50
+ hint: "Choose a different task name or clean up the existing worktree."
51
+ });
52
52
  }
53
- if (shouldCreateWorktree && worktreePath) {
54
- if (await deps.git.branchExists(repoRoot, taskBranch)) {
55
- throw new VcmError({
56
- code: "TASK_BRANCH_EXISTS",
57
- message: `Task branch already exists: ${taskBranch}`,
58
- statusCode: 409,
59
- hint: "Choose a different task name or clean up the existing branch."
60
- });
61
- }
62
- if (await deps.fs.pathExists(worktreePath)) {
63
- throw new VcmError({
64
- code: "TASK_WORKTREE_EXISTS",
65
- message: `Task worktree already exists: ${worktreePath}`,
66
- statusCode: 409,
67
- hint: "Choose a different task name or clean up the existing worktree."
68
- });
69
- }
70
- const baseStatus = await deps.git.getStatusPorcelain(repoRoot);
71
- if (baseStatus.trim()) {
72
- throw new VcmError({
73
- code: "BASE_REPO_DIRTY",
74
- message: "The connected repository has uncommitted changes.",
75
- statusCode: 409,
76
- hint: "Commit, stash, or discard base repository changes before creating a task worktree."
77
- });
78
- }
79
- await deps.fs.ensureDir(path.dirname(worktreePath));
80
- await deps.git.createWorktree({
81
- repoRoot,
82
- branch: taskBranch,
83
- worktreePath,
84
- baseRef: "HEAD"
53
+ const baseStatus = await deps.git.getStatusPorcelain(repoRoot);
54
+ if (baseStatus.trim()) {
55
+ throw new VcmError({
56
+ code: "BASE_REPO_DIRTY",
57
+ message: "The connected repository has uncommitted changes.",
58
+ statusCode: 409,
59
+ hint: "Commit, stash, or discard base repository changes before creating a task worktree."
85
60
  });
86
61
  }
87
62
  const timestamp = now();
88
- const taskRepoRoot = worktreePath ?? repoRoot;
63
+ await deps.fs.ensureDir(path.dirname(worktreePath));
64
+ await deps.git.createWorktree({
65
+ repoRoot,
66
+ branch: taskBranch,
67
+ worktreePath,
68
+ baseRef: "HEAD"
69
+ });
89
70
  const task = {
90
71
  version: 1,
91
72
  taskSlug: input.taskSlug,
@@ -100,14 +81,14 @@ export function createTaskService(deps) {
100
81
  specPath: input.specPath,
101
82
  cleanupStatus: "active"
102
83
  };
103
- await ensureTaskRuntimeStateDirs(deps.fs, taskRepoRoot, config.stateRoot);
84
+ await ensureTaskRuntimeStateDirs(deps.fs, worktreePath, config.stateRoot);
104
85
  await deps.artifactService.ensureHandoffStructure({
105
- repoRoot: taskRepoRoot,
86
+ repoRoot: worktreePath,
106
87
  taskSlug: input.taskSlug,
107
88
  handoffDir: task.handoffDir
108
89
  });
109
90
  await deps.artifactService.createArtifactTemplates({
110
- repoRoot: taskRepoRoot,
91
+ repoRoot: worktreePath,
111
92
  taskSlug: input.taskSlug,
112
93
  handoffDir: task.handoffDir,
113
94
  branch: task.branch
@@ -123,7 +104,7 @@ export function createTaskService(deps) {
123
104
  const entries = await deps.fs.readDir(tasksDir);
124
105
  const tasks = [];
125
106
  for (const entry of entries.filter((candidate) => candidate.endsWith(".json"))) {
126
- tasks.push(normalizeTaskRecord(await deps.fs.readJson(path.join(tasksDir, entry))));
107
+ tasks.push(await deps.fs.readJson(path.join(tasksDir, entry)));
127
108
  }
128
109
  return tasks.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
129
110
  },
@@ -137,7 +118,7 @@ export function createTaskService(deps) {
137
118
  statusCode: 404
138
119
  });
139
120
  }
140
- return normalizeTaskRecord(await deps.fs.readJson(taskPath));
121
+ return deps.fs.readJson(taskPath);
141
122
  },
142
123
  async saveTask(repoRoot, task) {
143
124
  await deps.fs.writeJsonAtomic(getTaskPath(deps.projectService.getProjectDataRoot(repoRoot), task.taskSlug), task);
@@ -167,15 +148,9 @@ export function createTaskService(deps) {
167
148
  const statePaths = getTaskStatePaths(deps.projectService.getProjectDataRoot(repoRoot), taskRepoRoot, config.stateRoot, config.handoffRoot, taskSlug);
168
149
  const removedStatePaths = [];
169
150
  const cleanedAt = now();
170
- if (task.worktreePath) {
171
- assertTaskWorktreePath(repoRoot, task.worktreePath);
172
- await deps.git.removeWorktree(repoRoot, task.worktreePath, { force: options.force ?? true });
173
- }
174
- let deletedBranch;
175
- if (task.worktreePath && (options.deleteBranch ?? true)) {
176
- await deps.git.deleteBranch(repoRoot, task.branch, { force: options.forceDeleteBranch ?? true });
177
- deletedBranch = task.branch;
178
- }
151
+ assertTaskWorktreePath(repoRoot, task.worktreePath);
152
+ await deps.git.removeWorktree(repoRoot, task.worktreePath, { force: options.force ?? true });
153
+ await deps.git.deleteBranch(repoRoot, task.branch, { force: options.forceDeleteBranch ?? true });
179
154
  for (const statePath of statePaths) {
180
155
  await deps.fs.removePath(statePath, { recursive: true, force: true });
181
156
  removedStatePaths.push(statePath);
@@ -184,14 +159,14 @@ export function createTaskService(deps) {
184
159
  taskSlug,
185
160
  removedWorktreePath: task.worktreePath,
186
161
  removedStatePaths,
187
- deletedBranch,
162
+ deletedBranch: task.branch,
188
163
  cleanedAt
189
164
  };
190
165
  }
191
166
  };
192
167
  }
193
168
  export function getTaskRuntimeRepoRoot(task) {
194
- return task.worktreePath ?? task.repoRoot;
169
+ return task.worktreePath;
195
170
  }
196
171
  function getTaskPath(taskStoreRoot, taskSlug) {
197
172
  return path.join(taskStoreRoot, "tasks", `${taskSlug}.json`);
@@ -206,49 +181,6 @@ async function ensureTaskRuntimeStateDirs(fs, taskRepoRoot, stateRoot) {
206
181
  await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "translation"));
207
182
  await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "codex-reviews"));
208
183
  }
209
- async function findActiveInlineTask(fs, taskStoreRoot) {
210
- const tasksDir = path.join(taskStoreRoot, "tasks");
211
- if (!(await fs.pathExists(tasksDir))) {
212
- return undefined;
213
- }
214
- const entries = await fs.readDir(tasksDir);
215
- for (const entry of entries.filter((candidate) => candidate.endsWith(".json"))) {
216
- const task = normalizeTaskRecord(await fs.readJson(path.join(tasksDir, entry)));
217
- if (!task.worktreePath && task.cleanupStatus !== "cleaned") {
218
- return task;
219
- }
220
- }
221
- return undefined;
222
- }
223
- function normalizeTaskRecord(input) {
224
- return {
225
- version: 1,
226
- taskSlug: input.taskSlug ?? "",
227
- title: input.title,
228
- createdAt: input.createdAt ?? "",
229
- updatedAt: input.updatedAt ?? input.createdAt ?? "",
230
- repoRoot: input.repoRoot ?? "",
231
- worktreePath: input.worktreePath,
232
- branch: input.branch ?? "",
233
- handoffDir: input.handoffDir ?? ".ai/vcm/handoffs",
234
- status: normalizeTaskStatus(input.status),
235
- specPath: input.specPath,
236
- cleanupStatus: input.cleanupStatus,
237
- cleanedAt: input.cleanedAt
238
- };
239
- }
240
- function normalizeTaskStatus(value) {
241
- if (value === "created" || value === "running" || value === "stopped") {
242
- return value;
243
- }
244
- if (value === undefined || value === null) {
245
- return "created";
246
- }
247
- if (value === "planning") {
248
- return "created";
249
- }
250
- return "stopped";
251
- }
252
184
  function getTaskStatePaths(taskStoreRoot, taskRepoRoot, stateRoot, handoffRoot, taskSlug) {
253
185
  return [
254
186
  getTaskPath(taskStoreRoot, taskSlug),