vibe-coding-master 0.7.17 → 0.7.19

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.
@@ -38,6 +38,7 @@ const VALIDATION_ANALYSIS_FIELDS = [
38
38
  "Public Contract Coverage",
39
39
  "Test Integrity",
40
40
  "Skips And Gaps",
41
+ "User Approval And Gap Disposition",
41
42
  "Validation Readiness"
42
43
  ];
43
44
  const CODE_DIFF_ANALYSIS_FIELDS = [
@@ -61,6 +62,8 @@ const SOURCE_ARTIFACTS = {
61
62
  ],
62
63
  "validation-adequacy": [
63
64
  ".ai/vcm/handoffs/architecture-plan.md",
65
+ ".ai/vcm/handoffs/architect-debug.md",
66
+ ".ai/vcm/handoffs/architecture-diagnosis.md",
64
67
  ".ai/vcm/handoffs/test-report.md",
65
68
  "docs/TESTING.md"
66
69
  ],
@@ -278,6 +281,32 @@ export function createGateReviewService(deps) {
278
281
  message: `${coreInput.path} is ${coreInput.status}.`
279
282
  };
280
283
  }
284
+ if (gate === "validation-adequacy") {
285
+ const validationReportError = await readValidationReportError(deps.fs, context.taskRepoRoot);
286
+ if (validationReportError) {
287
+ index = applyGateState(index, gate, {
288
+ status: "failed",
289
+ decision: undefined,
290
+ error: validationReportError,
291
+ exceptionReason: undefined,
292
+ requestId: undefined,
293
+ requestPath: undefined,
294
+ inputHash: undefined,
295
+ requestedAt: undefined,
296
+ startedAt: undefined,
297
+ completedAt: now(),
298
+ callbackStatus: "not_sent",
299
+ callbackError: undefined
300
+ }, now(), true);
301
+ await saveIndex(deps.fs, context.taskRepoRoot, index);
302
+ return {
303
+ status: "failed_to_start",
304
+ gate,
305
+ record: index.gates[gate],
306
+ message: validationReportError
307
+ };
308
+ }
309
+ }
281
310
  const codeDiffInput = gate === "code-diff"
282
311
  ? await resolveCodeDiffInput(deps, context, record)
283
312
  : undefined;
@@ -393,7 +422,6 @@ export function createGateReviewService(deps) {
393
422
  return;
394
423
  }
395
424
  activeRuns.add(runKey);
396
- let gateTurnStarted = false;
397
425
  try {
398
426
  const timestamp = now();
399
427
  await updateGateRecord(context, gate, {
@@ -418,7 +446,7 @@ export function createGateReviewService(deps) {
418
446
  }
419
447
  const session = await ensureGateReviewerSession(context);
420
448
  await submitTerminalInput(deps.runtime, session.id, prompt);
421
- await deps.sessionService.markRoleActivityRunning(context.repoRoot, context.taskSlug, GATE_REVIEWER_ROLE);
449
+ await deps.sessionService.markRoleActivityRunning(context.repoRoot, context.taskSlug, GATE_REVIEWER_ROLE, session.id);
422
450
  await deps.roundService.recordRoleTurnEvent({
423
451
  repoRoot: context.repoRoot,
424
452
  stateRepoRoot: context.taskRepoRoot,
@@ -427,14 +455,11 @@ export function createGateReviewService(deps) {
427
455
  role: GATE_REVIEWER_ROLE,
428
456
  eventName: "UserPromptSubmit"
429
457
  });
430
- gateTurnStarted = true;
431
458
  const parsed = await waitForGateReport(deps.fs, context.taskRepoRoot, gate, requestId, now(), {
432
459
  intervalMs: reportPollIntervalMs,
433
460
  timeoutMs: reportTimeoutMs
434
461
  });
435
462
  const completedAt = now();
436
- await recordGateReviewerTurnStop(context, gateTurnStarted);
437
- gateTurnStarted = false;
438
463
  await updateRequestStatus(deps.fs, context, requestId, "completed", {
439
464
  completedAt,
440
465
  decision: parsed.decision,
@@ -457,8 +482,6 @@ export function createGateReviewService(deps) {
457
482
  catch (error) {
458
483
  const timestamp = now();
459
484
  const message = errorMessage(error);
460
- await recordGateReviewerTurnStop(context, gateTurnStarted);
461
- gateTurnStarted = false;
462
485
  await updateRequestStatus(deps.fs, context, requestId, "failed", {
463
486
  completedAt: timestamp,
464
487
  error: message
@@ -478,20 +501,6 @@ export function createGateReviewService(deps) {
478
501
  activeRuns.delete(runKey);
479
502
  }
480
503
  }
481
- async function recordGateReviewerTurnStop(context, shouldRecord) {
482
- if (!shouldRecord) {
483
- return;
484
- }
485
- await deps.sessionService.markRoleActivityIdle(context.repoRoot, context.taskSlug, GATE_REVIEWER_ROLE);
486
- await deps.roundService.recordRoleTurnEvent({
487
- repoRoot: context.repoRoot,
488
- stateRepoRoot: context.taskRepoRoot,
489
- stateRoot: context.stateRoot,
490
- taskSlug: context.taskSlug,
491
- role: GATE_REVIEWER_ROLE,
492
- eventName: "Stop"
493
- });
494
- }
495
504
  async function ensureGateReviewerSession(context) {
496
505
  const existing = await deps.sessionService.getRoleSession(context.repoRoot, context.taskSlug, GATE_REVIEWER_ROLE);
497
506
  if (existing?.status === "running" && deps.runtime.getSession(existing.id)) {
@@ -541,7 +550,7 @@ export function createGateReviewService(deps) {
541
550
  });
542
551
  try {
543
552
  await submitTerminalInput(deps.runtime, session.id, prompt);
544
- await deps.sessionService.markRoleActivityRunning(context.repoRoot, context.taskSlug, "project-manager");
553
+ await deps.sessionService.markRoleActivityRunning(context.repoRoot, context.taskSlug, "project-manager", session.id);
545
554
  await deps.roundService.recordRoleTurnEvent({
546
555
  repoRoot: context.repoRoot,
547
556
  stateRepoRoot: context.taskRepoRoot,
@@ -986,6 +995,24 @@ async function readArchitectureBriefError(fs, taskRepoRoot) {
986
995
  }
987
996
  return undefined;
988
997
  }
998
+ async function readValidationReportError(fs, taskRepoRoot) {
999
+ const relativePath = CORE_INPUT_ARTIFACTS["validation-adequacy"];
1000
+ if (!relativePath) {
1001
+ return undefined;
1002
+ }
1003
+ const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
1004
+ const content = await fs.pathExists(absolutePath) ? await fs.readText(absolutePath) : null;
1005
+ const check = checkMarkdownArtifact("test-report", relativePath, content);
1006
+ if (check.status === "ok") {
1007
+ return undefined;
1008
+ }
1009
+ const details = [
1010
+ check.missingHeadings.length > 0 ? `missing headings: ${check.missingHeadings.join(", ")}` : "",
1011
+ check.invalidFields.length > 0 ? check.invalidFields.join(" ") : "",
1012
+ check.hasPlaceholder ? "contains placeholders" : ""
1013
+ ].filter(Boolean).join("; ");
1014
+ return `${relativePath} is incomplete and cannot start validation-adequacy review.${details ? ` ${details}` : ""}`;
1015
+ }
989
1016
  async function readArchitectureEvidenceError(fs, taskRepoRoot) {
990
1017
  const relativePath = ".ai/vcm/handoffs/architecture-evidence.md";
991
1018
  const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
@@ -1214,16 +1241,14 @@ async function validateValidationApprovalInput(fs, taskRepoRoot) {
1214
1241
  const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
1215
1242
  const content = await fs.pathExists(absolutePath) ? await fs.readText(absolutePath) : null;
1216
1243
  const check = checkMarkdownArtifact("test-report", relativePath, content);
1217
- const testResult = content ? matchField(content, "Test Result")?.toLowerCase() : undefined;
1218
- if (check.status === "ok" && testResult === "pass") {
1244
+ if (check.status === "ok") {
1219
1245
  return;
1220
1246
  }
1221
1247
  const details = [
1222
- check.status !== "ok" ? `status=${check.status}` : "",
1248
+ `status=${check.status}`,
1223
1249
  check.missingHeadings.length > 0 ? `missing headings: ${check.missingHeadings.join(", ")}` : "",
1224
1250
  check.invalidFields.length > 0 ? check.invalidFields.join(" ") : "",
1225
- check.hasPlaceholder ? "contains placeholders" : "",
1226
- testResult !== "pass" ? "Test Result must be pass before approval." : ""
1251
+ check.hasPlaceholder ? "contains placeholders" : ""
1227
1252
  ].filter(Boolean).join("; ");
1228
1253
  throw new VcmError({
1229
1254
  code: "GATE_REVIEW_VALIDATION_INPUT_INCOMPLETE",
@@ -43,9 +43,9 @@ const LEGACY_CODEX_HARNESS_PATHS = [
43
43
  ".claude/skills/vcm-codex-review-gate",
44
44
  ".ai/tools/request-codex-review"
45
45
  ];
46
- const VCM_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,event}));});'"'"' | curl -fsS --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code" -H "content-type: application/json" --data-binary @- >/dev/null || true'`;
47
- const VCM_STOP_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,event}));});'"'"' | curl -fsS --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1 --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code/stop" -H "content-type: application/json" --data-binary @- || true'`;
48
- const VCM_PERMISSION_REQUEST_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,event}));});'"'"' | curl -fsS --max-time 5 -X POST "\${VCM_API_URL}/api/hooks/claude-code/permission-request" -H "content-type: application/json" --data-binary @- || true'`;
46
+ const VCM_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code" -H "content-type: application/json" --data-binary @- >/dev/null || true'`;
47
+ const VCM_STOP_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1 --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code/stop" -H "content-type: application/json" --data-binary @- || true'`;
48
+ const VCM_PERMISSION_REQUEST_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --max-time 5 -X POST "\${VCM_API_URL}/api/hooks/claude-code/permission-request" -H "content-type: application/json" --data-binary @- || true'`;
49
49
  const VCM_BASH_GUARD_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ]; then exit 0; fi; guard=""; repo="$(git rev-parse --show-toplevel 2>/dev/null || true)"; if [ -n "$repo" ] && [ -f "$repo/.ai/tools/vcm-bash-guard" ]; then guard="$repo/.ai/tools/vcm-bash-guard"; else cwd="$(pwd -P 2>/dev/null || pwd)"; dir="$cwd"; while [ -n "$dir" ] && [ "$dir" != "/" ]; do if [ -f "$dir/.ai/tools/vcm-bash-guard" ]; then guard="$dir/.ai/tools/vcm-bash-guard"; break; fi; dir="$(dirname "$dir")"; done; if [ -z "$guard" ] && [ -n "\${CLAUDE_PROJECT_DIR:-}" ] && [ -f "\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-bash-guard" ]; then guard="\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-bash-guard"; fi; fi; [ -n "$guard" ] || exit 0; python3 "$guard" || exit 0'`;
50
50
  const VCM_BASH_DEFAULT_TIMEOUT_MS = "600000";
51
51
  const VCM_AUTO_MEMORY_ENABLED = false;
@@ -118,7 +118,7 @@ export function createMessageService(deps) {
118
118
  await submitTerminalInput(deps.runtime, session.id, renderMessageEnvelope(delivered), {
119
119
  enterDelayMs: autoDispatchEnterDelayMs
120
120
  });
121
- await deps.sessionService.markRoleActivityRunning(input.repoRoot, input.taskSlug, routeFile.toRole);
121
+ await deps.sessionService.markRoleActivityRunning(input.repoRoot, input.taskSlug, routeFile.toRole, session.id);
122
122
  if (routeFile.fromRole === PM_ROLE) {
123
123
  await deps.taskWorkflowService?.recordPmDispatch({
124
124
  taskRepoRoot: input.taskRepoRoot ?? input.repoRoot,
@@ -51,8 +51,8 @@ export function createRuntimeCoordinatorService(deps) {
51
51
  .then((status) => status.initialized)
52
52
  .catch(() => false);
53
53
  await Promise.all([
54
- reconcileHarnessEngineer(repoRoot, activeTask),
55
- reconcileTranslator(repoRoot, activeTask, preferences.translationEnabled && harnessInitialized)
54
+ reconcileHarnessEngineer(repoRoot, activeTask, preferences.toolSessionDefaults["harness-engineer"]),
55
+ reconcileTranslator(repoRoot, activeTask, preferences.translationEnabled && harnessInitialized, preferences.toolSessionDefaults.translator)
56
56
  ]);
57
57
  if (preferences.translationEnabled && harnessInitialized) {
58
58
  await startConversationTranslationListeners(repoRoot, activeTask);
@@ -105,18 +105,18 @@ export function createRuntimeCoordinatorService(deps) {
105
105
  }
106
106
  return activeTasks[0] ?? null;
107
107
  }
108
- async function reconcileHarnessEngineer(repoRoot, task) {
108
+ async function reconcileHarnessEngineer(repoRoot, task, launchOptions) {
109
109
  const existing = await deps.sessionService.getRoleSession(repoRoot, task.taskSlug, "harness-engineer");
110
110
  if (!shouldAutoEnsureTaskToolSession(existing)) {
111
111
  return;
112
112
  }
113
113
  await ensureTaskToolRoleSession(repoRoot, task.taskSlug, "harness-engineer", {
114
- permissionMode: existing?.permissionMode,
115
- model: existing?.model,
116
- effort: existing?.effort
114
+ permissionMode: existing?.permissionMode ?? launchOptions.permissionMode,
115
+ model: existing?.model ?? launchOptions.model,
116
+ effort: existing?.effort ?? launchOptions.effort
117
117
  });
118
118
  }
119
- async function reconcileTranslator(repoRoot, task, enabled) {
119
+ async function reconcileTranslator(repoRoot, task, enabled, launchOptions) {
120
120
  if (!enabled) {
121
121
  return;
122
122
  }
@@ -125,9 +125,9 @@ export function createRuntimeCoordinatorService(deps) {
125
125
  return;
126
126
  }
127
127
  await ensureTaskToolRoleSession(repoRoot, task.taskSlug, "translator", {
128
- permissionMode: existing?.permissionMode,
129
- model: existing?.model,
130
- effort: existing?.effort
128
+ permissionMode: existing?.permissionMode ?? launchOptions.permissionMode,
129
+ model: existing?.model ?? launchOptions.model,
130
+ effort: existing?.effort ?? launchOptions.effort
131
131
  });
132
132
  }
133
133
  function shouldAutoEnsureTaskToolSession(session) {
@@ -18,6 +18,7 @@ const PROJECT_TRANSLATOR_SCOPE = "__project__";
18
18
  const PROJECT_HARNESS_ENGINEER_SCOPE = "__project_harness_engineer__";
19
19
  const PROJECT_TOOL_CD_ENTER_DELAY_MS = 500;
20
20
  const CLAUDE_CODE_DISABLE_AUTO_MEMORY = "1";
21
+ const CLAUDE_CODE_DISABLE_BACKGROUND_TASKS = "1";
21
22
  // Project tool sessions launch a Claude Code TUI inside a PTY. The PTY reports
22
23
  // "running" the instant it is spawned, which is earlier than the moment the TUI
23
24
  // can actually accept pasted input. These bounds drive a quiescence-based
@@ -84,6 +85,7 @@ export function createSessionService(deps) {
84
85
  ...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride, input.appendSystemPrompt),
85
86
  cwd: taskRepoRoot
86
87
  };
88
+ const runtimeSessionToken = randomUUID();
87
89
  const runtimeSession = await deps.runtime.createSession({
88
90
  repoRoot,
89
91
  taskSlug,
@@ -97,7 +99,8 @@ export function createSessionService(deps) {
97
99
  VCM_TASK_REPO_ROOT: taskRepoRoot,
98
100
  VCM_TASK_SLUG: taskSlug,
99
101
  VCM_ROLE: role,
100
- VCM_SESSION_ID: claudeSessionId || undefined
102
+ VCM_SESSION_ID: claudeSessionId || undefined,
103
+ VCM_RUNTIME_SESSION_TOKEN: runtimeSessionToken
101
104
  }, modelEnvironment, buildUsageTelemetryEnvironment(deps.apiUrl, role, model)),
102
105
  cols: input.cols,
103
106
  rows: input.rows
@@ -106,6 +109,7 @@ export function createSessionService(deps) {
106
109
  const harnessRevision = await readCurrentHarnessRevision(repoRoot);
107
110
  const record = {
108
111
  id: runtimeSession.id,
112
+ runtimeSessionToken,
109
113
  claudeSessionId,
110
114
  transcriptPath,
111
115
  taskSlug,
@@ -208,6 +212,7 @@ export function createSessionService(deps) {
208
212
  ...deps.claude.buildRoleStartCommand(TRANSLATOR_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
209
213
  cwd: launchCwd
210
214
  };
215
+ const runtimeSessionToken = randomUUID();
211
216
  const runtimeSession = await deps.runtime.createSession({
212
217
  repoRoot,
213
218
  taskSlug: PROJECT_TRANSLATOR_SCOPE,
@@ -224,7 +229,8 @@ export function createSessionService(deps) {
224
229
  // active task; VCM_TASK_REPO_ROOT remains the active worktree.
225
230
  VCM_TASK_SLUG: PROJECT_TRANSLATOR_SCOPE,
226
231
  VCM_ROLE: TRANSLATOR_ROLE,
227
- VCM_SESSION_ID: claudeSessionId || undefined
232
+ VCM_SESSION_ID: claudeSessionId || undefined,
233
+ VCM_RUNTIME_SESSION_TOKEN: runtimeSessionToken
228
234
  }, modelEnvironment, buildUsageTelemetryEnvironment(deps.apiUrl, TRANSLATOR_ROLE, model)),
229
235
  cols: input.cols,
230
236
  rows: input.rows
@@ -233,6 +239,7 @@ export function createSessionService(deps) {
233
239
  const harnessRevision = await readCurrentHarnessRevision(repoRoot);
234
240
  const record = {
235
241
  id: runtimeSession.id,
242
+ runtimeSessionToken,
236
243
  claudeSessionId,
237
244
  transcriptPath,
238
245
  taskSlug: PROJECT_TRANSLATOR_SCOPE,
@@ -321,6 +328,7 @@ export function createSessionService(deps) {
321
328
  ...deps.claude.buildRoleStartCommand(HARNESS_ENGINEER_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
322
329
  cwd: launchCwd
323
330
  };
331
+ const runtimeSessionToken = randomUUID();
324
332
  const runtimeSession = await deps.runtime.createSession({
325
333
  repoRoot,
326
334
  taskSlug: PROJECT_HARNESS_ENGINEER_SCOPE,
@@ -337,7 +345,8 @@ export function createSessionService(deps) {
337
345
  // active task; VCM_TASK_REPO_ROOT remains the active worktree.
338
346
  VCM_TASK_SLUG: PROJECT_HARNESS_ENGINEER_SCOPE,
339
347
  VCM_ROLE: HARNESS_ENGINEER_ROLE,
340
- VCM_SESSION_ID: claudeSessionId || undefined
348
+ VCM_SESSION_ID: claudeSessionId || undefined,
349
+ VCM_RUNTIME_SESSION_TOKEN: runtimeSessionToken
341
350
  }, modelEnvironment, buildUsageTelemetryEnvironment(deps.apiUrl, HARNESS_ENGINEER_ROLE, model)),
342
351
  cols: input.cols,
343
352
  rows: input.rows
@@ -346,6 +355,7 @@ export function createSessionService(deps) {
346
355
  const harnessRevision = await readCurrentHarnessRevision(repoRoot);
347
356
  const record = {
348
357
  id: runtimeSession.id,
358
+ runtimeSessionToken,
349
359
  claudeSessionId,
350
360
  transcriptPath,
351
361
  taskSlug: PROJECT_HARNESS_ENGINEER_SCOPE,
@@ -516,6 +526,7 @@ export function createSessionService(deps) {
516
526
  ...deps.claude.buildRoleStartCommand(session.role, config.claudeCommand, permissionMode, session.claudeSessionId, true, model, effort, modelSettingsOverride),
517
527
  cwd: launchCwd
518
528
  };
529
+ const runtimeSessionToken = randomUUID();
519
530
  const runtimeSession = await deps.runtime.createSession({
520
531
  repoRoot,
521
532
  taskSlug: normalizeProjectScopedRecordForPersistence(session).taskSlug,
@@ -529,7 +540,8 @@ export function createSessionService(deps) {
529
540
  VCM_TASK_REPO_ROOT: targetCwd,
530
541
  VCM_TASK_SLUG: normalizeProjectScopedRecordForPersistence(session).taskSlug,
531
542
  VCM_ROLE: session.role,
532
- VCM_SESSION_ID: session.claudeSessionId
543
+ VCM_SESSION_ID: session.claudeSessionId,
544
+ VCM_RUNTIME_SESSION_TOKEN: runtimeSessionToken
533
545
  }, modelEnvironment, buildUsageTelemetryEnvironment(deps.apiUrl, session.role, model))
534
546
  });
535
547
  if ((await waitForSessionInputReady(runtimeSession.id)) === "exited") {
@@ -537,6 +549,7 @@ export function createSessionService(deps) {
537
549
  return markProjectToolRuntimeUnavailable(repoRoot, {
538
550
  ...session,
539
551
  id: runtimeSession.id,
552
+ runtimeSessionToken,
540
553
  status: "crashed",
541
554
  activityStatus: "idle",
542
555
  command: startCommand.display,
@@ -554,6 +567,7 @@ export function createSessionService(deps) {
554
567
  const resumed = {
555
568
  ...session,
556
569
  id: runtimeSession.id,
570
+ runtimeSessionToken,
557
571
  status: runtimeSession.status,
558
572
  activityStatus: "idle",
559
573
  command: startCommand.display,
@@ -680,9 +694,9 @@ export function createSessionService(deps) {
680
694
  const view = toRoleSessionRecordView(record, deps.runtime);
681
695
  return view ? withHarnessRevisionView(repoRoot, view) : undefined;
682
696
  }
683
- async function markTaskRoleActivityIdle(repoRoot, taskSlug, role) {
697
+ async function markTaskRoleActivityIdle(repoRoot, taskSlug, role, expectedSessionId) {
684
698
  const current = await getTaskRoleSessionView(repoRoot, taskSlug, role);
685
- if (!current) {
699
+ if (!current || (expectedSessionId && current.id !== expectedSessionId)) {
686
700
  return undefined;
687
701
  }
688
702
  const timestamp = now();
@@ -795,7 +809,7 @@ export function createSessionService(deps) {
795
809
  },
796
810
  async recordProjectTranslatorHookEvent(repoRoot, input) {
797
811
  const current = await this.getProjectTranslatorSession(repoRoot);
798
- if (!current) {
812
+ if (!current || !matchesRoleHookSession(current, input)) {
799
813
  return undefined;
800
814
  }
801
815
  const timestamp = now();
@@ -921,7 +935,7 @@ export function createSessionService(deps) {
921
935
  },
922
936
  async recordProjectHarnessEngineerHookEvent(repoRoot, input) {
923
937
  const current = await this.getProjectHarnessEngineerSession(repoRoot);
924
- if (!current) {
938
+ if (!current || !matchesRoleHookSession(current, input)) {
925
939
  return undefined;
926
940
  }
927
941
  const timestamp = now();
@@ -1040,7 +1054,7 @@ export function createSessionService(deps) {
1040
1054
  },
1041
1055
  async recordRoleHookEvent(repoRoot, input) {
1042
1056
  const current = await this.getRoleSession(repoRoot, input.taskSlug, input.role);
1043
- if (!current || (!input.allowSessionMismatch && !matchesRoleHookSession(current, input))) {
1057
+ if (!current || !matchesRoleHookSession(current, input)) {
1044
1058
  return undefined;
1045
1059
  }
1046
1060
  const timestamp = now();
@@ -1072,7 +1086,9 @@ export function createSessionService(deps) {
1072
1086
  eventName: input.eventName,
1073
1087
  sessionId: input.claudeSessionId,
1074
1088
  transcriptPath: input.transcriptPath,
1075
- cwd: input.cwd
1089
+ cwd: input.cwd,
1090
+ runtimeSessionId: input.runtimeSessionId,
1091
+ runtimeSessionToken: input.runtimeSessionToken
1076
1092
  });
1077
1093
  },
1078
1094
  async markTerminalSessionActivityIdle(repoRoot, sessionId) {
@@ -1095,11 +1111,11 @@ export function createSessionService(deps) {
1095
1111
  ? markProjectToolActivityIdle(repoRoot, current, persistHarnessEngineerSession)
1096
1112
  : undefined;
1097
1113
  }
1098
- return markTaskRoleActivityIdle(repoRoot, taskSlug, role);
1114
+ return markTaskRoleActivityIdle(repoRoot, taskSlug, role, sessionId);
1099
1115
  },
1100
- async markRoleActivityRunning(repoRoot, taskSlug, role) {
1116
+ async markRoleActivityRunning(repoRoot, taskSlug, role, expectedSessionId) {
1101
1117
  const current = await this.getRoleSession(repoRoot, taskSlug, role);
1102
- if (!current) {
1118
+ if (!current || (expectedSessionId && current.id !== expectedSessionId)) {
1103
1119
  return undefined;
1104
1120
  }
1105
1121
  const timestamp = now();
@@ -1155,7 +1171,18 @@ function toRoleSessionRecordView(record, runtime) {
1155
1171
  exitCode: runtimeSession.exitCode
1156
1172
  };
1157
1173
  }
1158
- function matchesRoleHookSession(record, input) {
1174
+ export function matchesRoleHookSession(record, input) {
1175
+ if (!record.claudeSessionId
1176
+ && !record.transcriptPath
1177
+ && input.eventName !== "UserPromptSubmit") {
1178
+ return false;
1179
+ }
1180
+ if (input.runtimeSessionId) {
1181
+ return record.id === input.runtimeSessionId;
1182
+ }
1183
+ if (record.runtimeSessionToken) {
1184
+ return record.runtimeSessionToken === input.runtimeSessionToken;
1185
+ }
1159
1186
  if (!record.claudeSessionId && !record.transcriptPath) {
1160
1187
  return input.eventName === "UserPromptSubmit";
1161
1188
  }
@@ -1165,9 +1192,6 @@ function matchesRoleHookSession(record, input) {
1165
1192
  if (input.transcriptPath && record.transcriptPath === input.transcriptPath) {
1166
1193
  return true;
1167
1194
  }
1168
- if (!input.sessionId && !input.transcriptPath) {
1169
- return true;
1170
- }
1171
1195
  return false;
1172
1196
  }
1173
1197
  function nextHookSessionIdentity(current, input) {
@@ -1209,7 +1233,7 @@ function isSessionMissingError(error) {
1209
1233
  error.code === "SESSION_MISSING";
1210
1234
  }
1211
1235
  function withoutRuntimeOnlySessionFields(session) {
1212
- const { pid: _pid, ...persisted } = session;
1236
+ const { pid: _pid, runtimeSessionToken: _runtimeSessionToken, ...persisted } = session;
1213
1237
  return persisted;
1214
1238
  }
1215
1239
  function defaultIsProcessAlive(pid) {
@@ -1542,7 +1566,8 @@ function withClaudeCodeRuntimeEnv(env, modelEnvironment = {}, telemetryEnvironme
1542
1566
  ...env,
1543
1567
  ...modelEnvironment,
1544
1568
  ...telemetryEnvironment,
1545
- CLAUDE_CODE_DISABLE_AUTO_MEMORY
1569
+ CLAUDE_CODE_DISABLE_AUTO_MEMORY,
1570
+ CLAUDE_CODE_DISABLE_BACKGROUND_TASKS
1546
1571
  };
1547
1572
  }
1548
1573
  function buildUsageTelemetryEnvironment(apiUrl, role, model) {
@@ -11,7 +11,10 @@ export function createTerminalInterruptService(deps) {
11
11
  if (!repoRoot) {
12
12
  return;
13
13
  }
14
- await deps.sessionService.markTerminalSessionActivityIdle(repoRoot, sessionId);
14
+ const session = await deps.sessionService.markTerminalSessionActivityIdle(repoRoot, sessionId);
15
+ if (!session) {
16
+ return;
17
+ }
15
18
  if (!isVcmRoleName(terminalSession.role)) {
16
19
  return;
17
20
  }
@@ -96,6 +96,7 @@ function buildReconciledHook(taskSlug, role, session, eventName, evidence) {
96
96
  ...(session?.claudeSessionId ? { session_id: session.claudeSessionId } : {}),
97
97
  ...(session?.transcriptPath ? { transcript_path: session.transcriptPath } : {}),
98
98
  ...(session?.cwd ? { cwd: session.cwd } : {}),
99
+ ...(session?.id ? { vcm_runtime_session_id: session.id } : {}),
99
100
  vcm_reconciled: true,
100
101
  ...evidence
101
102
  }
@@ -183,6 +183,10 @@ TBD
183
183
 
184
184
  ## Blocking Validation Issues
185
185
 
186
+ TBD
187
+
188
+ ## User Approval Evidence
189
+
186
190
  TBD
187
191
  `;
188
192
  }
@@ -110,6 +110,8 @@ production entry points needed to verify what those tests exercise. Read the
110
110
  relevant architect/coder definitions and \`.ai/vcm/handoffs/architecture-plan.md\`
111
111
  when the active flow produced an architecture plan. Read
112
112
  \`.ai/generated/public-surface.json\` when public contracts changed.
113
+ When the report contains an approved Coverage Gap, also read the relevant
114
+ Architect Debug and Architecture Diagnosis evidence.
113
115
 
114
116
  Reconstruct the accepted validation target, observable behavior, and risks
115
117
  from the active flow evidence and current implementation. Treat Tester
@@ -138,7 +140,20 @@ actual tests, validation level does not match risk, an important behavior has
138
140
  no concrete coverage mapping, a required check was skipped, required coverage
139
141
  is unavailable, or a current-task coverage gap remains. A concrete risk-based
140
142
  reason may show that integration or E2E coverage is unnecessary; unavailable
141
- required coverage is not an approval reason.
143
+ required coverage without exact user approval is not an approval reason.
144
+
145
+ Treat every unresolved required-coverage item as gate-blocking unless
146
+ \`test-report.md\` contains the user's exact approval routed by project-manager.
147
+ Verify that Architect Debug and Architecture Diagnosis were completed before
148
+ user acceptance was requested, the approved gap exactly matches the final
149
+ Tester evidence, the affected behavior and remaining risk are stated
150
+ completely, and any new or changed \`Known Testing Gaps\` entry matches the
151
+ approved durable limitation. Project-manager, Architect, or Tester judgment is
152
+ not user authorization.
153
+
154
+ An approved gap keeps \`Test Result: fail\`. Gate approval means the validation
155
+ evidence and exact user exception are complete and consistent; it does not
156
+ convert the result to \`pass\` or independently accept the risk.
142
157
 
143
158
  ## Code Diff Gate
144
159
 
@@ -246,6 +261,7 @@ Use this findings structure:
246
261
  - Public Contract Coverage:
247
262
  - Test Integrity:
248
263
  - Skips And Gaps:
264
+ - User Approval And Gap Disposition:
249
265
  - Validation Readiness:
250
266
 
251
267
  <!-- Include Code Diff Analysis only for code-diff gate. -->
@@ -306,6 +322,7 @@ If there are no findings, write:
306
322
  - Public Contract Coverage:
307
323
  - Test Integrity:
308
324
  - Skips And Gaps:
325
+ - User Approval And Gap Disposition:
309
326
  - Validation Readiness:
310
327
 
311
328
  <!-- Include Code Diff Analysis only for code-diff gate. -->
@@ -180,7 +180,7 @@ Architecture Diagnosis Mode must run before another Debug Mode fix or Coder disp
180
180
  #### Allowed Branches
181
181
 
182
182
  - **Code-Diff Revision:** If the code-diff Gate returns \`request_changes\`, route the report to Architecture Diagnosis Mode and rerun \`code-diff --source architect-diagnosis\` after correction.
183
- - **Tester Failure:** If Tester returns \`Test Result: fail\` for the Diagnosis implementation, pause and report to the user.
183
+ - **Tester Failure:** If Tester returns \`Test Result: fail\` for the Diagnosis implementation, pause and report to the user. If required validation remains unavailable, ask whether the user explicitly approves retaining that exact Coverage Gap.
184
184
 
185
185
  #### Successful Exit
186
186
 
@@ -195,6 +195,14 @@ After Tester Failure, PM should summarize:
195
195
  - what the Architect diagnosed
196
196
  - what Tester still found wrong
197
197
 
198
+ If the user approves the exact gap, record the approval verbatim and route
199
+ Tester to add the approved \`Coverage Gaps\` entry and, when applicable, the
200
+ durable \`Known Testing Gaps\` entry. Then run the validation-adequacy Gate and
201
+ continue using the recorded user-approved exception.
202
+
203
+ Without explicit user approval, the gap remains blocking and the workflow
204
+ stays paused.
205
+
198
206
  ### Docs-Only Flow
199
207
 
200
208
  Use Docs-Only Flow when the accepted task changes Architect-owned project documentation and does not require production-code, test-code, runtime-behavior, public-contract, dependency, or Harness changes.