vibe-coding-master 0.0.12 → 0.0.14

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 (37) hide show
  1. package/README.md +33 -14
  2. package/dist/backend/api/claude-hook-routes.js +5 -0
  3. package/dist/backend/api/message-routes.js +4 -0
  4. package/dist/backend/api/round-routes.js +33 -0
  5. package/dist/backend/api/session-routes.js +2 -0
  6. package/dist/backend/api/task-routes.js +1 -0
  7. package/dist/backend/server.js +27 -3
  8. package/dist/backend/services/app-settings-service.js +2 -1
  9. package/dist/backend/services/claude-hook-service.js +70 -0
  10. package/dist/backend/services/harness-service.js +95 -0
  11. package/dist/backend/services/message-service.js +222 -49
  12. package/dist/backend/services/project-service.js +1 -1
  13. package/dist/backend/services/round-service.js +117 -0
  14. package/dist/backend/services/session-service.js +39 -0
  15. package/dist/backend/services/status-service.js +0 -76
  16. package/dist/backend/services/task-service.js +14 -6
  17. package/dist/backend/templates/harness/architect-agent.js +5 -0
  18. package/dist/backend/templates/harness/claude-root.js +6 -0
  19. package/dist/backend/templates/harness/coder-agent.js +5 -0
  20. package/dist/backend/templates/harness/gitignore.js +2 -1
  21. package/dist/backend/templates/harness/project-manager-agent.js +7 -0
  22. package/dist/backend/templates/harness/reviewer-agent.js +5 -0
  23. package/dist/backend/templates/message-envelope.js +3 -1
  24. package/dist/backend/ws/terminal-ws.js +15 -1
  25. package/dist/cli/vcmctl.js +30 -0
  26. package/dist/shared/types/claude-hook.js +1 -0
  27. package/dist/shared/types/round.js +1 -0
  28. package/dist-frontend/assets/index-DVhkEVnA.js +89 -0
  29. package/dist-frontend/assets/index-jEkUTnIY.css +32 -0
  30. package/dist-frontend/index.html +2 -2
  31. package/docs/cc-best-practices.md +15 -4
  32. package/docs/product-design.md +71 -38
  33. package/docs/v1-architecture-design.md +108 -49
  34. package/docs/v1-implementation-plan.md +107 -32
  35. package/package.json +1 -1
  36. package/dist-frontend/assets/index-Bi4X3GSR.css +0 -32
  37. package/dist-frontend/assets/index-DaHXq14j.js +0 -88
@@ -8,58 +8,191 @@ import { renderManualStagePrompt, renderMessageEnvelope } from "../templates/mes
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 MARK_ALL_DONE_STATUSES = new Set([
12
+ "pending_approval",
13
+ "queued",
14
+ "staged",
15
+ "delivering",
16
+ "submitted",
17
+ "delivered",
18
+ "failed",
19
+ "delivery_failed",
20
+ "response_ready_missing_result"
21
+ ]);
11
22
  export function createMessageService(deps) {
12
23
  const now = deps.now ?? (() => new Date().toISOString());
13
24
  const id = deps.id ?? (() => `msg_${randomUUID()}`);
25
+ const taskLocks = new Map();
26
+ async function getOrchestrationState(input) {
27
+ const statePath = getOrchestrationStatePath(getStateRepoRoot(input), input.stateRoot, input.taskSlug);
28
+ if (!(await deps.fs.pathExists(statePath))) {
29
+ return {
30
+ taskSlug: input.taskSlug,
31
+ mode: "manual",
32
+ paused: false,
33
+ updatedAt: now()
34
+ };
35
+ }
36
+ return deps.fs.readJson(statePath);
37
+ }
38
+ async function sendMessageLocked(input) {
39
+ await deps.taskService.loadTask(input.repoRoot, input.taskSlug);
40
+ validateMessagePolicy(input.fromRole, input.toRole, input.type);
41
+ const timestamp = now();
42
+ const message = {
43
+ id: id(),
44
+ taskSlug: input.taskSlug,
45
+ fromRole: input.fromRole,
46
+ toRole: input.toRole,
47
+ type: input.type,
48
+ body: input.body,
49
+ artifactRefs: input.artifactRefs ?? [],
50
+ parentMessageId: input.parentMessageId,
51
+ status: "queued",
52
+ createdAt: timestamp
53
+ };
54
+ message.bodyPath = await writeMessageBody(deps.fs, input.taskRepoRoot ?? input.repoRoot, input.handoffDir, message);
55
+ let messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
56
+ if (isRoleActor(input.fromRole)) {
57
+ const acknowledged = acknowledgeActiveMessages(messages, input.fromRole, timestamp);
58
+ for (const acknowledgedMessage of acknowledged) {
59
+ await appendMessageSnapshot(deps.fs, input, acknowledgedMessage);
60
+ }
61
+ messages = applyMessageSnapshots(messages, acknowledged);
62
+ }
63
+ const state = await getOrchestrationState(input);
64
+ const result = await routeOutboundMessage(input, message, messages, state, timestamp);
65
+ await appendMessageSnapshot(deps.fs, input, result.message);
66
+ if (isRoleActor(input.fromRole)) {
67
+ await deliverNextQueuedMessage(input, input.fromRole);
68
+ }
69
+ return result;
70
+ }
71
+ async function routeOutboundMessage(input, message, existingMessages, state, timestamp) {
72
+ const session = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, input.toRole);
73
+ if (!session || session.status !== "running") {
74
+ const queued = {
75
+ ...message,
76
+ status: "queued",
77
+ failureReason: `${input.toRole} session is not running.`
78
+ };
79
+ return { message: queued, delivered: false, requiresUserApproval: false };
80
+ }
81
+ if (state.mode === "manual") {
82
+ const pending = {
83
+ ...message,
84
+ status: "pending_approval"
85
+ };
86
+ return { message: pending, delivered: false, requiresUserApproval: true };
87
+ }
88
+ if (state.paused) {
89
+ const queued = {
90
+ ...message,
91
+ status: "queued",
92
+ failureReason: "Auto orchestration is paused."
93
+ };
94
+ return { message: queued, delivered: false, requiresUserApproval: false };
95
+ }
96
+ const activeMessage = findActiveMessageForRole(existingMessages, input.toRole);
97
+ if (activeMessage) {
98
+ const queued = {
99
+ ...message,
100
+ status: "queued",
101
+ queuedBehindMessageId: activeMessage.id,
102
+ failureReason: `${input.toRole} is still handling VCM message ${activeMessage.id}.`
103
+ };
104
+ return { message: queued, delivered: false, requiresUserApproval: false };
105
+ }
106
+ const delivered = await deliverMessageToSession(session.id, message, timestamp);
107
+ return { message: delivered, delivered: true, requiresUserApproval: false };
108
+ }
109
+ async function deliverNextQueuedMessage(input, targetRole) {
110
+ const state = await getOrchestrationState(input);
111
+ if (state.mode !== "auto" || state.paused) {
112
+ return undefined;
113
+ }
114
+ const messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
115
+ if (findActiveMessageForRole(messages, targetRole)) {
116
+ return undefined;
117
+ }
118
+ const nextMessage = messages.find((message) => message.toRole === targetRole && message.status === "queued");
119
+ if (!nextMessage) {
120
+ return undefined;
121
+ }
122
+ const session = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, targetRole);
123
+ if (!session || session.status !== "running") {
124
+ return undefined;
125
+ }
126
+ const delivered = await deliverMessageToSession(session.id, nextMessage, now());
127
+ await appendMessageSnapshot(deps.fs, input, delivered);
128
+ return delivered;
129
+ }
130
+ async function deliverMessageToSession(sessionId, message, timestamp) {
131
+ const delivering = {
132
+ ...message,
133
+ status: "delivering",
134
+ deliveredAt: timestamp,
135
+ queuedBehindMessageId: undefined,
136
+ failureReason: undefined
137
+ };
138
+ await submitTerminalInput(deps.runtime, sessionId, renderMessageEnvelope(delivering));
139
+ return delivering;
140
+ }
14
141
  return {
15
142
  listMessages(input) {
16
143
  return readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
17
144
  },
18
145
  async sendMessage(input) {
19
- await deps.taskService.loadTask(input.repoRoot, input.taskSlug);
20
- validateMessagePolicy(input.fromRole, input.toRole, input.type);
21
- const timestamp = now();
22
- const message = {
23
- id: id(),
24
- taskSlug: input.taskSlug,
25
- fromRole: input.fromRole,
26
- toRole: input.toRole,
27
- type: input.type,
28
- body: input.body,
29
- artifactRefs: input.artifactRefs ?? [],
30
- parentMessageId: input.parentMessageId,
31
- status: "queued",
32
- createdAt: timestamp
33
- };
34
- message.bodyPath = await writeMessageBody(deps.fs, input.taskRepoRoot ?? input.repoRoot, input.handoffDir, message);
35
- const state = await this.getOrchestrationState(input);
36
- const session = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, input.toRole);
37
- if (!session || session.status !== "running") {
38
- message.status = "queued";
39
- message.failureReason = `${input.toRole} session is not running.`;
40
- await appendMessageSnapshot(deps.fs, input, message);
41
- return { message, delivered: false, requiresUserApproval: false };
146
+ return withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), () => sendMessageLocked(input));
147
+ },
148
+ async confirmPromptSubmitted(input) {
149
+ const messageId = extractSubmittedMessageId(input.prompt);
150
+ if (!messageId) {
151
+ return undefined;
42
152
  }
43
- if (state.mode === "manual") {
44
- message.status = "pending_approval";
45
- await appendMessageSnapshot(deps.fs, input, message);
46
- return { message, delivered: false, requiresUserApproval: true };
153
+ const messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
154
+ const message = messages.find((candidate) => candidate.id === messageId && candidate.toRole === input.role);
155
+ if (!message) {
156
+ return undefined;
47
157
  }
48
- if (state.paused) {
49
- message.status = "queued";
50
- message.failureReason = "Auto orchestration is paused.";
51
- await appendMessageSnapshot(deps.fs, input, message);
52
- return { message, delivered: false, requiresUserApproval: false };
158
+ if (message.status === "submitted" || message.status === "acknowledged") {
159
+ return message;
53
160
  }
54
- const delivered = {
161
+ if (!isSubmittableMessageStatus(message.status)) {
162
+ return undefined;
163
+ }
164
+ const submitted = {
55
165
  ...message,
56
- status: "delivered",
57
- deliveredAt: timestamp,
166
+ status: "submitted",
167
+ submittedAt: now(),
58
168
  failureReason: undefined
59
169
  };
60
- await submitTerminalInput(deps.runtime, session.id, renderMessageEnvelope(delivered));
61
- await appendMessageSnapshot(deps.fs, input, delivered);
62
- return { message: delivered, delivered: true, requiresUserApproval: false };
170
+ await appendMessageSnapshot(deps.fs, input, submitted);
171
+ return submitted;
172
+ },
173
+ async markAllDone(input) {
174
+ return withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), async () => {
175
+ const timestamp = now();
176
+ const messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
177
+ const updated = messages
178
+ .filter((message) => MARK_ALL_DONE_STATUSES.has(message.status))
179
+ .map((message) => ({
180
+ ...message,
181
+ status: "acknowledged",
182
+ acknowledgedAt: timestamp,
183
+ queuedBehindMessageId: undefined,
184
+ failureReason: undefined
185
+ }));
186
+ for (const message of updated) {
187
+ await appendMessageSnapshot(deps.fs, input, message);
188
+ }
189
+ const latest = applyMessageSnapshots(messages, updated);
190
+ return {
191
+ taskSlug: input.taskSlug,
192
+ updatedCount: updated.length,
193
+ messages: latest
194
+ };
195
+ });
63
196
  },
64
197
  async stageMessage(input) {
65
198
  const message = await getMessageOrThrow(deps.fs, input);
@@ -96,19 +229,10 @@ export function createMessageService(deps) {
96
229
  return rejected;
97
230
  },
98
231
  async getOrchestrationState(input) {
99
- const statePath = getOrchestrationStatePath(getStateRepoRoot(input), input.stateRoot, input.taskSlug);
100
- if (!(await deps.fs.pathExists(statePath))) {
101
- return {
102
- taskSlug: input.taskSlug,
103
- mode: "manual",
104
- paused: false,
105
- updatedAt: now()
106
- };
107
- }
108
- return deps.fs.readJson(statePath);
232
+ return getOrchestrationState(input);
109
233
  },
110
234
  async updateOrchestrationState(input) {
111
- const current = await this.getOrchestrationState(input);
235
+ const current = await getOrchestrationState(input);
112
236
  const next = {
113
237
  ...current,
114
238
  mode: input.mode ?? current.mode,
@@ -158,6 +282,55 @@ function validateMessagePolicy(fromRole, toRole, type) {
158
282
  hint: "Use project-manager as the orchestration hub."
159
283
  });
160
284
  }
285
+ async function withTaskLock(locks, key, run) {
286
+ const previous = locks.get(key) ?? Promise.resolve();
287
+ const next = previous.catch(() => undefined).then(run);
288
+ locks.set(key, next);
289
+ try {
290
+ return await next;
291
+ }
292
+ finally {
293
+ if (locks.get(key) === next) {
294
+ locks.delete(key);
295
+ }
296
+ }
297
+ }
298
+ function isRoleActor(actor) {
299
+ return ROLE_NAMES.includes(actor);
300
+ }
301
+ function findActiveMessageForRole(messages, role) {
302
+ return messages.find((message) => message.toRole === role && isActiveMessageStatus(message.status));
303
+ }
304
+ function acknowledgeActiveMessages(messages, role, timestamp) {
305
+ return messages
306
+ .filter((message) => message.toRole === role && isActiveMessageStatus(message.status))
307
+ .map((message) => ({
308
+ ...message,
309
+ status: "acknowledged",
310
+ acknowledgedAt: timestamp,
311
+ failureReason: undefined
312
+ }));
313
+ }
314
+ function isActiveMessageStatus(status) {
315
+ return status === "delivering" || status === "submitted" || status === "delivered" || status === "staged";
316
+ }
317
+ function isSubmittableMessageStatus(status) {
318
+ return status === "delivering" || status === "delivered" || status === "staged" || status === "pending_approval";
319
+ }
320
+ function extractSubmittedMessageId(prompt) {
321
+ const match = prompt.match(/\bmsg_[A-Za-z0-9_-]+\b/);
322
+ return match?.[0];
323
+ }
324
+ function applyMessageSnapshots(messages, snapshots) {
325
+ if (snapshots.length === 0) {
326
+ return messages;
327
+ }
328
+ const latest = new Map(messages.map((message) => [message.id, message]));
329
+ for (const snapshot of snapshots) {
330
+ latest.set(snapshot.id, snapshot);
331
+ }
332
+ return [...latest.values()].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
333
+ }
161
334
  async function writeMessageBody(fs, repoRoot, handoffDir, message) {
162
335
  const bodyPath = path.posix.join(handoffDir, "messages", `${message.id}.md`);
163
336
  await fs.writeText(resolveRepoPath(repoRoot, bodyPath), renderMessageBodyFile(message));
@@ -31,7 +31,7 @@ export function createProjectService(deps) {
31
31
  const config = await this.loadConfig(repoRoot);
32
32
  await deps.fs.ensureDir(path.join(repoRoot, config.handoffRoot));
33
33
  await deps.fs.ensureDir(path.join(repoRoot, config.stateRoot, "tasks"));
34
- await deps.fs.ensureDir(path.join(repoRoot, config.stateRoot, "worktrees"));
34
+ await deps.fs.ensureDir(path.join(repoRoot, ".claude", "worktrees"));
35
35
  await this.saveConfig(config, true);
36
36
  const warnings = [];
37
37
  let branch = "unknown";
@@ -0,0 +1,117 @@
1
+ import { ROLE_NAMES } from "../../shared/constants.js";
2
+ export function createRoundService(deps = {}) {
3
+ const now = deps.now ?? (() => new Date().toISOString());
4
+ return {
5
+ getTaskRoundState(input) {
6
+ const roleStates = ROLE_NAMES.map((role) => toRoleTurnState(role, input.sessions));
7
+ const 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
+ const state = evaluateTaskRoundState({
11
+ taskSlug: input.taskSlug,
12
+ messages: input.messages,
13
+ roleStates,
14
+ updatedAt: now()
15
+ });
16
+ return {
17
+ ...state,
18
+ latestMessageId: latestDelivered?.id,
19
+ latestMessageDeliveredAt: getMessageDeliveredAt(latestDelivered),
20
+ queuedMessageCount,
21
+ pendingMessageCount
22
+ };
23
+ },
24
+ stopSession() { },
25
+ stopTask() { }
26
+ };
27
+ }
28
+ 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
+ }
48
+ return {
49
+ taskSlug: input.taskSlug,
50
+ status: targetState?.status === "waiting_user" ? "waiting_user" : "active",
51
+ activeRole: latestDelivered.toRole,
52
+ roles: input.roleStates,
53
+ updatedAt: input.updatedAt
54
+ };
55
+ }
56
+ const latestIdleRole = !hasQueuedOrPending ? getLatestIdleRole(input.roleStates) : undefined;
57
+ if (latestIdleRole?.lastAnswerEndedAt) {
58
+ return {
59
+ taskSlug: input.taskSlug,
60
+ status: "completed",
61
+ activeRole: latestIdleRole.role,
62
+ completionId: `direct:${latestIdleRole.role}:${latestIdleRole.lastAnswerEndedAt}`,
63
+ completedAt: latestIdleRole.lastAnswerEndedAt,
64
+ roles: input.roleStates,
65
+ updatedAt: input.updatedAt
66
+ };
67
+ }
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
+ return {
73
+ taskSlug: input.taskSlug,
74
+ status: activeRole?.status === "waiting_user" ? "waiting_user" : activeRole ? "active" : "idle",
75
+ activeRole: activeRole?.role,
76
+ roles: input.roleStates,
77
+ updatedAt: input.updatedAt
78
+ };
79
+ }
80
+ function toRoleTurnState(role, sessions) {
81
+ const session = sessions.find((candidate) => candidate.role === role && candidate.status === "running");
82
+ const activityStatus = session?.activityStatus ?? "idle";
83
+ return {
84
+ role,
85
+ sessionId: session?.id,
86
+ status: session ? activityStatus === "running" ? "answering" : "idle" : "unknown",
87
+ pendingToolUseCount: 0,
88
+ lastActivityAt: session?.lastPromptSubmittedAt ?? session?.lastHookEventAt,
89
+ lastAnswerEndedAt: session?.lastStopAt,
90
+ reason: session
91
+ ? activityStatus === "running"
92
+ ? "Claude Code accepted a prompt and has not emitted Stop yet."
93
+ : "Claude Code emitted Stop or has not started a prompt in this process."
94
+ : undefined
95
+ };
96
+ }
97
+ function getLatestDeliveredMessage(messages) {
98
+ return messages
99
+ .filter((message) => message.status === "delivering" || message.status === "submitted" || message.status === "delivered" || message.status === "staged")
100
+ .sort((left, right) => getMessageDeliveredAt(left).localeCompare(getMessageDeliveredAt(right)))
101
+ .at(-1);
102
+ }
103
+ function getMessageDeliveredAt(message) {
104
+ return message?.submittedAt ?? message?.deliveredAt ?? message?.stagedAt ?? 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
+ function getLatestIdleRole(roleStates) {
113
+ return roleStates
114
+ .filter((roleState) => roleState.status === "idle" && roleState.lastAnswerEndedAt)
115
+ .sort((left, right) => (left.lastAnswerEndedAt ?? "").localeCompare(right.lastAnswerEndedAt ?? ""))
116
+ .at(-1);
117
+ }
@@ -57,6 +57,7 @@ export function createSessionService(deps) {
57
57
  taskSlug,
58
58
  role,
59
59
  status: runtimeSession.status,
60
+ activityStatus: "idle",
60
61
  command: startCommand.display,
61
62
  permissionMode,
62
63
  cwd: taskRepoRoot,
@@ -99,6 +100,7 @@ export function createSessionService(deps) {
99
100
  const updated = {
100
101
  ...existing,
101
102
  status: "exited",
103
+ activityStatus: "idle",
102
104
  updatedAt: now()
103
105
  };
104
106
  deps.registry.upsert(updated);
@@ -139,6 +141,7 @@ export function createSessionService(deps) {
139
141
  return {
140
142
  ...record,
141
143
  status: runtimeSession.status,
144
+ activityStatus: record.activityStatus ?? "idle",
142
145
  pid: runtimeSession.pid,
143
146
  lastOutputAt: runtimeSession.lastOutputAt,
144
147
  exitCode: runtimeSession.exitCode
@@ -153,9 +156,45 @@ export function createSessionService(deps) {
153
156
  }
154
157
  }
155
158
  return sessions;
159
+ },
160
+ async recordClaudeHookEvent(repoRoot, input) {
161
+ const current = await this.getRoleSession(repoRoot, input.taskSlug, input.role);
162
+ if (!current || !matchesClaudeHookSession(current, input)) {
163
+ return undefined;
164
+ }
165
+ const timestamp = now();
166
+ const updated = {
167
+ ...current,
168
+ activityStatus: input.eventName === "UserPromptSubmit" ? "running" : "idle",
169
+ lastHookEventAt: timestamp,
170
+ lastPromptSubmittedAt: input.eventName === "UserPromptSubmit"
171
+ ? timestamp
172
+ : current.lastPromptSubmittedAt,
173
+ lastStopAt: input.eventName === "Stop"
174
+ ? timestamp
175
+ : current.lastStopAt,
176
+ updatedAt: timestamp
177
+ };
178
+ deps.registry.upsert(updated);
179
+ const config = await deps.projectService.loadConfig(repoRoot);
180
+ const task = await deps.taskService.loadTask(repoRoot, input.taskSlug);
181
+ await persistTaskSession(deps.fs, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
182
+ return updated;
156
183
  }
157
184
  };
158
185
  }
186
+ function matchesClaudeHookSession(record, input) {
187
+ if (input.claudeSessionId && record.claudeSessionId === input.claudeSessionId) {
188
+ return true;
189
+ }
190
+ if (input.transcriptPath && record.transcriptPath === input.transcriptPath) {
191
+ return true;
192
+ }
193
+ if (!input.claudeSessionId && !input.transcriptPath) {
194
+ return true;
195
+ }
196
+ return false;
197
+ }
159
198
  function getRecoverableStatus(record) {
160
199
  if (!record.claudeSessionId) {
161
200
  return record.status === "running" ? "missing" : record.status;
@@ -16,84 +16,8 @@ export function createStatusService(deps) {
16
16
  task,
17
17
  sessions,
18
18
  artifacts,
19
- workflow: buildWorkflowReport(artifacts, sessions),
20
19
  warnings
21
20
  };
22
21
  }
23
22
  };
24
23
  }
25
- function buildWorkflowReport(artifacts, sessions) {
26
- const isComplete = (kind) => artifacts.checks.find((check) => check.kind === kind)?.status === "ok";
27
- const roleIsRunning = (role) => sessions.some((session) => session.role === role && session.status === "running");
28
- const architectureComplete = isComplete("architecture-plan");
29
- const implementationComplete = isComplete("implementation-log") && isComplete("validation-log");
30
- const reviewComplete = isComplete("review-report");
31
- const docsSyncComplete = isComplete("docs-sync-report");
32
- const steps = [
33
- {
34
- id: "architecture-plan",
35
- label: "Architecture",
36
- role: "architect",
37
- artifactPaths: [artifacts.paths.architecturePlanPath],
38
- status: architectureComplete ? "complete" : "ready",
39
- detail: architectureComplete
40
- ? "architecture-plan.md is ready."
41
- : roleIsRunning("architect")
42
- ? "Architect is running; produce architecture-plan.md before coder work."
43
- : "Start architect and produce architecture-plan.md before coder work."
44
- },
45
- {
46
- id: "implementation",
47
- label: "Implementation",
48
- role: "coder",
49
- artifactPaths: [artifacts.paths.implementationLogPath, artifacts.paths.validationLogPath],
50
- status: implementationComplete ? "complete" : architectureComplete ? "ready" : "blocked",
51
- detail: implementationComplete
52
- ? "implementation-log.md and validation-log.md are ready."
53
- : architectureComplete
54
- ? "Start coder, then update implementation-log.md and validation-log.md."
55
- : "Blocked until architecture-plan.md is complete."
56
- },
57
- {
58
- id: "review",
59
- label: "Review",
60
- role: "reviewer",
61
- artifactPaths: [artifacts.paths.reviewReportPath],
62
- status: reviewComplete ? "complete" : implementationComplete ? "ready" : "blocked",
63
- detail: reviewComplete
64
- ? "review-report.md is ready."
65
- : implementationComplete
66
- ? "Start reviewer for independent review and final test adequacy."
67
- : "Blocked until implementation-log.md and validation-log.md are complete."
68
- },
69
- {
70
- id: "docs-sync",
71
- label: "Docs Sync",
72
- role: "architect",
73
- artifactPaths: [artifacts.paths.docsSyncReportPath],
74
- status: docsSyncComplete ? "complete" : reviewComplete ? "ready" : "blocked",
75
- detail: docsSyncComplete
76
- ? "docs-sync-report.md is ready."
77
- : reviewComplete
78
- ? "Send architect a docs-sync / architecture drift check task."
79
- : "Blocked until review-report.md is complete."
80
- },
81
- {
82
- id: "final-acceptance",
83
- label: "PM Final",
84
- role: "project-manager",
85
- artifactPaths: [],
86
- status: docsSyncComplete ? "ready" : "blocked",
87
- detail: docsSyncComplete
88
- ? "Project Manager can prepare final acceptance, commit, and PR."
89
- : "Blocked until architect docs sync is complete."
90
- }
91
- ];
92
- const current = steps.find((step) => step.status !== "complete") ?? steps[steps.length - 1];
93
- return {
94
- currentStepId: current.id,
95
- nextAction: current.detail,
96
- blocked: current.status === "blocked",
97
- steps
98
- };
99
- }
@@ -13,7 +13,7 @@ export function createTaskService(deps) {
13
13
  ? `feature/${input.taskSlug}`
14
14
  : await deps.git.getCurrentBranch(repoRoot);
15
15
  const worktreePath = shouldCreateWorktree
16
- ? getTaskWorktreePath(repoRoot, config.stateRoot, input.taskSlug)
16
+ ? getTaskWorktreePath(repoRoot, input.taskSlug)
17
17
  : undefined;
18
18
  if (await deps.fs.pathExists(taskPath)) {
19
19
  throw new VcmError({
@@ -30,6 +30,14 @@ export function createTaskService(deps) {
30
30
  hint: "Apply VCM Harness first so .gitignore contains the VCM managed block."
31
31
  });
32
32
  }
33
+ if (shouldCreateWorktree && !(await deps.git.isIgnored(repoRoot, ".claude/worktrees/.probe"))) {
34
+ throw new VcmError({
35
+ code: "VCM_WORKTREES_NOT_IGNORED",
36
+ message: ".claude/worktrees/ is not ignored by Git.",
37
+ statusCode: 409,
38
+ hint: "Apply VCM Harness first so .gitignore ignores Claude-compatible task worktrees."
39
+ });
40
+ }
33
41
  if (!shouldCreateWorktree) {
34
42
  const activeInlineTask = await findActiveInlineTask(deps.fs, repoRoot, config.stateRoot);
35
43
  if (activeInlineTask) {
@@ -161,7 +169,7 @@ export function createTaskService(deps) {
161
169
  const removedStatePaths = [];
162
170
  const cleanedAt = now();
163
171
  if (task.worktreePath) {
164
- assertTaskWorktreePath(repoRoot, config.stateRoot, task.worktreePath);
172
+ assertTaskWorktreePath(repoRoot, task.worktreePath);
165
173
  await deps.git.removeWorktree(repoRoot, task.worktreePath, { force: options.force ?? true });
166
174
  }
167
175
  let deletedBranch;
@@ -189,8 +197,8 @@ export function getTaskRuntimeRepoRoot(task) {
189
197
  function getTaskPath(repoRoot, stateRoot, taskSlug) {
190
198
  return path.join(repoRoot, stateRoot, "tasks", `${taskSlug}.json`);
191
199
  }
192
- function getTaskWorktreePath(repoRoot, stateRoot, taskSlug) {
193
- return path.join(repoRoot, stateRoot, "worktrees", taskSlug);
200
+ function getTaskWorktreePath(repoRoot, taskSlug) {
201
+ return path.join(repoRoot, ".claude", "worktrees", taskSlug);
194
202
  }
195
203
  async function ensureTaskRuntimeStateDirs(fs, taskRepoRoot, stateRoot) {
196
204
  await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "sessions"));
@@ -222,8 +230,8 @@ function getTaskStatePaths(baseRepoRoot, taskRepoRoot, stateRoot, handoffRoot, t
222
230
  path.join(taskRepoRoot, handoffRoot)
223
231
  ];
224
232
  }
225
- function assertTaskWorktreePath(repoRoot, stateRoot, worktreePath) {
226
- const worktreeRoot = path.resolve(repoRoot, stateRoot, "worktrees");
233
+ function assertTaskWorktreePath(repoRoot, worktreePath) {
234
+ const worktreeRoot = path.resolve(repoRoot, ".claude", "worktrees");
227
235
  const resolvedWorktreePath = path.resolve(worktreePath);
228
236
  const relative = path.relative(worktreeRoot, resolvedWorktreePath);
229
237
  if (relative.startsWith("..") || path.isAbsolute(relative) || relative === "") {
@@ -8,5 +8,10 @@ export function renderArchitectHarnessRules() {
8
8
  - Update stale architecture/module/testing/security/dependency docs when the final code made them stale.
9
9
  - Write docs-sync-report.md with docs changed, docs intentionally left unchanged, remaining documentation risks, and decision.
10
10
  - Stop and reply to project-manager if implementation drift changes architecture, public contracts, dependency direction, schema, auth, permission, payment, or design assumptions.
11
+ - Reply to project-manager once per received VCM message when complete, blocked, or unclear; do not send fragmented progress updates unless project-manager explicitly requested them.
12
+ - If you need to send a VCM message, send at most one message to any single target role in the current Claude Code turn, then end the turn.
13
+ - After a successful vcmctl reply or vcmctl result, end the turn immediately. Do not run vcmctl inbox, poll, loop, or keep working while waiting for project-manager to answer.
14
+ - Do not wait in a loop for another role to answer. VCM will deliver later replies in a new turn.
15
+ - Do not use Claude Code Task/Subagent for VCM role delegation; communicate through vcmctl only.
11
16
  `;
12
17
  }
@@ -8,6 +8,12 @@ export function renderRootClaudeHarnessRules() {
8
8
  - Do not create or write task handoffs outside .ai/vcm/handoffs/ for the current task.
9
9
  - Use vcmctl for role-to-role messaging instead of asking the user to copy prompts.
10
10
  - Non-PM roles only reply to project-manager; they do not message other roles directly.
11
+ - Role messaging is turn-based: do not send more than one active message to the same target role.
12
+ - After sending a message to a role, wait for that role to reply with vcmctl reply or vcmctl result before sending another message to the same role.
13
+ - After any successful vcmctl send, vcmctl reply, or vcmctl result, end the current Claude Code turn. Treat the command as the final coordination action of this turn.
14
+ - Do not run vcmctl inbox, poll files, start shell loops, or keep the turn open waiting for another role's answer. VCM will deliver replies in a later turn.
15
+ - Do not use Claude Code Task/Subagent for VCM role delegation; VCM owns the four role sessions and the message queue.
16
+ - If new information arrives while a role is still processing, update the relevant handoff artifact or wait; do not spam the target role's terminal.
11
17
  - High-risk decisions involving schema, auth, permissions, payment, billing, security, data deletion, or unclear user intent must stop for project-manager/user approval.
12
18
  - Required workflow gates: architect plan -> coder implementation/validation -> reviewer review -> architect docs sync -> project-manager final acceptance/commit/PR.
13
19
  `;