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.
Files changed (43) hide show
  1. package/README.md +164 -9
  2. package/dist/backend/api/claude-hook-routes.js +7 -1
  3. package/dist/backend/api/gateway-routes.js +17 -0
  4. package/dist/backend/api/round-routes.js +1 -1
  5. package/dist/backend/gateway/channels/weixin-ilink-channel.js +304 -0
  6. package/dist/backend/gateway/gateway-audit-log.js +39 -0
  7. package/dist/backend/gateway/gateway-command-parser.js +77 -0
  8. package/dist/backend/gateway/gateway-service.js +848 -0
  9. package/dist/backend/gateway/gateway-settings-service.js +214 -0
  10. package/dist/backend/server.js +42 -2
  11. package/dist/backend/services/claude-hook-service.js +46 -7
  12. package/dist/backend/services/harness-service.js +37 -32
  13. package/dist/backend/services/job-guard-service.js +126 -0
  14. package/dist/backend/services/round-service.js +110 -64
  15. package/dist/backend/services/session-service.js +10 -4
  16. package/dist/backend/services/task-service.js +32 -3
  17. package/dist/backend/services/translation-service.js +15 -0
  18. package/dist/backend/templates/harness/architect-agent.js +19 -8
  19. package/dist/backend/templates/harness/claude-root.js +7 -11
  20. package/dist/backend/templates/harness/coder-agent.js +45 -17
  21. package/dist/backend/templates/harness/gitignore.js +3 -2
  22. package/dist/backend/templates/harness/project-manager-agent.js +16 -11
  23. package/dist/backend/templates/harness/reviewer-agent.js +25 -28
  24. package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +42 -31
  25. package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +37 -18
  26. package/dist/shared/types/gateway.js +1 -0
  27. package/dist-frontend/assets/{index-CPXFnxAY.css → index-7lq6YPCq.css} +1 -1
  28. package/dist-frontend/assets/index-DHuS-DYr.js +92 -0
  29. package/dist-frontend/index.html +2 -2
  30. package/docs/full-harness-baseline.md +7 -5
  31. package/docs/gateway-design.md +200 -27
  32. package/docs/product-design.md +35 -14
  33. package/docs/v0.2-implementation-plan.md +22 -7
  34. package/docs/vcm-cc-best-practices.md +11 -4
  35. package/package.json +2 -1
  36. package/scripts/harness-tools/run-long-check +401 -0
  37. package/scripts/harness-tools/vcm-bash-guard +107 -0
  38. package/scripts/harness-tools/watch-job +204 -0
  39. package/scripts/install-vcm-harness.mjs +93 -387
  40. package/scripts/uninstall-vcm-harness.mjs +18 -1
  41. package/scripts/verify-package.mjs +3 -0
  42. package/dist/backend/templates/harness/known-issues-doc.js +0 -22
  43. 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
- totalPromptSubmitCount: 0,
22
- totalStopCount: 0,
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 !== "settling" || !current.settleDeadlineAt) {
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 paused = {
52
+ const stopped = {
41
53
  ...current,
42
- status: "paused",
43
- pausedAt: current.settleDeadlineAt
54
+ status: "stopped",
55
+ stoppedAt: current.settleDeadlineAt,
56
+ settleDeadlineAt: undefined
44
57
  };
45
58
  const next = {
46
59
  ...state,
47
- currentRound: paused,
48
- lastPausedRound: paused,
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 !== "settling" ||
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 !== "settling" || !round.settleDeadlineAt) {
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 getTaskRoundState(input) {
152
+ async getSessionRoundState(input) {
136
153
  return withTaskLock(input, async () => {
137
154
  const timestamp = now();
138
- return toTaskRoundState(await load(input), timestamp);
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 === "paused");
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 toTaskRoundState(next, timestamp);
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 === "paused";
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: "active",
206
+ status: "running",
189
207
  activeRole: input.role,
190
208
  startedAt: input.timestamp,
191
- lastPromptSubmittedAt: input.timestamp,
192
- runningSince: input.timestamp,
209
+ lastTurnStartedAt: input.timestamp,
210
+ activeTurnStartedAt: input.timestamp,
193
211
  ccActiveMs: 0,
194
- promptSubmitCount: 1,
195
- stopCount: 0,
212
+ turnCount: 1,
213
+ completedTurnCount: 0,
196
214
  roles: [input.role]
197
215
  }
198
216
  : {
199
217
  ...current,
200
- status: "active",
218
+ status: "running",
201
219
  activeRole: input.role,
202
- lastPromptSubmittedAt: input.timestamp,
220
+ lastTurnStartedAt: input.timestamp,
203
221
  settleDeadlineAt: undefined,
204
- pausedAt: undefined,
205
- runningSince: current.runningSince ?? input.timestamp,
206
- promptSubmitCount: current.promptSubmitCount + 1,
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
- totalPromptSubmitCount: input.state.totalPromptSubmitCount + 1,
215
- totalStopCount: input.state.totalStopCount,
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 === "paused") {
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.runningSince
230
- ? getDurationMs(current.runningSince, input.timestamp)
247
+ const activeDurationMs = current.activeTurnStartedAt
248
+ ? getDurationMs(current.activeTurnStartedAt, input.timestamp)
231
249
  : 0;
232
250
  const nextRound = {
233
251
  ...current,
234
- status: "settling",
252
+ status: "running",
235
253
  activeRole: input.role,
236
- lastStopAt: input.timestamp,
254
+ lastTurnEndedAt: input.timestamp,
237
255
  settleDeadlineAt: addMilliseconds(input.timestamp, input.settleMs),
238
- runningSince: undefined,
256
+ activeTurnStartedAt: undefined,
239
257
  ccActiveMs: current.ccActiveMs + activeDurationMs,
240
- stopCount: current.stopCount + 1,
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
- totalStopCount: input.state.totalStopCount + 1,
265
+ totalCompletedTurnCount: input.state.totalCompletedTurnCount + 1,
248
266
  totalCcActiveMs: input.state.totalCcActiveMs + activeDurationMs,
249
267
  updatedAt: input.timestamp
250
268
  };
251
269
  }
252
- function toTaskRoundState(state, updatedAt) {
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: "idle",
258
- promptSubmitCount: 0,
259
- stopCount: 0,
275
+ status: "stopped",
276
+ turnCount: 0,
277
+ completedTurnCount: 0,
260
278
  totalRoundCount: state.totalRoundCount,
261
- totalPromptSubmitCount: state.totalPromptSubmitCount,
262
- totalStopCount: state.totalStopCount,
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.runningSince
270
- ? getDurationMs(current.runningSince, updatedAt)
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
- lastPromptSubmittedAt: current.lastPromptSubmittedAt,
280
- lastStopAt: current.lastStopAt,
297
+ lastTurnStartedAt: current.lastTurnStartedAt,
298
+ lastTurnEndedAt: current.lastTurnEndedAt,
281
299
  settleDeadlineAt: current.settleDeadlineAt,
282
- pausedAt: current.pausedAt,
283
- runningSince: current.runningSince,
300
+ stoppedAt: current.stoppedAt,
301
+ activeTurnStartedAt: current.activeTurnStartedAt,
284
302
  roundSequence: current.sequence,
285
- promptSubmitCount: current.promptSubmitCount,
286
- stopCount: current.stopCount,
303
+ turnCount: current.turnCount,
304
+ completedTurnCount: current.completedTurnCount,
287
305
  totalRoundCount: state.totalRoundCount,
288
- totalPromptSubmitCount: state.totalPromptSubmitCount,
289
- totalStopCount: state.totalStopCount,
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
- lastPausedRound: normalizeRound(input.lastPausedRound),
320
+ lastStoppedRound: normalizeRound(input.lastStoppedRound ?? legacy.lastPausedRound),
302
321
  totalRoundCount: normalizeNumber(input.totalRoundCount),
303
- totalPromptSubmitCount: normalizeNumber(input.totalPromptSubmitCount),
304
- totalStopCount: normalizeNumber(input.totalStopCount),
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
- if (input.status !== "active" && input.status !== "settling" && input.status !== "paused") {
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: input.status,
340
+ status,
320
341
  activeRole: input.activeRole,
321
342
  startedAt: input.startedAt,
322
- lastPromptSubmittedAt: input.lastPromptSubmittedAt,
323
- lastStopAt: input.lastStopAt,
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
- pausedAt: input.pausedAt,
326
- runningSince: input.runningSince,
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
- promptSubmitCount: Number.isFinite(input.promptSubmitCount) ? input.promptSubmitCount : 0,
329
- stopCount: Number.isFinite(input.stopCount) ? input.stopCount : 0,
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: "pause" };
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
- lastStopAt: isStop ? timestamp : current.lastStopAt,
175
- lastPromptSubmittedAt: isStop ? current.lastPromptSubmittedAt : timestamp,
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
- lastPromptSubmittedAt: timestamp,
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 `## VCM Architect Rules
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 \`CLAUDE.md\`, the role command, durable plans when present, relevant handoff artifacts, and affected project docs before planning.
15
- - Use \`docs/ARCHITECTURE.md\`, \`docs/MODULE_MAP.md\`, \`docs/SECURITY.md\`, and \`docs/DEPENDENCY_RULES.md\` as durable project truth when relevant.
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
- - Check whether final code changed durable project truth: architecture, module map, public contracts, security constraints, dependency rules, or durable plans.
43
- - Update affected durable docs when project truth changed; otherwise state which docs were checked and why they remain current.
44
- - Treat reviewer-reported docs gaps as inputs; resolve technical docs drift or report conflicts back to project-manager.
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
  }