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.
- package/README.md +33 -14
- package/dist/backend/api/claude-hook-routes.js +5 -0
- package/dist/backend/api/message-routes.js +4 -0
- package/dist/backend/api/round-routes.js +33 -0
- package/dist/backend/api/session-routes.js +2 -0
- package/dist/backend/api/task-routes.js +1 -0
- package/dist/backend/server.js +27 -3
- package/dist/backend/services/app-settings-service.js +2 -1
- package/dist/backend/services/claude-hook-service.js +70 -0
- package/dist/backend/services/harness-service.js +95 -0
- package/dist/backend/services/message-service.js +222 -49
- package/dist/backend/services/project-service.js +1 -1
- package/dist/backend/services/round-service.js +117 -0
- package/dist/backend/services/session-service.js +39 -0
- package/dist/backend/services/status-service.js +0 -76
- package/dist/backend/services/task-service.js +14 -6
- package/dist/backend/templates/harness/architect-agent.js +5 -0
- package/dist/backend/templates/harness/claude-root.js +6 -0
- package/dist/backend/templates/harness/coder-agent.js +5 -0
- package/dist/backend/templates/harness/gitignore.js +2 -1
- package/dist/backend/templates/harness/project-manager-agent.js +7 -0
- package/dist/backend/templates/harness/reviewer-agent.js +5 -0
- package/dist/backend/templates/message-envelope.js +3 -1
- package/dist/backend/ws/terminal-ws.js +15 -1
- package/dist/cli/vcmctl.js +30 -0
- package/dist/shared/types/claude-hook.js +1 -0
- package/dist/shared/types/round.js +1 -0
- package/dist-frontend/assets/index-DVhkEVnA.js +89 -0
- package/dist-frontend/assets/index-jEkUTnIY.css +32 -0
- package/dist-frontend/index.html +2 -2
- package/docs/cc-best-practices.md +15 -4
- package/docs/product-design.md +71 -38
- package/docs/v1-architecture-design.md +108 -49
- package/docs/v1-implementation-plan.md +107 -32
- package/package.json +1 -1
- package/dist-frontend/assets/index-Bi4X3GSR.css +0 -32
- 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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return
|
|
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 (
|
|
49
|
-
message
|
|
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
|
-
|
|
161
|
+
if (!isSubmittableMessageStatus(message.status)) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
const submitted = {
|
|
55
165
|
...message,
|
|
56
|
-
status: "
|
|
57
|
-
|
|
166
|
+
status: "submitted",
|
|
167
|
+
submittedAt: now(),
|
|
58
168
|
failureReason: undefined
|
|
59
169
|
};
|
|
60
|
-
await
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
193
|
-
return path.join(repoRoot,
|
|
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,
|
|
226
|
-
const worktreeRoot = path.resolve(repoRoot,
|
|
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
|
`;
|