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.
- package/README.md +74 -41
- package/dist/backend/api/artifact-routes.js +5 -5
- package/dist/backend/api/harness-routes.js +8 -0
- package/dist/backend/api/message-routes.js +4 -4
- package/dist/backend/api/round-routes.js +4 -2
- package/dist/backend/server.js +8 -2
- package/dist/backend/services/artifact-service.js +12 -12
- package/dist/backend/services/claude-hook-service.js +1 -1
- package/dist/backend/services/harness-service.js +579 -5
- package/dist/backend/services/message-service.js +71 -137
- package/dist/backend/services/project-service.js +4 -1
- package/dist/backend/services/round-service.js +14 -52
- package/dist/backend/services/session-service.js +1 -3
- package/dist/backend/services/task-service.js +16 -17
- package/dist/backend/templates/handoff.js +64 -26
- package/dist/backend/templates/harness/architect-agent.js +42 -12
- package/dist/backend/templates/harness/claude-root.js +42 -18
- package/dist/backend/templates/harness/coder-agent.js +15 -11
- package/dist/backend/templates/harness/known-issues-doc.js +22 -0
- package/dist/backend/templates/harness/project-manager-agent.js +66 -15
- package/dist/backend/templates/harness/pull-request-template.js +29 -0
- package/dist/backend/templates/harness/reviewer-agent.js +40 -12
- package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +105 -0
- package/dist/backend/templates/harness/vcm-harness-bootstrap-skill.js +78 -0
- package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +50 -0
- package/dist/backend/templates/harness/vcm-route-message-skill.js +86 -0
- package/dist/backend/templates/message-envelope.js +1 -0
- package/dist/backend/templates/role-command.js +7 -1
- package/dist/shared/validation/artifact-check.js +14 -9
- package/dist-frontend/assets/index-CrY5Ryps.js +90 -0
- package/dist-frontend/assets/index-CvvtrrCN.css +32 -0
- package/dist-frontend/index.html +2 -2
- package/docs/cc-best-practices.md +434 -192
- package/docs/full-harness-baseline.md +254 -0
- package/docs/product-design.md +31 -28
- package/docs/v0.2-implementation-plan.md +379 -0
- package/docs/vcm-cc-best-practices.md +449 -0
- package/package.json +3 -1
- package/scripts/harness-tools/generate-module-index +298 -0
- package/scripts/harness-tools/generate-public-surface +692 -0
- package/scripts/install-vcm-harness.mjs +1607 -0
- package/scripts/uninstall-vcm-harness.mjs +490 -0
- package/scripts/verify-package.mjs +4 -0
- package/dist-frontend/assets/index-CvtyKEfS.js +0 -89
- package/dist-frontend/assets/index-jEkUTnIY.css +0 -32
- package/docs/v1-architecture-design.md +0 -1009
- 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
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
44
|
+
if (result.message) {
|
|
45
|
+
await appendMessageSnapshot(deps.fs, input, result.message);
|
|
46
|
+
}
|
|
76
47
|
if (result.delivered) {
|
|
77
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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(
|
|
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:
|
|
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 =
|
|
131
|
+
const message = findDeliveredMessageForPrompt(messages, input.role, messageId);
|
|
178
132
|
if (!message) {
|
|
179
133
|
return undefined;
|
|
180
134
|
}
|
|
181
|
-
const
|
|
135
|
+
const accepted = {
|
|
182
136
|
...message,
|
|
183
|
-
|
|
184
|
-
submittedAt: timestamp,
|
|
185
|
-
queuedBehindMessageId: undefined,
|
|
137
|
+
acceptedAt: timestamp,
|
|
186
138
|
failureReason: undefined
|
|
187
139
|
};
|
|
188
|
-
await appendMessageSnapshot(deps.fs, input,
|
|
189
|
-
await clearRouteFileIfStillMatchesMessage(deps.fs, input,
|
|
190
|
-
return
|
|
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
|
-
|
|
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:
|
|
218
|
-
messages
|
|
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
|
|
405
|
-
|
|
406
|
-
.
|
|
407
|
-
|
|
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,
|
|
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
|
-
|
|
9
|
+
pendingRouteCount: input.pendingRouteCount,
|
|
13
10
|
roleStates,
|
|
14
11
|
updatedAt: now()
|
|
15
12
|
});
|
|
16
13
|
return {
|
|
17
14
|
...state,
|
|
18
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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:
|
|
51
|
-
activeRole:
|
|
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 =
|
|
57
|
-
|
|
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:
|
|
75
|
-
activeRole:
|
|
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
|
|
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}
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
198
|
-
return path.join(
|
|
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,
|
|
210
|
-
const tasksDir = path.join(
|
|
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(
|
|
222
|
+
function getTaskStatePaths(taskStoreRoot, taskRepoRoot, stateRoot, handoffRoot, taskSlug) {
|
|
224
223
|
return [
|
|
225
|
-
|
|
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
|
|
26
|
-
return `#
|
|
25
|
+
export function renderKnownIssuesTemplate(taskSlug) {
|
|
26
|
+
return `# Known Issues: ${taskSlug}
|
|
27
27
|
|
|
28
|
-
##
|
|
29
|
-
|
|
30
|
-
TBD
|
|
31
|
-
|
|
32
|
-
## Files Changed
|
|
33
|
-
|
|
34
|
-
TBD
|
|
35
|
-
|
|
36
|
-
## Validation
|
|
28
|
+
## Task Issues
|
|
37
29
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
## Deviations From Architecture Plan
|
|
41
|
-
|
|
42
|
-
TBD
|
|
30
|
+
No unresolved task issues recorded yet.
|
|
43
31
|
|
|
44
|
-
##
|
|
32
|
+
## Escalation To Docs
|
|
45
33
|
|
|
46
|
-
|
|
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
|
}
|