vibe-coding-master 0.0.16 → 0.2.0

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 (47) hide show
  1. package/README.md +74 -41
  2. package/dist/backend/api/artifact-routes.js +5 -5
  3. package/dist/backend/api/harness-routes.js +8 -0
  4. package/dist/backend/api/message-routes.js +4 -4
  5. package/dist/backend/api/round-routes.js +4 -2
  6. package/dist/backend/server.js +8 -2
  7. package/dist/backend/services/artifact-service.js +12 -12
  8. package/dist/backend/services/claude-hook-service.js +1 -1
  9. package/dist/backend/services/harness-service.js +579 -5
  10. package/dist/backend/services/message-service.js +71 -137
  11. package/dist/backend/services/project-service.js +4 -1
  12. package/dist/backend/services/round-service.js +14 -52
  13. package/dist/backend/services/session-service.js +1 -3
  14. package/dist/backend/services/task-service.js +16 -17
  15. package/dist/backend/templates/handoff.js +64 -26
  16. package/dist/backend/templates/harness/architect-agent.js +42 -12
  17. package/dist/backend/templates/harness/claude-root.js +42 -18
  18. package/dist/backend/templates/harness/coder-agent.js +15 -11
  19. package/dist/backend/templates/harness/known-issues-doc.js +22 -0
  20. package/dist/backend/templates/harness/project-manager-agent.js +66 -15
  21. package/dist/backend/templates/harness/pull-request-template.js +29 -0
  22. package/dist/backend/templates/harness/reviewer-agent.js +40 -12
  23. package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +105 -0
  24. package/dist/backend/templates/harness/vcm-harness-bootstrap-skill.js +78 -0
  25. package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +50 -0
  26. package/dist/backend/templates/harness/vcm-route-message-skill.js +86 -0
  27. package/dist/backend/templates/message-envelope.js +1 -0
  28. package/dist/backend/templates/role-command.js +7 -1
  29. package/dist/shared/validation/artifact-check.js +14 -9
  30. package/dist-frontend/assets/index-CrY5Ryps.js +90 -0
  31. package/dist-frontend/assets/index-CvvtrrCN.css +32 -0
  32. package/dist-frontend/index.html +2 -2
  33. package/docs/cc-best-practices.md +434 -192
  34. package/docs/full-harness-baseline.md +254 -0
  35. package/docs/product-design.md +31 -28
  36. package/docs/v0.2-implementation-plan.md +379 -0
  37. package/docs/vcm-cc-best-practices.md +449 -0
  38. package/package.json +3 -1
  39. package/scripts/harness-tools/generate-module-index +298 -0
  40. package/scripts/harness-tools/generate-public-surface +692 -0
  41. package/scripts/install-vcm-harness.mjs +1607 -0
  42. package/scripts/uninstall-vcm-harness.mjs +490 -0
  43. package/scripts/verify-package.mjs +4 -0
  44. package/dist-frontend/assets/index-CvtyKEfS.js +0 -89
  45. package/dist-frontend/assets/index-jEkUTnIY.css +0 -32
  46. package/docs/v1-architecture-design.md +0 -1009
  47. package/docs/v1-implementation-plan.md +0 -1376
@@ -8,25 +8,11 @@ import { renderMessageEnvelope } from "../templates/message-envelope.js";
8
8
  const PM_ROLE = "project-manager";
9
9
  const PM_TO_ROLE_TYPES = new Set(["task", "question", "review-request", "revise", "cancel"]);
10
10
  const ROLE_TO_PM_TYPES = new Set(["result", "question", "blocked", "finding"]);
11
- const OPEN_MESSAGE_STATUSES = new Set([
12
- "pending_approval",
13
- "queued",
14
- "delivering",
15
- "submitted",
16
- "failed",
17
- "delivery_failed",
18
- "response_ready_missing_result"
19
- ]);
20
- const MUTABLE_ROUTE_MESSAGE_STATUSES = new Set([
21
- "pending_approval",
22
- "queued",
23
- "failed",
24
- "delivery_failed",
25
- "response_ready_missing_result"
26
- ]);
11
+ const DEFAULT_PRE_DISPATCH_SWITCH_DELAY_MS = 500;
27
12
  export function createMessageService(deps) {
28
13
  const now = deps.now ?? (() => new Date().toISOString());
29
14
  const id = deps.id ?? (() => `msg_${randomUUID()}`);
15
+ const preDispatchSwitchDelayMs = deps.preDispatchSwitchDelayMs ?? DEFAULT_PRE_DISPATCH_SWITCH_DELAY_MS;
30
16
  const taskLocks = new Map();
31
17
  async function getOrchestrationState(input) {
32
18
  const statePath = getOrchestrationStatePath(getStateRepoRoot(input), input.stateRoot, input.taskSlug);
@@ -49,32 +35,17 @@ export function createMessageService(deps) {
49
35
  const timestamp = now();
50
36
  const base = toRouteContext(input);
51
37
  const state = await getOrchestrationState(input);
52
- const messagesPath = getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug);
53
- let messages = await readLatestMessages(deps.fs, messagesPath);
54
- const updates = [];
55
- if (input.stoppedRole) {
56
- const acknowledged = acknowledgeActiveMessages(messages, input.stoppedRole, timestamp);
57
- for (const message of acknowledged) {
58
- await appendMessageSnapshot(deps.fs, input, message);
59
- await clearRouteFileIfStillMatchesMessage(deps.fs, input, message);
60
- }
61
- updates.push(...acknowledged);
62
- messages = applyMessageSnapshots(messages, acknowledged);
63
- }
64
38
  const pendingFiles = (await listRouteFiles(deps.fs, base)).filter((routeFile) => routeFile.pending);
65
39
  const candidates = selectDispatchCandidates(pendingFiles, input.stoppedRole);
66
40
  const results = [];
67
- const deliveredTargets = new Set();
68
41
  for (const routeFile of candidates) {
69
- if (deliveredTargets.has(routeFile.toRole)) {
70
- continue;
71
- }
72
- const result = await dispatchRouteFile(input, routeFile, messages, state, timestamp);
42
+ const result = await dispatchRouteFile(input, routeFile, state, timestamp);
73
43
  results.push(result);
74
- await appendMessageSnapshot(deps.fs, input, result.message);
75
- messages = applyMessageSnapshots(messages, [result.message]);
44
+ if (result.message) {
45
+ await appendMessageSnapshot(deps.fs, input, result.message);
46
+ }
76
47
  if (result.delivered) {
77
- deliveredTargets.add(routeFile.toRole);
48
+ break;
78
49
  }
79
50
  }
80
51
  return results;
@@ -84,73 +55,56 @@ export function createMessageService(deps) {
84
55
  const all = await listRouteFiles(deps.fs, context);
85
56
  return all.filter((routeFile) => routeFile.pending);
86
57
  }
87
- async function dispatchRouteFile(input, routeFile, existingMessages, state, timestamp) {
58
+ async function dispatchRouteFile(input, routeFile, state, timestamp) {
88
59
  validateMessagePolicy(routeFile.fromRole, routeFile.toRole, routeFile.type);
89
- const existingOpen = findOpenMessageForRoute(existingMessages, routeFile.path);
90
- const message = {
91
- ...(existingOpen ?? {
92
- id: id(),
93
- taskSlug: input.taskSlug,
94
- createdAt: timestamp
95
- }),
96
- fromRole: routeFile.fromRole,
97
- toRole: routeFile.toRole,
98
- type: routeFile.type,
99
- body: routeFile.body,
100
- artifactRefs: routeFile.artifactRefs,
101
- bodyPath: routeFile.path,
102
- routePath: routeFile.path,
103
- status: "queued",
104
- queuedBehindMessageId: undefined,
105
- failureReason: undefined
106
- };
107
60
  const session = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, routeFile.toRole);
108
61
  if (!session || session.status !== "running") {
109
62
  return {
110
- message: {
111
- ...message,
112
- status: "queued",
113
- failureReason: `${routeFile.toRole} session is not running.`
114
- },
115
63
  delivered: false,
116
64
  requiresUserApproval: false,
117
- clearedRouteFile: false
65
+ clearedRouteFile: false,
66
+ failureReason: `${routeFile.toRole} session is not running.`
118
67
  };
119
68
  }
120
69
  if (state.mode === "manual") {
121
70
  return {
122
- message: {
123
- ...message,
124
- status: "pending_approval"
125
- },
126
71
  delivered: false,
127
72
  requiresUserApproval: true,
128
73
  clearedRouteFile: false
129
74
  };
130
75
  }
131
- const activeMessage = findActiveMessageForRole(existingMessages, routeFile.toRole);
132
- if (activeMessage) {
76
+ if (session.activityStatus === "running") {
133
77
  return {
134
- message: {
135
- ...message,
136
- status: "queued",
137
- queuedBehindMessageId: activeMessage.id,
138
- failureReason: `${routeFile.toRole} is still handling VCM message ${activeMessage.id}.`
139
- },
140
78
  delivered: false,
141
79
  requiresUserApproval: false,
142
- clearedRouteFile: false
80
+ clearedRouteFile: false,
81
+ failureReason: `${routeFile.toRole} is still running.`
143
82
  };
144
83
  }
145
- const delivering = {
84
+ const message = {
85
+ id: id(),
86
+ taskSlug: input.taskSlug,
87
+ createdAt: timestamp,
88
+ fromRole: routeFile.fromRole,
89
+ toRole: routeFile.toRole,
90
+ type: routeFile.type,
91
+ body: routeFile.body,
92
+ artifactRefs: routeFile.artifactRefs,
93
+ bodyPath: routeFile.path,
94
+ routePath: routeFile.path,
95
+ dispatchingAt: timestamp,
96
+ failureReason: undefined
97
+ };
98
+ await appendMessageSnapshot(deps.fs, input, message);
99
+ await delay(preDispatchSwitchDelayMs);
100
+ const delivered = {
146
101
  ...message,
147
- status: "delivering",
148
102
  deliveredAt: timestamp
149
103
  };
150
- await submitTerminalInput(deps.runtime, session.id, renderMessageEnvelope(delivering));
104
+ await submitTerminalInput(deps.runtime, session.id, renderMessageEnvelope(delivered));
151
105
  await deps.sessionService.markRoleActivityRunning(input.repoRoot, input.taskSlug, routeFile.toRole);
152
106
  return {
153
- message: delivering,
107
+ message: delivered,
154
108
  delivered: true,
155
109
  requiresUserApproval: false,
156
110
  clearedRouteFile: false
@@ -174,48 +128,46 @@ export function createMessageService(deps) {
174
128
  if (!messageId) {
175
129
  return undefined;
176
130
  }
177
- const message = findDeliveringMessageForPrompt(messages, input.role, messageId);
131
+ const message = findDeliveredMessageForPrompt(messages, input.role, messageId);
178
132
  if (!message) {
179
133
  return undefined;
180
134
  }
181
- const submitted = {
135
+ const accepted = {
182
136
  ...message,
183
- status: "submitted",
184
- submittedAt: timestamp,
185
- queuedBehindMessageId: undefined,
137
+ acceptedAt: timestamp,
186
138
  failureReason: undefined
187
139
  };
188
- await appendMessageSnapshot(deps.fs, input, submitted);
189
- await clearRouteFileIfStillMatchesMessage(deps.fs, input, submitted);
190
- return submitted;
140
+ await appendMessageSnapshot(deps.fs, input, accepted);
141
+ await clearRouteFileIfStillMatchesMessage(deps.fs, input, accepted);
142
+ return accepted;
191
143
  });
192
144
  },
193
145
  async markAllDone(input) {
194
146
  return withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), async () => {
195
- const timestamp = now();
196
147
  const messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
197
- const updated = messages
198
- .filter((message) => OPEN_MESSAGE_STATUSES.has(message.status))
199
- .map((message) => ({
200
- ...message,
201
- status: "acknowledged",
202
- acknowledgedAt: timestamp,
203
- queuedBehindMessageId: undefined,
204
- failureReason: undefined
205
- }));
206
- for (const message of updated) {
207
- await appendMessageSnapshot(deps.fs, input, message);
208
- }
148
+ let clearedCount = 0;
209
149
  if (input.clearRouteFiles) {
210
150
  for (const routeFile of await listPendingRouteFiles(input)) {
211
151
  await deps.fs.writeText(resolveRepoPath(input.taskRepoRoot ?? input.repoRoot, routeFile.path), "");
152
+ clearedCount += 1;
212
153
  }
213
154
  }
214
- const latest = applyMessageSnapshots(messages, updated);
215
155
  return {
216
156
  taskSlug: input.taskSlug,
217
- updatedCount: updated.length,
218
- messages: latest
157
+ updatedCount: clearedCount,
158
+ messages
159
+ };
160
+ });
161
+ },
162
+ async deleteMessageHistory(input) {
163
+ return withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), async () => {
164
+ const messagesPath = getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug);
165
+ const messages = await readLatestMessages(deps.fs, messagesPath);
166
+ await writeMessageSnapshots(deps.fs, messagesPath, []);
167
+ return {
168
+ taskSlug: input.taskSlug,
169
+ deletedCount: messages.length,
170
+ messages: []
219
171
  };
220
172
  });
221
173
  },
@@ -401,31 +353,11 @@ async function withTaskLock(locks, key, run) {
401
353
  }
402
354
  }
403
355
  }
404
- function findOpenMessageForRoute(messages, routePath) {
405
- return [...messages]
406
- .reverse()
407
- .find((message) => (message.routePath ?? message.bodyPath) === routePath && MUTABLE_ROUTE_MESSAGE_STATUSES.has(message.status));
408
- }
409
- function findActiveMessageForRole(messages, role) {
410
- return messages.find((message) => message.toRole === role &&
411
- (message.status === "delivering" || message.status === "submitted"));
412
- }
413
- function acknowledgeActiveMessages(messages, role, timestamp) {
414
- return messages
415
- .filter((message) => message.toRole === role && (message.status === "delivering" || message.status === "submitted"))
416
- .map((message) => ({
417
- ...message,
418
- status: "acknowledged",
419
- submittedAt: message.submittedAt ?? message.deliveredAt ?? timestamp,
420
- acknowledgedAt: timestamp,
421
- failureReason: undefined
422
- }));
423
- }
424
- function findDeliveringMessageForPrompt(messages, role, messageId) {
425
- const delivering = messages.filter((message) => message.toRole === role &&
426
- message.status === "delivering" &&
427
- message.id === messageId);
428
- return delivering.sort((left, right) => getMessageDeliveryTime(left).localeCompare(getMessageDeliveryTime(right))).at(-1);
356
+ function findDeliveredMessageForPrompt(messages, role, messageId) {
357
+ const delivered = messages.filter((message) => message.toRole === role &&
358
+ message.id === messageId &&
359
+ message.deliveredAt);
360
+ return delivered.sort((left, right) => getMessageDeliveryTime(left).localeCompare(getMessageDeliveryTime(right))).at(-1);
429
361
  }
430
362
  function extractVcmMessageId(prompt) {
431
363
  return prompt?.match(/^\s*id:\s*(\S+)\s*$/m)?.[1];
@@ -455,16 +387,6 @@ function arraysEqual(left, right) {
455
387
  }
456
388
  return left.every((value, index) => value === right[index]);
457
389
  }
458
- function applyMessageSnapshots(messages, snapshots) {
459
- if (snapshots.length === 0) {
460
- return messages;
461
- }
462
- const latest = new Map(messages.map((message) => [message.id, message]));
463
- for (const snapshot of snapshots) {
464
- latest.set(snapshot.id, snapshot);
465
- }
466
- return [...latest.values()].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
467
- }
468
390
  async function readLatestMessages(fs, messagesPath) {
469
391
  if (!(await fs.pathExists(messagesPath))) {
470
392
  return [];
@@ -480,6 +402,12 @@ async function readLatestMessages(fs, messagesPath) {
480
402
  async function appendMessageSnapshot(fs, input, message) {
481
403
  await fs.appendText(getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), `${JSON.stringify(message)}\n`);
482
404
  }
405
+ async function writeMessageSnapshots(fs, messagesPath, messages) {
406
+ const content = messages.length > 0
407
+ ? `${messages.map((message) => JSON.stringify(message)).join("\n")}\n`
408
+ : "";
409
+ await fs.writeText(messagesPath, content);
410
+ }
483
411
  function getMessagesPath(repoRoot, stateRoot, taskSlug) {
484
412
  return path.join(repoRoot, stateRoot, "messages", `${taskSlug}.jsonl`);
485
413
  }
@@ -489,3 +417,9 @@ function getOrchestrationStatePath(repoRoot, stateRoot, taskSlug) {
489
417
  function getStateRepoRoot(input) {
490
418
  return input.stateRepoRoot ?? input.repoRoot;
491
419
  }
420
+ function delay(ms) {
421
+ if (ms <= 0) {
422
+ return Promise.resolve();
423
+ }
424
+ return new Promise((resolve) => setTimeout(resolve, ms));
425
+ }
@@ -30,7 +30,7 @@ export function createProjectService(deps) {
30
30
  }
31
31
  const config = await this.loadConfig(repoRoot);
32
32
  await deps.fs.ensureDir(path.join(repoRoot, config.handoffRoot));
33
- await deps.fs.ensureDir(path.join(repoRoot, config.stateRoot, "tasks"));
33
+ await deps.fs.ensureDir(path.join(this.getProjectDataRoot(repoRoot), "tasks"));
34
34
  await deps.fs.ensureDir(path.join(repoRoot, ".claude", "worktrees"));
35
35
  await this.saveConfig(config, true);
36
36
  const warnings = [];
@@ -87,6 +87,9 @@ export function createProjectService(deps) {
87
87
  },
88
88
  getConfigPath(repoRoot) {
89
89
  return deps.appSettings.getProjectConfigPath(repoRoot);
90
+ },
91
+ getProjectDataRoot(repoRoot) {
92
+ return path.dirname(deps.appSettings.getProjectConfigPath(repoRoot));
90
93
  }
91
94
  };
92
95
  }
@@ -4,21 +4,15 @@ export function createRoundService(deps = {}) {
4
4
  return {
5
5
  getTaskRoundState(input) {
6
6
  const roleStates = ROLE_NAMES.map((role) => toRoleTurnState(role, input.sessions));
7
- const latestDelivered = getLatestDeliveredMessage(input.messages);
8
- const queuedMessageCount = input.messages.filter((message) => message.status === "queued").length;
9
- const pendingMessageCount = input.messages.filter((message) => message.status === "pending_approval").length;
10
7
  const state = evaluateTaskRoundState({
11
8
  taskSlug: input.taskSlug,
12
- messages: input.messages,
9
+ pendingRouteCount: input.pendingRouteCount,
13
10
  roleStates,
14
11
  updatedAt: now()
15
12
  });
16
13
  return {
17
14
  ...state,
18
- latestMessageId: latestDelivered?.id,
19
- latestMessageDeliveredAt: getMessageDeliveredAt(latestDelivered),
20
- queuedMessageCount,
21
- pendingMessageCount
15
+ pendingRouteCount: input.pendingRouteCount
22
16
  };
23
17
  },
24
18
  stopSession() { },
@@ -26,35 +20,22 @@ export function createRoundService(deps = {}) {
26
20
  };
27
21
  }
28
22
  export function evaluateTaskRoundState(input) {
29
- const latestDelivered = getLatestDeliveredMessage(input.messages);
30
- const hasQueuedOrPending = input.messages.some((message) => message.status === "queued" || message.status === "pending_approval");
31
- if (latestDelivered) {
32
- const deliveredAt = getMessageDeliveredAt(latestDelivered);
33
- const targetState = input.roleStates.find((roleState) => roleState.role === latestDelivered.toRole);
34
- const completedAt = targetState && deliveredAt
35
- ? getCompletedAtForDelivery(targetState, deliveredAt)
36
- : undefined;
37
- if (completedAt && !hasQueuedOrPending) {
38
- return {
39
- taskSlug: input.taskSlug,
40
- status: "completed",
41
- activeRole: latestDelivered.toRole,
42
- completionId: `${latestDelivered.id}:${completedAt}`,
43
- completedAt,
44
- roles: input.roleStates,
45
- updatedAt: input.updatedAt
46
- };
47
- }
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) {
48
28
  return {
49
29
  taskSlug: input.taskSlug,
50
- status: targetState?.status === "waiting_user" ? "waiting_user" : "active",
51
- activeRole: latestDelivered.toRole,
30
+ status: activeRole.status === "waiting_user" ? "waiting_user" : "active",
31
+ activeRole: activeRole.role,
52
32
  roles: input.roleStates,
53
33
  updatedAt: input.updatedAt
54
34
  };
55
35
  }
56
- const latestIdleRole = !hasQueuedOrPending ? getLatestIdleRole(input.roleStates) : undefined;
57
- if (latestIdleRole?.lastAnswerEndedAt) {
36
+ const latestIdleRole = getLatestIdleRole(input.roleStates);
37
+ const hasPendingRoutes = input.pendingRouteCount > 0;
38
+ if (latestIdleRole?.lastAnswerEndedAt && !hasPendingRoutes) {
58
39
  return {
59
40
  taskSlug: input.taskSlug,
60
41
  status: "completed",
@@ -65,14 +46,10 @@ export function evaluateTaskRoundState(input) {
65
46
  updatedAt: input.updatedAt
66
47
  };
67
48
  }
68
- const activeRole = input.roleStates.find((roleState) => roleState.status === "answering" ||
69
- roleState.status === "using_tools" ||
70
- roleState.status === "waiting_user" ||
71
- roleState.status === "abnormal");
72
49
  return {
73
50
  taskSlug: input.taskSlug,
74
- status: activeRole?.status === "waiting_user" ? "waiting_user" : activeRole ? "active" : "idle",
75
- activeRole: activeRole?.role,
51
+ status: hasPendingRoutes ? "active" : "idle",
52
+ activeRole: latestIdleRole?.role,
76
53
  roles: input.roleStates,
77
54
  updatedAt: input.updatedAt
78
55
  };
@@ -94,21 +71,6 @@ function toRoleTurnState(role, sessions) {
94
71
  : undefined
95
72
  };
96
73
  }
97
- function getLatestDeliveredMessage(messages) {
98
- return messages
99
- .filter((message) => message.status === "delivering" || message.status === "submitted")
100
- .sort((left, right) => getMessageDeliveredAt(left).localeCompare(getMessageDeliveredAt(right)))
101
- .at(-1);
102
- }
103
- function getMessageDeliveredAt(message) {
104
- return message?.submittedAt ?? message?.deliveredAt ?? message?.createdAt ?? "";
105
- }
106
- function getCompletedAtForDelivery(roleState, deliveredAt) {
107
- if (roleState.status !== "idle" || !roleState.lastAnswerEndedAt) {
108
- return undefined;
109
- }
110
- return roleState.lastAnswerEndedAt >= deliveredAt ? roleState.lastAnswerEndedAt : undefined;
111
- }
112
74
  function getLatestIdleRole(roleStates) {
113
75
  return roleStates
114
76
  .filter((roleState) => roleState.status === "idle" && roleState.lastAnswerEndedAt)
@@ -41,6 +41,7 @@ export function createSessionService(deps) {
41
41
  cwd: taskRepoRoot,
42
42
  env: {
43
43
  VCM_API_URL: deps.apiUrl,
44
+ VCM_TASK_REPO_ROOT: taskRepoRoot,
44
45
  VCM_TASK_SLUG: taskSlug,
45
46
  VCM_ROLE: role,
46
47
  VCM_SESSION_ID: claudeSessionId
@@ -224,9 +225,6 @@ function getHandoffArtifactPath(paths, role) {
224
225
  if (role === "architect") {
225
226
  return paths.architecturePlanPath;
226
227
  }
227
- if (role === "coder") {
228
- return paths.implementationLogPath;
229
- }
230
228
  if (role === "reviewer") {
231
229
  return paths.reviewReportPath;
232
230
  }
@@ -7,7 +7,8 @@ export function createTaskService(deps) {
7
7
  async createTask(repoRoot, input) {
8
8
  assertValidTaskSlug(input.taskSlug);
9
9
  const config = await deps.projectService.loadConfig(repoRoot);
10
- const taskPath = getTaskPath(repoRoot, config.stateRoot, input.taskSlug);
10
+ const taskStoreRoot = deps.projectService.getProjectDataRoot(repoRoot);
11
+ const taskPath = getTaskPath(taskStoreRoot, input.taskSlug);
11
12
  const shouldCreateWorktree = input.createWorktree !== false;
12
13
  const taskBranch = shouldCreateWorktree
13
14
  ? `feature/${input.taskSlug}`
@@ -22,7 +23,7 @@ export function createTaskService(deps) {
22
23
  statusCode: 409
23
24
  });
24
25
  }
25
- if (!(await deps.git.isIgnored(repoRoot, `${config.stateRoot}/tasks/.probe`))) {
26
+ if (!(await deps.git.isIgnored(repoRoot, `${config.stateRoot}/.probe`))) {
26
27
  throw new VcmError({
27
28
  code: "VCM_STATE_NOT_IGNORED",
28
29
  message: `${config.stateRoot}/ is not ignored by Git.`,
@@ -39,7 +40,7 @@ export function createTaskService(deps) {
39
40
  });
40
41
  }
41
42
  if (!shouldCreateWorktree) {
42
- const activeInlineTask = await findActiveInlineTask(deps.fs, repoRoot, config.stateRoot);
43
+ const activeInlineTask = await findActiveInlineTask(deps.fs, taskStoreRoot);
43
44
  if (activeInlineTask) {
44
45
  throw new VcmError({
45
46
  code: "INLINE_TASK_EXISTS",
@@ -108,14 +109,14 @@ export function createTaskService(deps) {
108
109
  await deps.artifactService.createArtifactTemplates({
109
110
  repoRoot: taskRepoRoot,
110
111
  taskSlug: input.taskSlug,
111
- handoffDir: task.handoffDir
112
+ handoffDir: task.handoffDir,
113
+ branch: task.branch
112
114
  });
113
115
  await this.saveTask(repoRoot, task);
114
116
  return task;
115
117
  },
116
118
  async listTasks(repoRoot) {
117
- const config = await deps.projectService.loadConfig(repoRoot);
118
- const tasksDir = path.join(repoRoot, config.stateRoot, "tasks");
119
+ const tasksDir = path.join(deps.projectService.getProjectDataRoot(repoRoot), "tasks");
119
120
  if (!(await deps.fs.pathExists(tasksDir))) {
120
121
  return [];
121
122
  }
@@ -128,8 +129,7 @@ export function createTaskService(deps) {
128
129
  },
129
130
  async loadTask(repoRoot, taskSlug) {
130
131
  assertValidTaskSlug(taskSlug);
131
- const config = await deps.projectService.loadConfig(repoRoot);
132
- const taskPath = getTaskPath(repoRoot, config.stateRoot, taskSlug);
132
+ const taskPath = getTaskPath(deps.projectService.getProjectDataRoot(repoRoot), taskSlug);
133
133
  if (!(await deps.fs.pathExists(taskPath))) {
134
134
  throw new VcmError({
135
135
  code: "TASK_MISSING",
@@ -140,8 +140,7 @@ export function createTaskService(deps) {
140
140
  return deps.fs.readJson(taskPath);
141
141
  },
142
142
  async saveTask(repoRoot, task) {
143
- const config = await deps.projectService.loadConfig(repoRoot);
144
- await deps.fs.writeJsonAtomic(getTaskPath(repoRoot, config.stateRoot, task.taskSlug), task);
143
+ await deps.fs.writeJsonAtomic(getTaskPath(deps.projectService.getProjectDataRoot(repoRoot), task.taskSlug), task);
145
144
  },
146
145
  async updateTaskStatus(repoRoot, taskSlug, status) {
147
146
  const task = await this.loadTask(repoRoot, taskSlug);
@@ -165,7 +164,7 @@ export function createTaskService(deps) {
165
164
  const config = await deps.projectService.loadConfig(repoRoot);
166
165
  const task = await this.loadTask(repoRoot, taskSlug);
167
166
  const taskRepoRoot = getTaskRuntimeRepoRoot(task);
168
- const statePaths = getTaskStatePaths(repoRoot, taskRepoRoot, config.stateRoot, config.handoffRoot, taskSlug);
167
+ const statePaths = getTaskStatePaths(deps.projectService.getProjectDataRoot(repoRoot), taskRepoRoot, config.stateRoot, config.handoffRoot, taskSlug);
169
168
  const removedStatePaths = [];
170
169
  const cleanedAt = now();
171
170
  if (task.worktreePath) {
@@ -194,8 +193,8 @@ export function createTaskService(deps) {
194
193
  export function getTaskRuntimeRepoRoot(task) {
195
194
  return task.worktreePath ?? task.repoRoot;
196
195
  }
197
- function getTaskPath(repoRoot, stateRoot, taskSlug) {
198
- return path.join(repoRoot, stateRoot, "tasks", `${taskSlug}.json`);
196
+ function getTaskPath(taskStoreRoot, taskSlug) {
197
+ return path.join(taskStoreRoot, "tasks", `${taskSlug}.json`);
199
198
  }
200
199
  function getTaskWorktreePath(repoRoot, taskSlug) {
201
200
  return path.join(repoRoot, ".claude", "worktrees", taskSlug);
@@ -206,8 +205,8 @@ async function ensureTaskRuntimeStateDirs(fs, taskRepoRoot, stateRoot) {
206
205
  await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "orchestration"));
207
206
  await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "translation"));
208
207
  }
209
- async function findActiveInlineTask(fs, repoRoot, stateRoot) {
210
- const tasksDir = path.join(repoRoot, stateRoot, "tasks");
208
+ async function findActiveInlineTask(fs, taskStoreRoot) {
209
+ const tasksDir = path.join(taskStoreRoot, "tasks");
211
210
  if (!(await fs.pathExists(tasksDir))) {
212
211
  return undefined;
213
212
  }
@@ -220,9 +219,9 @@ async function findActiveInlineTask(fs, repoRoot, stateRoot) {
220
219
  }
221
220
  return undefined;
222
221
  }
223
- function getTaskStatePaths(baseRepoRoot, taskRepoRoot, stateRoot, handoffRoot, taskSlug) {
222
+ function getTaskStatePaths(taskStoreRoot, taskRepoRoot, stateRoot, handoffRoot, taskSlug) {
224
223
  return [
225
- path.join(baseRepoRoot, stateRoot, "tasks", `${taskSlug}.json`),
224
+ getTaskPath(taskStoreRoot, taskSlug),
226
225
  path.join(taskRepoRoot, stateRoot, "sessions", `${taskSlug}.json`),
227
226
  path.join(taskRepoRoot, stateRoot, "messages", `${taskSlug}.jsonl`),
228
227
  path.join(taskRepoRoot, stateRoot, "orchestration", `${taskSlug}.json`),
@@ -22,36 +22,16 @@ TBD
22
22
  TBD
23
23
  `;
24
24
  }
25
- export function renderImplementationLogTemplate(taskSlug) {
26
- return `# Implementation Log: ${taskSlug}
25
+ export function renderKnownIssuesTemplate(taskSlug) {
26
+ return `# Known Issues: ${taskSlug}
27
27
 
28
- ## Summary
29
-
30
- TBD
31
-
32
- ## Files Changed
33
-
34
- TBD
35
-
36
- ## Validation
28
+ ## Task Issues
37
29
 
38
- TBD
39
-
40
- ## Deviations From Architecture Plan
41
-
42
- TBD
30
+ No unresolved task issues recorded yet.
43
31
 
44
- ## Follow-ups
32
+ ## Escalation To Docs
45
33
 
46
- TBD
47
- `;
48
- }
49
- export function renderValidationLogTemplate(taskSlug) {
50
- return `# Validation Log: ${taskSlug}
51
-
52
- ## Validation
53
-
54
- Not run yet.
34
+ At task close, promote still-relevant confirmed issues to \`docs/known-issues.md\`; delete this task-local file with the rest of \`.ai/vcm/\` runtime state.
55
35
  `;
56
36
  }
57
37
  export function renderReviewReportTemplate(taskSlug) {
@@ -101,8 +81,66 @@ TBD
101
81
 
102
82
  TBD
103
83
 
84
+ ## Known Issues Disposition
85
+
86
+ TBD
87
+
104
88
  ## Decision
105
89
 
90
+ TBD
91
+ `;
92
+ }
93
+ export function renderFinalAcceptanceTemplate(taskSlug) {
94
+ return `# Final Acceptance: ${taskSlug}
95
+
96
+ ## Decision
97
+
98
+ TBD
99
+
100
+ ## Evidence Reviewed
101
+
102
+ TBD
103
+
104
+ ## Scope Traceability
105
+
106
+ ### Expected Changes
107
+
108
+ TBD
109
+
110
+ ### Supporting Changes
111
+
112
+ TBD
113
+
114
+ ### Approved Deviations
115
+
116
+ TBD
117
+
118
+ ### Unexplained Changes
119
+
120
+ TBD
121
+
122
+ ### High-Risk Unexpected Changes
123
+
124
+ TBD
125
+
126
+ ## Validation Summary
127
+
128
+ TBD
129
+
130
+ ## Review And Docs Sync
131
+
132
+ TBD
133
+
134
+ ## Known Issues Disposition
135
+
136
+ TBD
137
+
138
+ ## Cleanup Readiness
139
+
140
+ TBD
141
+
142
+ ## Final User Summary
143
+
106
144
  TBD
107
145
  `;
108
146
  }