vibe-coding-master 0.2.6 → 0.2.8
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 +164 -9
- package/dist/backend/api/claude-hook-routes.js +7 -1
- package/dist/backend/api/gateway-routes.js +17 -0
- package/dist/backend/api/round-routes.js +1 -1
- package/dist/backend/gateway/channels/weixin-ilink-channel.js +304 -0
- package/dist/backend/gateway/gateway-audit-log.js +39 -0
- package/dist/backend/gateway/gateway-command-parser.js +77 -0
- package/dist/backend/gateway/gateway-service.js +848 -0
- package/dist/backend/gateway/gateway-settings-service.js +214 -0
- package/dist/backend/server.js +42 -2
- package/dist/backend/services/claude-hook-service.js +46 -7
- package/dist/backend/services/harness-service.js +37 -32
- package/dist/backend/services/job-guard-service.js +126 -0
- package/dist/backend/services/round-service.js +110 -64
- package/dist/backend/services/session-service.js +10 -4
- package/dist/backend/services/task-service.js +32 -3
- package/dist/backend/services/translation-service.js +15 -0
- package/dist/backend/templates/harness/architect-agent.js +19 -8
- package/dist/backend/templates/harness/claude-root.js +7 -11
- package/dist/backend/templates/harness/coder-agent.js +45 -17
- package/dist/backend/templates/harness/gitignore.js +3 -2
- package/dist/backend/templates/harness/project-manager-agent.js +16 -11
- package/dist/backend/templates/harness/reviewer-agent.js +25 -28
- package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +42 -31
- package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +37 -18
- package/dist/shared/types/gateway.js +1 -0
- package/dist-frontend/assets/{index-CPXFnxAY.css → index-7lq6YPCq.css} +1 -1
- package/dist-frontend/assets/index-DHuS-DYr.js +92 -0
- package/dist-frontend/index.html +2 -2
- package/docs/full-harness-baseline.md +7 -5
- package/docs/gateway-design.md +200 -27
- package/docs/product-design.md +35 -14
- package/docs/v0.2-implementation-plan.md +22 -7
- package/docs/vcm-cc-best-practices.md +11 -4
- package/package.json +2 -1
- package/scripts/harness-tools/run-long-check +401 -0
- package/scripts/harness-tools/vcm-bash-guard +107 -0
- package/scripts/harness-tools/watch-job +204 -0
- package/scripts/install-vcm-harness.mjs +93 -387
- package/scripts/uninstall-vcm-harness.mjs +18 -1
- package/scripts/verify-package.mjs +3 -0
- package/dist/backend/templates/harness/known-issues-doc.js +0 -22
- package/dist-frontend/assets/index-D0_02lmQ.js +0 -90
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const ACTIVE_JOB_STATUSES = new Set(["queued", "starting", "running"]);
|
|
4
|
+
const QUEUED_JOB_FRESH_MS = 120_000;
|
|
5
|
+
export const MAX_CONSECUTIVE_STOP_BLOCKS = 3;
|
|
6
|
+
export function createJobGuardService(deps = {}) {
|
|
7
|
+
const isProcessAlive = deps.isProcessAlive ?? defaultIsProcessAlive;
|
|
8
|
+
const now = deps.now ?? Date.now;
|
|
9
|
+
const blockStates = new Map();
|
|
10
|
+
async function findActiveJobs(taskRepoRoot) {
|
|
11
|
+
const jobsRoot = path.join(taskRepoRoot, ".ai/vcm/jobs");
|
|
12
|
+
let entries;
|
|
13
|
+
try {
|
|
14
|
+
entries = await fs.readdir(jobsRoot);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
const jobs = [];
|
|
20
|
+
for (const entry of entries.sort()) {
|
|
21
|
+
const statusPath = path.join(jobsRoot, entry, "status.json");
|
|
22
|
+
let status;
|
|
23
|
+
try {
|
|
24
|
+
status = JSON.parse(await fs.readFile(statusPath, "utf8"));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (typeof status.status !== "string" || !ACTIVE_JOB_STATUSES.has(status.status)) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const processId = numberOrUndefined(status.processId);
|
|
33
|
+
const workerPid = numberOrUndefined(status.workerPid);
|
|
34
|
+
const pid = processId ?? workerPid;
|
|
35
|
+
if (pid !== undefined) {
|
|
36
|
+
if (!isProcessAlive(pid)) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// queued entry whose worker has not reported a pid yet: only trust it briefly
|
|
42
|
+
try {
|
|
43
|
+
const stat = await fs.stat(statusPath);
|
|
44
|
+
if (now() - stat.mtimeMs > QUEUED_JOB_FRESH_MS) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
let leaseMtimeMs;
|
|
53
|
+
try {
|
|
54
|
+
leaseMtimeMs = (await fs.stat(path.join(jobsRoot, entry, "lease"))).mtimeMs;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
leaseMtimeMs = undefined;
|
|
58
|
+
}
|
|
59
|
+
jobs.push({
|
|
60
|
+
jobId: typeof status.jobId === "string" ? status.jobId : entry,
|
|
61
|
+
status: status.status,
|
|
62
|
+
startedAt: typeof status.startedAt === "string" ? status.startedAt : undefined,
|
|
63
|
+
timeoutSeconds: numberOrUndefined(status.timeoutSeconds),
|
|
64
|
+
processId,
|
|
65
|
+
workerPid,
|
|
66
|
+
leaseMtimeMs
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return jobs;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
findActiveJobs,
|
|
73
|
+
async evaluateStop(input) {
|
|
74
|
+
const key = stateKey(input);
|
|
75
|
+
const jobs = await findActiveJobs(input.taskRepoRoot);
|
|
76
|
+
if (jobs.length === 0) {
|
|
77
|
+
blockStates.delete(key);
|
|
78
|
+
return { behavior: "allow" };
|
|
79
|
+
}
|
|
80
|
+
const leaseMtimeMs = jobs.reduce((latest, job) => job.leaseMtimeMs !== undefined && (latest === undefined || job.leaseMtimeMs > latest)
|
|
81
|
+
? job.leaseMtimeMs
|
|
82
|
+
: latest, undefined);
|
|
83
|
+
let state = blockStates.get(key) ?? { count: 0 };
|
|
84
|
+
const watcherProgressed = state.count > 0
|
|
85
|
+
&& leaseMtimeMs !== undefined
|
|
86
|
+
&& state.lastLeaseMtimeMs !== undefined
|
|
87
|
+
&& leaseMtimeMs > state.lastLeaseMtimeMs;
|
|
88
|
+
if (watcherProgressed) {
|
|
89
|
+
state = { count: 0 };
|
|
90
|
+
}
|
|
91
|
+
if (state.count >= MAX_CONSECUTIVE_STOP_BLOCKS) {
|
|
92
|
+
// The role keeps trying to stop without watching; let it stop so the
|
|
93
|
+
// round can settle. The job worker lease will reap the job itself.
|
|
94
|
+
blockStates.delete(key);
|
|
95
|
+
return { behavior: "allow" };
|
|
96
|
+
}
|
|
97
|
+
blockStates.set(key, { count: state.count + 1, lastLeaseMtimeMs: leaseMtimeMs });
|
|
98
|
+
return { behavior: "block", reason: buildBlockReason(jobs) };
|
|
99
|
+
},
|
|
100
|
+
notePromptSubmitted(input) {
|
|
101
|
+
blockStates.delete(stateKey(input));
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function stateKey(input) {
|
|
106
|
+
return `${input.repoRoot}::${input.taskSlug}::${input.role}`;
|
|
107
|
+
}
|
|
108
|
+
function buildBlockReason(jobs) {
|
|
109
|
+
const first = jobs[0];
|
|
110
|
+
const listing = jobs.map((job) => `${job.jobId} (${job.status})`).join(", ");
|
|
111
|
+
return `VCM: validation job ${listing} is still running. Do not end the turn while a validation job is running. `
|
|
112
|
+
+ `Run \`.ai/tools/watch-job ${first.jobId}\` again now and keep watching until it reports a terminal result `
|
|
113
|
+
+ `(success, failed, timeout, or orphaned), then record the result.`;
|
|
114
|
+
}
|
|
115
|
+
function numberOrUndefined(value) {
|
|
116
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
117
|
+
}
|
|
118
|
+
function defaultIsProcessAlive(pid) {
|
|
119
|
+
try {
|
|
120
|
+
process.kill(pid, 0);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
return error.code === "EPERM";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -18,8 +18,8 @@ export function createRoundService(deps) {
|
|
|
18
18
|
version: 1,
|
|
19
19
|
taskSlug: input.taskSlug,
|
|
20
20
|
totalRoundCount: 0,
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
totalTurnCount: 0,
|
|
22
|
+
totalCompletedTurnCount: 0,
|
|
23
23
|
totalCcActiveMs: 0,
|
|
24
24
|
updatedAt: now()
|
|
25
25
|
};
|
|
@@ -29,26 +29,40 @@ export function createRoundService(deps) {
|
|
|
29
29
|
async function save(input, state) {
|
|
30
30
|
await deps.fs.writeJsonAtomic(getRoundStatePath(input), state);
|
|
31
31
|
}
|
|
32
|
+
async function updateSessionStatus(input, status) {
|
|
33
|
+
if (!input.repoRoot || !deps.onSessionStatusChange) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
await deps.onSessionStatusChange({
|
|
37
|
+
repoRoot: input.repoRoot,
|
|
38
|
+
taskSlug: input.taskSlug,
|
|
39
|
+
status
|
|
40
|
+
});
|
|
41
|
+
}
|
|
32
42
|
async function settleIfNeeded(input, state, timestamp) {
|
|
33
43
|
const current = state.currentRound;
|
|
34
|
-
if (current?.status !== "
|
|
44
|
+
if (current?.status !== "running" ||
|
|
45
|
+
!current.settleDeadlineAt ||
|
|
46
|
+
current.activeTurnStartedAt) {
|
|
35
47
|
return state;
|
|
36
48
|
}
|
|
37
49
|
if (timestamp < current.settleDeadlineAt) {
|
|
38
50
|
return state;
|
|
39
51
|
}
|
|
40
|
-
const
|
|
52
|
+
const stopped = {
|
|
41
53
|
...current,
|
|
42
|
-
status: "
|
|
43
|
-
|
|
54
|
+
status: "stopped",
|
|
55
|
+
stoppedAt: current.settleDeadlineAt,
|
|
56
|
+
settleDeadlineAt: undefined
|
|
44
57
|
};
|
|
45
58
|
const next = {
|
|
46
59
|
...state,
|
|
47
|
-
currentRound:
|
|
48
|
-
|
|
60
|
+
currentRound: stopped,
|
|
61
|
+
lastStoppedRound: stopped,
|
|
49
62
|
updatedAt: timestamp
|
|
50
63
|
};
|
|
51
64
|
await save(input, next);
|
|
65
|
+
await updateSessionStatus(input, "stopped");
|
|
52
66
|
return next;
|
|
53
67
|
}
|
|
54
68
|
async function settleIfStillCurrent(input, roundId, settleDeadlineAt, settleGuard) {
|
|
@@ -56,8 +70,9 @@ export function createRoundService(deps) {
|
|
|
56
70
|
const state = await load(input);
|
|
57
71
|
const current = state.currentRound;
|
|
58
72
|
if (current?.id !== roundId ||
|
|
59
|
-
current.status !== "
|
|
60
|
-
current.settleDeadlineAt !== settleDeadlineAt
|
|
73
|
+
current.status !== "running" ||
|
|
74
|
+
current.settleDeadlineAt !== settleDeadlineAt ||
|
|
75
|
+
current.activeTurnStartedAt) {
|
|
61
76
|
return;
|
|
62
77
|
}
|
|
63
78
|
const timestamp = maxIsoTimestamp(now(), settleDeadlineAt);
|
|
@@ -105,7 +120,9 @@ export function createRoundService(deps) {
|
|
|
105
120
|
}
|
|
106
121
|
}
|
|
107
122
|
function scheduleSettleTimer(input, round, timestamp, settleGuard) {
|
|
108
|
-
if (round.status !== "
|
|
123
|
+
if (round.status !== "running" ||
|
|
124
|
+
!round.settleDeadlineAt ||
|
|
125
|
+
round.activeTurnStartedAt) {
|
|
109
126
|
clearSettleTimer(input);
|
|
110
127
|
return;
|
|
111
128
|
}
|
|
@@ -132,10 +149,10 @@ export function createRoundService(deps) {
|
|
|
132
149
|
}
|
|
133
150
|
}
|
|
134
151
|
return {
|
|
135
|
-
async
|
|
152
|
+
async getSessionRoundState(input) {
|
|
136
153
|
return withTaskLock(input, async () => {
|
|
137
154
|
const timestamp = now();
|
|
138
|
-
return
|
|
155
|
+
return toSessionRoundState(await load(input), timestamp);
|
|
139
156
|
});
|
|
140
157
|
},
|
|
141
158
|
async recordClaudeHookEvent(input) {
|
|
@@ -143,7 +160,7 @@ export function createRoundService(deps) {
|
|
|
143
160
|
const timestamp = now();
|
|
144
161
|
const settled = await settleIfNeeded(input, await load(input), timestamp);
|
|
145
162
|
const current = settled.currentRound;
|
|
146
|
-
const shouldStartNewRound = input.eventName === "UserPromptSubmit" && (!current || current.status === "
|
|
163
|
+
const shouldStartNewRound = input.eventName === "UserPromptSubmit" && (!current || current.status === "stopped");
|
|
147
164
|
const next = applyRoundHookEvent({
|
|
148
165
|
state: settled,
|
|
149
166
|
taskSlug: input.taskSlug,
|
|
@@ -156,11 +173,12 @@ export function createRoundService(deps) {
|
|
|
156
173
|
await save(input, next);
|
|
157
174
|
if (input.eventName === "UserPromptSubmit") {
|
|
158
175
|
clearSettleTimer(input);
|
|
176
|
+
await updateSessionStatus(input, "running");
|
|
159
177
|
}
|
|
160
178
|
else if (next.currentRound) {
|
|
161
179
|
scheduleSettleTimer(input, next.currentRound, timestamp, input.settleGuard);
|
|
162
180
|
}
|
|
163
|
-
return
|
|
181
|
+
return toSessionRoundState(next, timestamp);
|
|
164
182
|
});
|
|
165
183
|
},
|
|
166
184
|
stopSession() { },
|
|
@@ -177,7 +195,7 @@ export function applyRoundHookEvent(input) {
|
|
|
177
195
|
}
|
|
178
196
|
function applyPromptSubmitted(input) {
|
|
179
197
|
const current = input.state.currentRound;
|
|
180
|
-
const shouldStartNewRound = !current || current.status === "
|
|
198
|
+
const shouldStartNewRound = !current || current.status === "stopped";
|
|
181
199
|
const totalRoundCount = shouldStartNewRound
|
|
182
200
|
? input.state.totalRoundCount + 1
|
|
183
201
|
: input.state.totalRoundCount;
|
|
@@ -185,25 +203,25 @@ function applyPromptSubmitted(input) {
|
|
|
185
203
|
? {
|
|
186
204
|
id: input.roundId,
|
|
187
205
|
sequence: totalRoundCount,
|
|
188
|
-
status: "
|
|
206
|
+
status: "running",
|
|
189
207
|
activeRole: input.role,
|
|
190
208
|
startedAt: input.timestamp,
|
|
191
|
-
|
|
192
|
-
|
|
209
|
+
lastTurnStartedAt: input.timestamp,
|
|
210
|
+
activeTurnStartedAt: input.timestamp,
|
|
193
211
|
ccActiveMs: 0,
|
|
194
|
-
|
|
195
|
-
|
|
212
|
+
turnCount: 1,
|
|
213
|
+
completedTurnCount: 0,
|
|
196
214
|
roles: [input.role]
|
|
197
215
|
}
|
|
198
216
|
: {
|
|
199
217
|
...current,
|
|
200
|
-
status: "
|
|
218
|
+
status: "running",
|
|
201
219
|
activeRole: input.role,
|
|
202
|
-
|
|
220
|
+
lastTurnStartedAt: input.timestamp,
|
|
203
221
|
settleDeadlineAt: undefined,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
222
|
+
stoppedAt: undefined,
|
|
223
|
+
activeTurnStartedAt: current.activeTurnStartedAt ?? input.timestamp,
|
|
224
|
+
turnCount: current.turnCount + 1,
|
|
207
225
|
roles: appendUniqueRole(current.roles, input.role)
|
|
208
226
|
};
|
|
209
227
|
return {
|
|
@@ -211,63 +229,63 @@ function applyPromptSubmitted(input) {
|
|
|
211
229
|
taskSlug: input.taskSlug,
|
|
212
230
|
currentRound: nextRound,
|
|
213
231
|
totalRoundCount,
|
|
214
|
-
|
|
215
|
-
|
|
232
|
+
totalTurnCount: input.state.totalTurnCount + 1,
|
|
233
|
+
totalCompletedTurnCount: input.state.totalCompletedTurnCount,
|
|
216
234
|
totalCcActiveMs: input.state.totalCcActiveMs,
|
|
217
235
|
updatedAt: input.timestamp
|
|
218
236
|
};
|
|
219
237
|
}
|
|
220
238
|
function applyStop(input) {
|
|
221
239
|
const current = input.state.currentRound;
|
|
222
|
-
if (!current || current.status === "
|
|
240
|
+
if (!current || current.status === "stopped" || !current.activeTurnStartedAt) {
|
|
223
241
|
return {
|
|
224
242
|
...input.state,
|
|
225
243
|
taskSlug: input.taskSlug,
|
|
226
244
|
updatedAt: input.timestamp
|
|
227
245
|
};
|
|
228
246
|
}
|
|
229
|
-
const activeDurationMs = current.
|
|
230
|
-
? getDurationMs(current.
|
|
247
|
+
const activeDurationMs = current.activeTurnStartedAt
|
|
248
|
+
? getDurationMs(current.activeTurnStartedAt, input.timestamp)
|
|
231
249
|
: 0;
|
|
232
250
|
const nextRound = {
|
|
233
251
|
...current,
|
|
234
|
-
status: "
|
|
252
|
+
status: "running",
|
|
235
253
|
activeRole: input.role,
|
|
236
|
-
|
|
254
|
+
lastTurnEndedAt: input.timestamp,
|
|
237
255
|
settleDeadlineAt: addMilliseconds(input.timestamp, input.settleMs),
|
|
238
|
-
|
|
256
|
+
activeTurnStartedAt: undefined,
|
|
239
257
|
ccActiveMs: current.ccActiveMs + activeDurationMs,
|
|
240
|
-
|
|
258
|
+
completedTurnCount: current.completedTurnCount + 1,
|
|
241
259
|
roles: appendUniqueRole(current.roles, input.role)
|
|
242
260
|
};
|
|
243
261
|
return {
|
|
244
262
|
...input.state,
|
|
245
263
|
taskSlug: input.taskSlug,
|
|
246
264
|
currentRound: nextRound,
|
|
247
|
-
|
|
265
|
+
totalCompletedTurnCount: input.state.totalCompletedTurnCount + 1,
|
|
248
266
|
totalCcActiveMs: input.state.totalCcActiveMs + activeDurationMs,
|
|
249
267
|
updatedAt: input.timestamp
|
|
250
268
|
};
|
|
251
269
|
}
|
|
252
|
-
function
|
|
270
|
+
function toSessionRoundState(state, updatedAt) {
|
|
253
271
|
const current = state.currentRound;
|
|
254
272
|
if (!current) {
|
|
255
273
|
return {
|
|
256
274
|
taskSlug: state.taskSlug,
|
|
257
|
-
status: "
|
|
258
|
-
|
|
259
|
-
|
|
275
|
+
status: "stopped",
|
|
276
|
+
turnCount: 0,
|
|
277
|
+
completedTurnCount: 0,
|
|
260
278
|
totalRoundCount: state.totalRoundCount,
|
|
261
|
-
|
|
262
|
-
|
|
279
|
+
totalTurnCount: state.totalTurnCount,
|
|
280
|
+
totalCompletedTurnCount: state.totalCompletedTurnCount,
|
|
263
281
|
totalCcActiveMs: state.totalCcActiveMs,
|
|
264
282
|
currentRoundCcActiveMs: 0,
|
|
265
283
|
roles: [],
|
|
266
284
|
updatedAt
|
|
267
285
|
};
|
|
268
286
|
}
|
|
269
|
-
const activeDurationMs = current.
|
|
270
|
-
? getDurationMs(current.
|
|
287
|
+
const activeDurationMs = current.activeTurnStartedAt
|
|
288
|
+
? getDurationMs(current.activeTurnStartedAt, updatedAt)
|
|
271
289
|
: 0;
|
|
272
290
|
const currentRoundCcActiveMs = current.ccActiveMs + activeDurationMs;
|
|
273
291
|
return {
|
|
@@ -276,17 +294,17 @@ function toTaskRoundState(state, updatedAt) {
|
|
|
276
294
|
roundId: current.id,
|
|
277
295
|
activeRole: current.activeRole,
|
|
278
296
|
startedAt: current.startedAt,
|
|
279
|
-
|
|
280
|
-
|
|
297
|
+
lastTurnStartedAt: current.lastTurnStartedAt,
|
|
298
|
+
lastTurnEndedAt: current.lastTurnEndedAt,
|
|
281
299
|
settleDeadlineAt: current.settleDeadlineAt,
|
|
282
|
-
|
|
283
|
-
|
|
300
|
+
stoppedAt: current.stoppedAt,
|
|
301
|
+
activeTurnStartedAt: current.activeTurnStartedAt,
|
|
284
302
|
roundSequence: current.sequence,
|
|
285
|
-
|
|
286
|
-
|
|
303
|
+
turnCount: current.turnCount,
|
|
304
|
+
completedTurnCount: current.completedTurnCount,
|
|
287
305
|
totalRoundCount: state.totalRoundCount,
|
|
288
|
-
|
|
289
|
-
|
|
306
|
+
totalTurnCount: state.totalTurnCount,
|
|
307
|
+
totalCompletedTurnCount: state.totalCompletedTurnCount,
|
|
290
308
|
totalCcActiveMs: state.totalCcActiveMs + activeDurationMs,
|
|
291
309
|
currentRoundCcActiveMs,
|
|
292
310
|
roles: current.roles,
|
|
@@ -294,14 +312,15 @@ function toTaskRoundState(state, updatedAt) {
|
|
|
294
312
|
};
|
|
295
313
|
}
|
|
296
314
|
function normalizeRoundFile(input, taskSlug, updatedAt) {
|
|
315
|
+
const legacy = input;
|
|
297
316
|
return {
|
|
298
317
|
version: 1,
|
|
299
318
|
taskSlug,
|
|
300
319
|
currentRound: normalizeRound(input.currentRound),
|
|
301
|
-
|
|
320
|
+
lastStoppedRound: normalizeRound(input.lastStoppedRound ?? legacy.lastPausedRound),
|
|
302
321
|
totalRoundCount: normalizeNumber(input.totalRoundCount),
|
|
303
|
-
|
|
304
|
-
|
|
322
|
+
totalTurnCount: normalizeNumber(input.totalTurnCount ?? legacy.totalPromptSubmitCount),
|
|
323
|
+
totalCompletedTurnCount: normalizeNumber(input.totalCompletedTurnCount ?? legacy.totalStopCount),
|
|
305
324
|
totalCcActiveMs: normalizeNumber(input.totalCcActiveMs),
|
|
306
325
|
updatedAt: typeof input.updatedAt === "string" ? input.updatedAt : updatedAt
|
|
307
326
|
};
|
|
@@ -310,26 +329,53 @@ function normalizeRound(input) {
|
|
|
310
329
|
if (!input || typeof input.id !== "string") {
|
|
311
330
|
return undefined;
|
|
312
331
|
}
|
|
313
|
-
|
|
332
|
+
const status = normalizeRoundStatus(input.status);
|
|
333
|
+
if (!status) {
|
|
314
334
|
return undefined;
|
|
315
335
|
}
|
|
336
|
+
const legacy = input;
|
|
316
337
|
return {
|
|
317
338
|
id: input.id,
|
|
318
339
|
sequence: Number.isFinite(input.sequence) ? input.sequence : 1,
|
|
319
|
-
status
|
|
340
|
+
status,
|
|
320
341
|
activeRole: input.activeRole,
|
|
321
342
|
startedAt: input.startedAt,
|
|
322
|
-
|
|
323
|
-
|
|
343
|
+
lastTurnStartedAt: typeof input.lastTurnStartedAt === "string"
|
|
344
|
+
? input.lastTurnStartedAt
|
|
345
|
+
: typeof legacy.lastPromptSubmittedAt === "string"
|
|
346
|
+
? legacy.lastPromptSubmittedAt
|
|
347
|
+
: undefined,
|
|
348
|
+
lastTurnEndedAt: typeof input.lastTurnEndedAt === "string"
|
|
349
|
+
? input.lastTurnEndedAt
|
|
350
|
+
: typeof legacy.lastStopAt === "string"
|
|
351
|
+
? legacy.lastStopAt
|
|
352
|
+
: undefined,
|
|
324
353
|
settleDeadlineAt: input.settleDeadlineAt,
|
|
325
|
-
|
|
326
|
-
|
|
354
|
+
stoppedAt: typeof input.stoppedAt === "string"
|
|
355
|
+
? input.stoppedAt
|
|
356
|
+
: typeof legacy.pausedAt === "string"
|
|
357
|
+
? legacy.pausedAt
|
|
358
|
+
: undefined,
|
|
359
|
+
activeTurnStartedAt: typeof input.activeTurnStartedAt === "string"
|
|
360
|
+
? input.activeTurnStartedAt
|
|
361
|
+
: typeof legacy.runningSince === "string"
|
|
362
|
+
? legacy.runningSince
|
|
363
|
+
: undefined,
|
|
327
364
|
ccActiveMs: Number.isFinite(input.ccActiveMs) ? input.ccActiveMs : 0,
|
|
328
|
-
|
|
329
|
-
|
|
365
|
+
turnCount: normalizeNumber(input.turnCount ?? legacy.promptSubmitCount),
|
|
366
|
+
completedTurnCount: normalizeNumber(input.completedTurnCount ?? legacy.stopCount),
|
|
330
367
|
roles: Array.isArray(input.roles) ? input.roles : []
|
|
331
368
|
};
|
|
332
369
|
}
|
|
370
|
+
function normalizeRoundStatus(value) {
|
|
371
|
+
if (value === "running" || value === "active" || value === "settling") {
|
|
372
|
+
return "running";
|
|
373
|
+
}
|
|
374
|
+
if (value === "stopped" || value === "paused") {
|
|
375
|
+
return "stopped";
|
|
376
|
+
}
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
333
379
|
function appendUniqueRole(roles, role) {
|
|
334
380
|
return roles.includes(role) ? roles : [...roles, role];
|
|
335
381
|
}
|
|
@@ -344,7 +390,7 @@ async function runSettleGuard(settleGuard, input) {
|
|
|
344
390
|
return await settleGuard(input);
|
|
345
391
|
}
|
|
346
392
|
catch {
|
|
347
|
-
return { action: "
|
|
393
|
+
return { action: "stop" };
|
|
348
394
|
}
|
|
349
395
|
}
|
|
350
396
|
function getDurationMs(start, end) {
|
|
@@ -78,7 +78,6 @@ export function createSessionService(deps) {
|
|
|
78
78
|
};
|
|
79
79
|
deps.registry.upsert(record);
|
|
80
80
|
await persistTaskSession(deps.fs, taskRepoRoot, config.stateRoot, record);
|
|
81
|
-
await deps.taskService.updateTaskStatus(repoRoot, taskSlug, "running");
|
|
82
81
|
return record;
|
|
83
82
|
}
|
|
84
83
|
return {
|
|
@@ -171,8 +170,8 @@ export function createSessionService(deps) {
|
|
|
171
170
|
...current,
|
|
172
171
|
activityStatus: isStop ? "idle" : "running",
|
|
173
172
|
lastHookEventAt: timestamp,
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
lastTurnEndedAt: isStop ? timestamp : current.lastTurnEndedAt,
|
|
174
|
+
lastTurnStartedAt: isStop ? current.lastTurnStartedAt : timestamp,
|
|
176
175
|
updatedAt: timestamp
|
|
177
176
|
};
|
|
178
177
|
deps.registry.upsert(updated);
|
|
@@ -190,7 +189,7 @@ export function createSessionService(deps) {
|
|
|
190
189
|
const updated = {
|
|
191
190
|
...current,
|
|
192
191
|
activityStatus: "running",
|
|
193
|
-
|
|
192
|
+
lastTurnStartedAt: timestamp,
|
|
194
193
|
lastHookEventAt: timestamp,
|
|
195
194
|
updatedAt: timestamp
|
|
196
195
|
};
|
|
@@ -239,9 +238,16 @@ async function loadPersistedRoleRecord(fs, repoRoot, stateRoot, taskSlug, role)
|
|
|
239
238
|
}
|
|
240
239
|
const current = await fs.readJson(sessionPath);
|
|
241
240
|
const record = current.roles[role]?.record;
|
|
241
|
+
const legacy = record;
|
|
242
242
|
return record
|
|
243
243
|
? {
|
|
244
244
|
...record,
|
|
245
|
+
lastTurnStartedAt: record.lastTurnStartedAt ?? (typeof legacy?.lastPromptSubmittedAt === "string"
|
|
246
|
+
? legacy.lastPromptSubmittedAt
|
|
247
|
+
: undefined),
|
|
248
|
+
lastTurnEndedAt: record.lastTurnEndedAt ?? (typeof legacy?.lastStopAt === "string"
|
|
249
|
+
? legacy.lastStopAt
|
|
250
|
+
: undefined),
|
|
245
251
|
permissionMode: normalizeClaudePermissionMode(record.permissionMode),
|
|
246
252
|
model: normalizeClaudeModel(record.model)
|
|
247
253
|
}
|
|
@@ -123,7 +123,7 @@ export function createTaskService(deps) {
|
|
|
123
123
|
const entries = await deps.fs.readDir(tasksDir);
|
|
124
124
|
const tasks = [];
|
|
125
125
|
for (const entry of entries.filter((candidate) => candidate.endsWith(".json"))) {
|
|
126
|
-
tasks.push(await deps.fs.readJson(path.join(tasksDir, entry)));
|
|
126
|
+
tasks.push(normalizeTaskRecord(await deps.fs.readJson(path.join(tasksDir, entry))));
|
|
127
127
|
}
|
|
128
128
|
return tasks.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
129
129
|
},
|
|
@@ -137,7 +137,7 @@ export function createTaskService(deps) {
|
|
|
137
137
|
statusCode: 404
|
|
138
138
|
});
|
|
139
139
|
}
|
|
140
|
-
return deps.fs.readJson(taskPath);
|
|
140
|
+
return normalizeTaskRecord(await deps.fs.readJson(taskPath));
|
|
141
141
|
},
|
|
142
142
|
async saveTask(repoRoot, task) {
|
|
143
143
|
await deps.fs.writeJsonAtomic(getTaskPath(deps.projectService.getProjectDataRoot(repoRoot), task.taskSlug), task);
|
|
@@ -212,13 +212,42 @@ async function findActiveInlineTask(fs, taskStoreRoot) {
|
|
|
212
212
|
}
|
|
213
213
|
const entries = await fs.readDir(tasksDir);
|
|
214
214
|
for (const entry of entries.filter((candidate) => candidate.endsWith(".json"))) {
|
|
215
|
-
const task = await fs.readJson(path.join(tasksDir, entry));
|
|
215
|
+
const task = normalizeTaskRecord(await fs.readJson(path.join(tasksDir, entry)));
|
|
216
216
|
if (!task.worktreePath && task.cleanupStatus !== "cleaned") {
|
|
217
217
|
return task;
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
return undefined;
|
|
221
221
|
}
|
|
222
|
+
function normalizeTaskRecord(input) {
|
|
223
|
+
return {
|
|
224
|
+
version: 1,
|
|
225
|
+
taskSlug: input.taskSlug ?? "",
|
|
226
|
+
title: input.title,
|
|
227
|
+
createdAt: input.createdAt ?? "",
|
|
228
|
+
updatedAt: input.updatedAt ?? input.createdAt ?? "",
|
|
229
|
+
repoRoot: input.repoRoot ?? "",
|
|
230
|
+
worktreePath: input.worktreePath,
|
|
231
|
+
branch: input.branch ?? "",
|
|
232
|
+
handoffDir: input.handoffDir ?? ".ai/vcm/handoffs",
|
|
233
|
+
status: normalizeTaskStatus(input.status),
|
|
234
|
+
specPath: input.specPath,
|
|
235
|
+
cleanupStatus: input.cleanupStatus,
|
|
236
|
+
cleanedAt: input.cleanedAt
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function normalizeTaskStatus(value) {
|
|
240
|
+
if (value === "created" || value === "running" || value === "stopped") {
|
|
241
|
+
return value;
|
|
242
|
+
}
|
|
243
|
+
if (value === undefined || value === null) {
|
|
244
|
+
return "created";
|
|
245
|
+
}
|
|
246
|
+
if (value === "planning") {
|
|
247
|
+
return "created";
|
|
248
|
+
}
|
|
249
|
+
return "stopped";
|
|
250
|
+
}
|
|
222
251
|
function getTaskStatePaths(taskStoreRoot, taskRepoRoot, stateRoot, handoffRoot, taskSlug) {
|
|
223
252
|
return [
|
|
224
253
|
getTaskPath(taskStoreRoot, taskSlug),
|
|
@@ -780,6 +780,21 @@ export function createTranslationService(deps) {
|
|
|
780
780
|
publishFailures(sessionId);
|
|
781
781
|
}
|
|
782
782
|
return { failures: [] };
|
|
783
|
+
},
|
|
784
|
+
async translateGatewayOutput(input) {
|
|
785
|
+
const { settings, secrets } = await loadConfig();
|
|
786
|
+
const prompt = buildTranslationPrompt({
|
|
787
|
+
direction: "cc-output-to-user",
|
|
788
|
+
text: input.text,
|
|
789
|
+
settings
|
|
790
|
+
});
|
|
791
|
+
const result = await deps.provider.translate({
|
|
792
|
+
settings,
|
|
793
|
+
secrets,
|
|
794
|
+
systemPrompt: prompt.systemPrompt,
|
|
795
|
+
userPrompt: prompt.userPrompt
|
|
796
|
+
});
|
|
797
|
+
return result.text.trim();
|
|
783
798
|
}
|
|
784
799
|
};
|
|
785
800
|
async function writeToCurrentRole(repoRoot, taskSlug, role, text) {
|
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
export function renderArchitectHarnessRules() {
|
|
2
|
-
return
|
|
2
|
+
return `
|
|
3
|
+
## VCM Architect Rules
|
|
3
4
|
|
|
4
5
|
### Role Scope
|
|
5
6
|
|
|
6
7
|
- Own technical analysis, architecture planning, module boundaries, file responsibilities, public contracts, verifiable behavior, phase boundaries, behavior/contract proof points, risks, and Replan triggers.
|
|
8
|
+
- Own \`docs/known-issues.md\` promotion and durable issue updates.
|
|
9
|
+
- Own architecture docs sync across \`docs/ARCHITECTURE.md\` and affected \`<module>/ARCHITECTURE.md\` files.
|
|
7
10
|
- Do not implement production code.
|
|
8
11
|
- Do not design complete test cases, coverage matrices, or final validation strategy; reviewer owns independent test design, test adequacy, and validation confidence.
|
|
9
12
|
- Do not make product priority or approval decisions; route those questions back to project-manager.
|
|
10
|
-
- Reply to project-manager with the \`vcm-route-message\` skill.
|
|
11
13
|
|
|
12
14
|
### Planning Inputs
|
|
13
15
|
|
|
14
|
-
- Read
|
|
15
|
-
-
|
|
16
|
+
- Read the role message, durable plans when present, relevant handoff artifacts, \`docs/ARCHITECTURE.md\`, affected \`<module>/ARCHITECTURE.md\` files when present, and affected project docs before planning.
|
|
17
|
+
- Read \`.ai/generated/module-index.json\` when planning module scope, file scope, dependency direction, or phased work.
|
|
18
|
+
- Read \`.ai/generated/public-surface.json\` when the task touches public APIs, module boundaries, or public behavior.
|
|
16
19
|
- If durable docs conflict with the requested plan or code reality, report the conflict to project-manager and identify whether user approval is required.
|
|
17
20
|
|
|
18
21
|
### Architecture Plan
|
|
19
22
|
|
|
20
23
|
- Write \`.ai/vcm/handoffs/architecture-plan.md\` before coder work starts.
|
|
21
|
-
- The plan must cover changed files, file responsibilities, public interfaces or user-visible behavior, required behavior or contract proof points, docs impact, risks, and Replan triggers.
|
|
24
|
+
- The plan must cover changed files, task-local file responsibilities, affected modules, public interfaces or user-visible behavior, required behavior or contract proof points, docs impact, risks, and Replan triggers.
|
|
25
|
+
- For docs impact, state whether changes belong in \`docs/ARCHITECTURE.md\`, affected \`<module>/ARCHITECTURE.md\`, \`.ai/generated/public-surface.json\`, or no durable architecture doc.
|
|
22
26
|
- Keep implementation work scoped to what can be safely described, validated, reviewed, and handed off.
|
|
23
27
|
|
|
24
28
|
### Phase Planning
|
|
@@ -39,10 +43,17 @@ export function renderArchitectHarnessRules() {
|
|
|
39
43
|
### Docs Sync
|
|
40
44
|
|
|
41
45
|
- Perform docs sync only when project-manager requests it after reviewer completes.
|
|
42
|
-
-
|
|
43
|
-
- Update affected
|
|
44
|
-
- Treat
|
|
46
|
+
- Update \`docs/ARCHITECTURE.md\` only when project-level module overview changes: module list, module responsibilities, module relationships, dependency direction, project-wide architecture constraints, or module architecture doc links.
|
|
47
|
+
- Update affected \`<module>/ARCHITECTURE.md\` when module-level detailed design changes: boundaries, behavior, important public surface explanations, internal risks, or module-specific architecture notes.
|
|
48
|
+
- Treat \`.ai/generated/public-surface.json\` as the full machine index for public surface. Verify or report its freshness when public APIs changed; do not replace it with prose in architecture docs.
|
|
49
|
+
- When module structure changes, require \`.ai/tools/generate-module-index --check\` or regeneration.
|
|
50
|
+
- When crate-external public APIs change, require \`.ai/tools/generate-public-surface --check\` or regeneration.
|
|
45
51
|
- Read \`.ai/vcm/handoffs/known-issues.md\` and promote confirmed unresolved issues to \`docs/known-issues.md\`.
|
|
46
52
|
- Write \`.ai/vcm/handoffs/docs-sync-report.md\` with decision, evidence reviewed, architecture drift check, docs updated, docs left unchanged, remaining documentation risks, and handoff notes.
|
|
53
|
+
|
|
54
|
+
### Background Jobs
|
|
55
|
+
|
|
56
|
+
- Never background a Bash command: no \`run_in_background\`, \`nohup\`, \`setsid\`, \`disown\`, or trailing \`&\`.
|
|
57
|
+
- For any command that may exceed 2 minutes, use the \`vcm-long-running-validation\` skill and stay in the turn, re-running \`.ai/tools/watch-job\` until it reports a terminal result.
|
|
47
58
|
`;
|
|
48
59
|
}
|