vibe-coding-master 0.7.29 → 0.7.31

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.
@@ -70,7 +70,8 @@ export function registerTaskRoutes(app, deps) {
70
70
  messages,
71
71
  orchestration,
72
72
  roundState,
73
- workflowState
73
+ workflowState,
74
+ architectRestart: deps.architectRestartService.getState(project.repoRoot, taskSlug)
74
75
  };
75
76
  }
76
77
  catch (error) {
@@ -84,7 +85,8 @@ export function registerTaskRoutes(app, deps) {
84
85
  updatedAt: new Date().toISOString()
85
86
  },
86
87
  roundState: degradedRoundState(taskSlug),
87
- workflowState: degradedWorkflowState(taskSlug)
88
+ workflowState: degradedWorkflowState(taskSlug),
89
+ architectRestart: deps.architectRestartService.getState(repoRoot, taskSlug)
88
90
  };
89
91
  }
90
92
  throw error;
@@ -130,7 +130,8 @@ export async function createServer(deps, options = {}) {
130
130
  messageService: deps.messageService,
131
131
  taskLaunchService: deps.taskLaunchService,
132
132
  roundService: deps.roundService,
133
- taskWorkflowService: deps.taskWorkflowService
133
+ taskWorkflowService: deps.taskWorkflowService,
134
+ architectRestartService: deps.architectRestartService
134
135
  });
135
136
  registerSessionRoutes(app, {
136
137
  projectService: deps.projectService,
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { resolveRepoPath } from "../adapters/filesystem.js";
3
- import { VcmError } from "../errors.js";
3
+ import { toVcmError, VcmError } from "../errors.js";
4
4
  import { getTaskRuntimeRepoRoot } from "./task-service.js";
5
5
  import { ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH } from "./memory-review-paths.js";
6
6
  import { validateMemoryProposal } from "./memory-proposal-validation.js";
@@ -23,40 +23,41 @@ export function createArchitectRestartService(deps) {
23
23
  async schedule(repoRoot, taskSlug) {
24
24
  const session = await requireRunningArchitect(repoRoot, taskSlug);
25
25
  await requireCompletePlan(repoRoot, taskSlug);
26
- const memoryCandidatePath = (await deps.appSettings.getPreferences()).autoMemoryEnabled
27
- ? ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH
28
- : undefined;
29
- const task = await deps.taskService.loadTask(repoRoot, taskSlug);
30
- const candidatePath = resolveRepoPath(getTaskRuntimeRepoRoot(task), ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH);
31
- if (deps.fs.removePath) {
32
- await deps.fs.removePath(candidatePath, { force: true });
33
- }
34
- else if (await deps.fs.pathExists(candidatePath)) {
35
- await deps.fs.writeText(candidatePath, "");
36
- }
37
26
  const key = taskKey(repoRoot, taskSlug);
38
27
  const existing = pendingByTask.get(key);
39
28
  if (existing?.sessionId === session.id) {
40
- existing.stopped = false;
41
- existing.deliveredMessageId = undefined;
42
- existing.acceptedMessageId = undefined;
43
- existing.gateAccepted = false;
44
- existing.executing = false;
45
- existing.memoryCandidatePath = memoryCandidatePath;
29
+ if (existing.status === "blocked") {
30
+ existing.status = "pending";
31
+ existing.blocker = undefined;
32
+ await tryRestart(existing);
33
+ return {
34
+ taskSlug,
35
+ sessionId: session.id,
36
+ status: "scheduled",
37
+ ...(existing.memoryCandidatePath
38
+ ? { memoryCandidatePath: existing.memoryCandidatePath }
39
+ : {})
40
+ };
41
+ }
46
42
  return {
47
43
  taskSlug,
48
44
  sessionId: session.id,
49
- status: "scheduled",
50
- ...(memoryCandidatePath ? { memoryCandidatePath } : {})
45
+ status: "already_scheduled",
46
+ ...(existing.memoryCandidatePath
47
+ ? { memoryCandidatePath: existing.memoryCandidatePath }
48
+ : {})
51
49
  };
52
50
  }
51
+ const memoryCandidatePath = (await deps.appSettings.getPreferences()).autoMemoryEnabled
52
+ ? ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH
53
+ : undefined;
53
54
  pendingByTask.set(key, {
54
55
  repoRoot,
55
56
  taskSlug,
56
57
  sessionId: session.id,
57
58
  stopped: false,
58
59
  gateAccepted: false,
59
- executing: false,
60
+ status: "pending",
60
61
  memoryCandidatePath
61
62
  });
62
63
  return {
@@ -66,9 +67,24 @@ export function createArchitectRestartService(deps) {
66
67
  ...(memoryCandidatePath ? { memoryCandidatePath } : {})
67
68
  };
68
69
  },
70
+ getState(repoRoot, taskSlug) {
71
+ const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
72
+ if (!pending) {
73
+ return null;
74
+ }
75
+ return {
76
+ taskSlug: pending.taskSlug,
77
+ sessionId: pending.sessionId,
78
+ status: pending.status,
79
+ ...(pending.memoryCandidatePath
80
+ ? { memoryCandidatePath: pending.memoryCandidatePath }
81
+ : {}),
82
+ ...(pending.blocker ? { blocker: { ...pending.blocker } } : {})
83
+ };
84
+ },
69
85
  async recordArchitectStop(repoRoot, taskSlug, sessionId) {
70
86
  const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
71
- if (!pending || pending.sessionId !== sessionId) {
87
+ if (!pending || pending.sessionId !== sessionId || pending.status === "blocked") {
72
88
  return;
73
89
  }
74
90
  pending.stopped = true;
@@ -79,7 +95,7 @@ export function createArchitectRestartService(deps) {
79
95
  return;
80
96
  }
81
97
  const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
82
- if (!pending) {
98
+ if (!pending || pending.status === "blocked") {
83
99
  return;
84
100
  }
85
101
  pending.deliveredMessageId = message.id;
@@ -90,7 +106,7 @@ export function createArchitectRestartService(deps) {
90
106
  return;
91
107
  }
92
108
  const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
93
- if (!pending) {
109
+ if (!pending || pending.status === "blocked") {
94
110
  return;
95
111
  }
96
112
  pending.acceptedMessageId = message.id;
@@ -98,7 +114,7 @@ export function createArchitectRestartService(deps) {
98
114
  },
99
115
  async recordArchitectureGateDisposition(repoRoot, taskSlug, accepted) {
100
116
  const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
101
- if (!pending) {
117
+ if (!pending || pending.status === "blocked") {
102
118
  return;
103
119
  }
104
120
  pending.gateAccepted = accepted;
@@ -131,7 +147,7 @@ export function createArchitectRestartService(deps) {
131
147
  }
132
148
  }
133
149
  async function tryRestart(pending) {
134
- if (pending.executing
150
+ if (pending.status !== "pending"
135
151
  || !pending.stopped
136
152
  || !pending.deliveredMessageId
137
153
  || pending.deliveredMessageId !== pending.acceptedMessageId
@@ -140,12 +156,26 @@ export function createArchitectRestartService(deps) {
140
156
  }
141
157
  const session = await deps.sessionService.getRoleSession(pending.repoRoot, pending.taskSlug, ARCHITECT_ROLE);
142
158
  if (!session
143
- || session.id !== pending.sessionId
144
- || session.status !== "running"
145
- || session.activityStatus !== "idle") {
159
+ || session.id !== pending.sessionId) {
160
+ blockPending(pending, new VcmError({
161
+ code: "ARCHITECT_RESTART_SESSION_UNAVAILABLE",
162
+ message: "Architect restart is blocked because the scheduled Architect session no longer exists.",
163
+ statusCode: 409
164
+ }));
146
165
  return;
147
166
  }
148
- pending.executing = true;
167
+ if (session.status !== "running") {
168
+ blockPending(pending, new VcmError({
169
+ code: "ARCHITECT_RESTART_SESSION_NOT_RUNNING",
170
+ message: "Architect restart is blocked because the scheduled Architect session is not running.",
171
+ statusCode: 409
172
+ }));
173
+ return;
174
+ }
175
+ if (session.activityStatus !== "idle") {
176
+ return;
177
+ }
178
+ pending.status = "executing";
149
179
  try {
150
180
  await requireCompletePlan(pending.repoRoot, pending.taskSlug);
151
181
  await requirePlanningMemoryCandidate(pending);
@@ -157,10 +187,19 @@ export function createArchitectRestartService(deps) {
157
187
  });
158
188
  pendingByTask.delete(taskKey(pending.repoRoot, pending.taskSlug));
159
189
  }
160
- catch {
161
- pending.executing = false;
190
+ catch (error) {
191
+ blockPending(pending, error);
162
192
  }
163
193
  }
194
+ function blockPending(pending, error) {
195
+ const normalized = toVcmError(error);
196
+ pending.status = "blocked";
197
+ pending.blocker = {
198
+ code: normalized.code,
199
+ message: normalized.message,
200
+ blockedAt: new Date().toISOString()
201
+ };
202
+ }
164
203
  async function requirePlanningMemoryCandidate(pending) {
165
204
  if (!pending.memoryCandidatePath) {
166
205
  return;
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import path from "node:path";
3
- import { CODE_DIFF_SOURCES, GATE_REVIEW_GATES } from "../../shared/types/gate-review.js";
3
+ import { CODE_DIFF_FINDING_SCOPES, CODE_DIFF_SOURCES, GATE_REVIEW_GATES } from "../../shared/types/gate-review.js";
4
4
  import { checkMarkdownArtifact } from "../../shared/validation/artifact-check.js";
5
5
  import { VcmError } from "../errors.js";
6
6
  import { resolveRepoPath } from "../adapters/filesystem.js";
@@ -38,6 +38,7 @@ const VALIDATION_ANALYSIS_FIELDS = [
38
38
  "Boundary And Failure Coverage",
39
39
  "Public Contract Coverage",
40
40
  "Test Integrity",
41
+ "Test Infrastructure",
41
42
  "Skips And Gaps",
42
43
  "User Approval And Gap Disposition",
43
44
  "Validation Readiness"
@@ -68,7 +69,10 @@ const SOURCE_ARTIFACTS = {
68
69
  ".ai/vcm/handoffs/test-report.md",
69
70
  "docs/TESTING.md"
70
71
  ],
71
- "code-diff": []
72
+ "code-diff": [
73
+ ".ai/vcm/handoffs/test-report.md",
74
+ ".ai/vcm/gate-reviews/validation-adequacy-review.md"
75
+ ]
72
76
  };
73
77
  const CODE_DIFF_SOURCE_ARTIFACTS = {
74
78
  coder: [
@@ -88,6 +92,7 @@ const CORE_INPUT_ARTIFACTS = {
88
92
  "validation-adequacy": ".ai/vcm/handoffs/test-report.md"
89
93
  };
90
94
  const VALID_SEVERITIES = new Set(["critical", "high", "medium", "low"]);
95
+ const VALID_CODE_DIFF_FINDING_SCOPES = new Set(CODE_DIFF_FINDING_SCOPES);
91
96
  export function createGateReviewService(deps) {
92
97
  const now = deps.now ?? (() => new Date().toISOString());
93
98
  const reportPollIntervalMs = deps.reportPollIntervalMs ?? DEFAULT_REPORT_POLL_INTERVAL_MS;
@@ -309,6 +314,39 @@ export function createGateReviewService(deps) {
309
314
  };
310
315
  }
311
316
  }
317
+ if (gate === "code-diff") {
318
+ const prerequisiteError = await readCodeDiffPrerequisiteError(deps, context, index);
319
+ if (prerequisiteError) {
320
+ index = applyGateState(index, gate, {
321
+ status: "failed",
322
+ decision: undefined,
323
+ error: prerequisiteError,
324
+ exceptionReason: undefined,
325
+ requestId: undefined,
326
+ requestPath: undefined,
327
+ inputHash: undefined,
328
+ baseCommit: undefined,
329
+ headCommit: undefined,
330
+ commits: undefined,
331
+ changedFiles: undefined,
332
+ diffStat: undefined,
333
+ codeDiffSource,
334
+ codeDiffSources: codeDiffSource ? [codeDiffSource] : undefined,
335
+ requestedAt: undefined,
336
+ startedAt: undefined,
337
+ completedAt: now(),
338
+ callbackStatus: "not_sent",
339
+ callbackError: undefined
340
+ }, now(), true);
341
+ await saveIndex(deps.fs, context.taskRepoRoot, index);
342
+ return {
343
+ status: "failed_to_start",
344
+ gate,
345
+ record: index.gates[gate],
346
+ message: prerequisiteError
347
+ };
348
+ }
349
+ }
312
350
  const codeDiffInput = gate === "code-diff"
313
351
  ? await resolveCodeDiffInput(deps, context, record)
314
352
  : undefined;
@@ -1031,11 +1069,39 @@ async function readValidationReportError(fs, taskRepoRoot) {
1031
1069
  const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
1032
1070
  const content = await fs.pathExists(absolutePath) ? await fs.readText(absolutePath) : null;
1033
1071
  const check = checkMarkdownArtifact("test-report", relativePath, content);
1034
- if (check.status === "ok") {
1072
+ if (check.status !== "ok") {
1073
+ return `${relativePath} is incomplete and cannot start validation-adequacy review. `
1074
+ + formatValidationArtifactFailure(check, content);
1075
+ }
1076
+ const infrastructureStatus = matchField(extractMarkdownSection(content ?? "", "Test Infrastructure") ?? "", "Status");
1077
+ if (infrastructureStatus === "repair-required") {
1078
+ return `${relativePath} cannot start validation-adequacy review while Test Infrastructure Status is repair-required. Route Tester repair first.`;
1079
+ }
1080
+ if (infrastructureStatus === "production-change-required") {
1081
+ return `${relativePath} cannot start validation-adequacy review while Test Infrastructure Status is production-change-required. Route the active flow's implementation-failure branch first.`;
1082
+ }
1083
+ return undefined;
1084
+ }
1085
+ async function readCodeDiffPrerequisiteError(deps, context, index) {
1086
+ const reportError = await readValidationReportError(deps.fs, context.taskRepoRoot);
1087
+ if (reportError) {
1088
+ return "code-diff requires completed Tester validation. " + reportError;
1089
+ }
1090
+ const validationGate = index.gates["validation-adequacy"];
1091
+ if (!validationGate.required) {
1092
+ return undefined;
1093
+ }
1094
+ if (validationGate.status === "skipped" || validationGate.status === "overridden") {
1035
1095
  return undefined;
1036
1096
  }
1037
- return `${relativePath} is incomplete and cannot start validation-adequacy review. `
1038
- + formatValidationArtifactFailure(check, content);
1097
+ if (validationGate.status !== "completed" || validationGate.decision !== "approve") {
1098
+ return "code-diff requires the validation-adequacy Gate to complete successfully for the current Tester evidence.";
1099
+ }
1100
+ const currentValidationHash = await computeInputHash(deps, context.taskRepoRoot, "validation-adequacy");
1101
+ if (!validationGate.inputHash || validationGate.inputHash !== currentValidationHash) {
1102
+ return "code-diff requires a current validation-adequacy approval; code or test evidence changed after the recorded approval.";
1103
+ }
1104
+ return undefined;
1039
1105
  }
1040
1106
  async function readArchitectureEvidenceError(fs, taskRepoRoot) {
1041
1107
  const relativePath = ".ai/vcm/handoffs/architecture-evidence.md";
@@ -1310,11 +1376,13 @@ function validateRequestChangeFindings(findings) {
1310
1376
  }
1311
1377
  }
1312
1378
  function validateCodeDiffFindings(findings) {
1313
- const incomplete = findings.find((finding) => !finding.file?.trim() || !finding.location?.trim());
1379
+ const incomplete = findings.find((finding) => (!finding.file?.trim()
1380
+ || !finding.location?.trim()
1381
+ || !finding.scope));
1314
1382
  if (incomplete) {
1315
1383
  throw new VcmError({
1316
1384
  code: "GATE_REVIEW_CODE_DIFF_FINDING_LOCATION_MISSING",
1317
- message: `Code-diff finding ${incomplete.title} must contain File and Line Or Symbol.`,
1385
+ message: `Code-diff finding ${incomplete.title} must contain File, Line Or Symbol, and Finding Scope.`,
1318
1386
  statusCode: 500
1319
1387
  });
1320
1388
  }
@@ -1408,6 +1476,7 @@ function extractFindings(content) {
1408
1476
  file: matchField(block, "file"),
1409
1477
  line: parsePositiveInteger(matchField(block, "line")),
1410
1478
  location: matchField(block, "line or symbol"),
1479
+ scope: normalizeCodeDiffFindingScope(matchField(block, "finding scope")),
1411
1480
  evidence: matchField(block, "evidence") ?? "",
1412
1481
  expected: matchField(block, "expected") ?? "",
1413
1482
  gap: matchField(block, "gap") ?? "",
@@ -1420,7 +1489,10 @@ function getSourceArtifacts(gate, codeDiffSources) {
1420
1489
  if (gate !== "code-diff") {
1421
1490
  return SOURCE_ARTIFACTS[gate];
1422
1491
  }
1423
- return [...new Set((codeDiffSources ?? []).flatMap((source) => CODE_DIFF_SOURCE_ARTIFACTS[source]))];
1492
+ return [...new Set([
1493
+ ...SOURCE_ARTIFACTS["code-diff"],
1494
+ ...(codeDiffSources ?? []).flatMap((source) => CODE_DIFF_SOURCE_ARTIFACTS[source])
1495
+ ])];
1424
1496
  }
1425
1497
  function resolveCodeDiffSources(record, codeDiffInput, currentSource) {
1426
1498
  const continuingRecordedRange = record.baseCommit === codeDiffInput.baseCommit
@@ -1467,6 +1539,12 @@ function normalizeSeverity(value) {
1467
1539
  ? normalized
1468
1540
  : undefined;
1469
1541
  }
1542
+ function normalizeCodeDiffFindingScope(value) {
1543
+ const normalized = typeof value === "string" ? value.toLowerCase() : "";
1544
+ return VALID_CODE_DIFF_FINDING_SCOPES.has(normalized)
1545
+ ? normalized
1546
+ : undefined;
1547
+ }
1470
1548
  function normalizeCallbackStatus(value) {
1471
1549
  return value === "not_sent" || value === "sent" || value === "skipped" || value === "failed"
1472
1550
  ? value
@@ -1,4 +1,4 @@
1
- import { ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_PLAN_RESULTS, DOCS_SYNC_DECISIONS, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, STRICT_NONE_VALUE, TEST_RESULTS, renderArtifactOptions } from "../../shared/validation/artifact-contract.js";
1
+ import { ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_PLAN_RESULTS, DOCS_SYNC_DECISIONS, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, STRICT_NONE_VALUE, TEST_INFRASTRUCTURE_STATUSES, TEST_RESULTS, renderArtifactOptions } from "../../shared/validation/artifact-contract.js";
2
2
  export function renderArchitectureBriefTemplate(taskSlug) {
3
3
  return `# Architecture Brief: ${taskSlug}
4
4
 
@@ -201,6 +201,26 @@ TBD
201
201
 
202
202
  TBD
203
203
 
204
+ ## Test Infrastructure
205
+
206
+ Status: ${renderArtifactOptions(TEST_INFRASTRUCTURE_STATUSES)}
207
+
208
+ ### Affected Files
209
+
210
+ ${STRICT_NONE_VALUE}
211
+
212
+ ### Boundary Evidence
213
+
214
+ ${STRICT_NONE_VALUE}
215
+
216
+ ### Defect-Class Sweep
217
+
218
+ ${STRICT_NONE_VALUE}
219
+
220
+ ### Repair Commit
221
+
222
+ ${STRICT_NONE_VALUE}
223
+
204
224
  ## Failed Expectations
205
225
 
206
226
  ${STRICT_NONE_VALUE}
@@ -115,9 +115,9 @@ ${renderRoleMemoryRules("architect")}
115
115
 
116
116
  #### Planning Completion
117
117
 
118
- - After the complete plan, scaffold, reconciliation, L0 evidence, and commits are ready, use the \`restart-architect\` skill before writing the completed Architect-to-PM route message.
119
- - If VCM returns a \`memoryCandidatePath\`, use \`vcm-propose-memory\` to write the planning-session memory candidate to that path before routing. Include only verified, durable, reusable project knowledge from planning; do not record task narrative, temporary state, unverified conclusions, or Harness rules.
120
- - After the required candidate is written, write the route message with both architecture artifacts and the plan, then end the turn. VCM keeps this session for any architecture-plan Gate revision and restarts it only after the Gate is accepted. Do not wait for or inspect the replacement session.
118
+ - Before the first completed Architect-to-PM planning route, use the \`restart-architect\` skill after the complete plan, scaffold, reconciliation, L0 evidence, and commits are ready.
119
+ - If VCM returns a \`memoryCandidatePath\`, ensure one planning-session memory candidate exists at that path before routing. Use \`vcm-propose-memory\` only when the candidate is absent. Include only verified, durable, reusable project knowledge from planning; do not record task narrative, temporary state, unverified conclusions, or Harness rules.
120
+ - Gate revision rounds reuse the same pending restart and task-level candidate; do not recreate either one. Write the latest route message with both architecture artifacts and the plan, then end the turn. VCM restarts the session only after the Gate is accepted. Do not wait for or inspect the replacement session.
121
121
 
122
122
  ### Complete Task Planning
123
123
 
@@ -51,8 +51,8 @@ If a reusable harness problem is suspected, it is enough to record a concise fee
51
51
  - All standard workflow routes among project-manager, architect, coder, and tester are PM-hub routes. Project-manager starts and advances every flow; architect, coder, and tester report blockers, failures, conflicts, incomplete work, and findings back to project-manager.
52
52
  - Code changes use: \`project-manager -> architect interview -> architect planning -> coder -> tester -> architect docs sync -> project-manager final acceptance\`.
53
53
  - Architect Debug Mode runs inside either Architect Debug Flow or Architect Debug Branch. Architecture Diagnosis Mode runs inside either Architecture Diagnosis Flow or Architecture Diagnosis Branch.
54
- - Architect Debug Flow and an Architecture Diagnosis Flow that produces code changes continue through code-diff Gate Review, tester validation, architect docs sync, and project-manager final acceptance. An analysis-only Architecture Diagnosis Flow completes from the diagnosis result.
55
- - Architect Debug Branch and Architecture Diagnosis Branch preserve the active parent flow and resume point, then return there after successful validation. They do not run their own final acceptance.
54
+ - Code-Change Flow, Architect Debug Flow, and an Architecture Diagnosis Flow that produces code changes run tester validation, validation-adequacy Gate Review, and then code-diff Gate Review before architect docs sync and project-manager final acceptance. An analysis-only Architecture Diagnosis Flow completes from the diagnosis result.
55
+ - Architect Debug Branch and Architecture Diagnosis Branch preserve the active parent flow and resume point, then return there after tester validation, validation-adequacy Gate Review, and code-diff Gate Review complete. They do not run their own final acceptance.
56
56
  - Docs-Only Flow uses: \`project-manager -> architect -> project-manager completion\`.
57
57
  - Validation-Only Flow uses: \`project-manager -> tester -> validation-adequacy Gate Review -> project-manager completion\`.
58
58
  - Communication-Only Flow uses: \`project-manager response or relay -> completion\`.
@@ -185,6 +185,14 @@ when they are relevant to the changed behavior. Check that tests were not
185
185
  weakened, over-mocked, tied only to fixture values or implementation details,
186
186
  or made green by bypassing the real behavior path.
187
187
 
188
+ Inspect the \`Test Infrastructure\` section of \`test-report.md\`. A report with
189
+ \`repair-required\` or \`production-change-required\` is not gate-ready. When
190
+ status is \`repaired\`, verify the affected files remain Tester-owned, the
191
+ boundary evidence excludes production or shared changes, the defect-class sweep
192
+ covers the affected test-infrastructure family, the repair commit exists in the
193
+ current range, and required clean-state validation was rerun. Request changes
194
+ for missing, contradictory, incomplete, weakened, or unverified repair evidence.
195
+
188
196
  Do not approve only because \`Test Result: pass\` or all recorded commands are
189
197
  green. Request changes when the report is incomplete or inconsistent with the
190
198
  actual tests, validation level does not match risk, an important behavior has
@@ -207,10 +215,12 @@ convert the result to \`pass\` or independently accept the risk.
207
215
 
208
216
  ## Code Diff Gate
209
217
 
210
- Read \`.claude/agents/coder.md\` and \`docs/CODING_STANDARDS.md\`; use
211
- architect/tester definitions only to understand implementation and test
212
- responsibility boundaries. Review every commit in the range named by VCM and
213
- nothing outside that range.
218
+ Read \`.claude/agents/coder.md\`, \`.claude/agents/tester.md\`,
219
+ \`.ai/vcm/handoffs/test-report.md\`, the current validation-adequacy Gate report,
220
+ and \`docs/CODING_STANDARDS.md\`; use the architect definition to understand
221
+ implementation responsibility boundaries. Code-diff runs only after Tester
222
+ validation and the current validation-adequacy disposition. Review every commit
223
+ in the range named by VCM and nothing outside that range.
214
224
 
215
225
  Use every code source and evidence artifact named in the VCM prompt. A source
216
226
  chain means the range contains the original implementation and later corrective
@@ -237,6 +247,13 @@ scaffold, and coder completion evidence. Verify that the complete planned
237
247
  behavior is implemented without changing architect-owned boundaries or
238
248
  contracts.
239
249
 
250
+ When the range contains Tester-authored changes recorded in \`test-report.md\`,
251
+ review those tests, fixtures, test-only helpers, and \`docs/TESTING.md\` against
252
+ the Tester role, the repair or coverage evidence, and
253
+ \`docs/CODING_STANDARDS.md\`. Do not reject a valid Tester-owned change merely
254
+ because it is not a Coder scaffold item. Verify that Tester changes remain
255
+ test-only, preserve real behavior paths, and are committed and validated.
256
+
240
257
  For \`architect-debug\`, compare the commits with the current Architect route
241
258
  command and \`.ai/vcm/handoffs/architect-debug.md\`. Verify that the confirmed
242
259
  root cause is supported by the code, the implementation fixes that cause rather
@@ -262,6 +279,11 @@ and changes outside its governing evidence. Verify callable and public-surface
262
279
  changes against their callers, exports, compatibility obligations, generated
263
280
  context, and durable documentation.
264
281
 
282
+ Use the completed test report and validation-adequacy disposition as execution
283
+ evidence while independently deciding whether the implementation handles its
284
+ required behavior and boundary cases. Do not repeat the validation-adequacy
285
+ decision.
286
+
265
287
  Inspect changed baseline tests for the changed callable units and applicable
266
288
  branches. Request changes for weakened, deleted, skipped, fabricated, or
267
289
  implementation-shaped tests, and for obvious missing baseline coverage required
@@ -312,6 +334,7 @@ Use this findings structure:
312
334
  - Boundary And Failure Coverage:
313
335
  - Public Contract Coverage:
314
336
  - Test Integrity:
337
+ - Test Infrastructure:
315
338
  - Skips And Gaps:
316
339
  - User Approval And Gap Disposition:
317
340
  - Validation Readiness:
@@ -337,6 +360,8 @@ Use this findings structure:
337
360
  <!-- File and Line Or Symbol are required for code-diff findings. -->
338
361
  - File:
339
362
  - Line Or Symbol:
363
+ <!-- Finding Scope is required for code-diff findings. Use test-only only when correction needs no production, runtime, public-contract, dependency, generated-context, architecture, or shared-production change. -->
364
+ - Finding Scope: test-only|implementation
340
365
  - Evidence:
341
366
  - Expected:
342
367
  - Gap:
@@ -375,6 +400,7 @@ If there are no findings, write:
375
400
  - Boundary And Failure Coverage:
376
401
  - Public Contract Coverage:
377
402
  - Test Integrity:
403
+ - Test Infrastructure:
378
404
  - Skips And Gaps:
379
405
  - User Approval And Gap Disposition:
380
406
  - Validation Readiness:
@@ -464,8 +490,8 @@ Use this skill at every project-manager Gate Review trigger point and whenever V
464
490
  ## Trigger Points
465
491
 
466
492
  - \`architecture-plan\`: after the user confirms \`.ai/vcm/handoffs/architecture-brief.md\` and architect writes \`.ai/vcm/handoffs/architecture-plan.md\`, before coder dispatch.
467
- - \`validation-adequacy\`: after tester writes a terminal \`Test Result: pass|fail\` that the active flow permits to reach the gate, before post-validation docs sync or final acceptance in a code-delivery flow, or before Validation-Only Flow completion. Never request this gate for \`Test Result: incomplete\`.
468
- - \`code-diff\`: after Coder returns \`Decision: ready_for_review\`, Architect Debug Mode completes a code fix, or Architecture Diagnosis Mode completes a code fix, before PM routes to Tester. Identify the source with \`--source coder\`, \`--source architect-debug\`, or \`--source architect-diagnosis\`.
493
+ - \`validation-adequacy\`: after tester writes a terminal \`Test Result: pass|fail\` that the active flow permits to reach the gate. Never request this gate for \`Test Result: incomplete\`.
494
+ - \`code-diff\`: after Tester completes and the current validation-adequacy Gate finishes successfully for a Coder implementation, Architect Debug fix, or Architecture Diagnosis fix. Identify the production-code source with \`--source coder\`, \`--source architect-debug\`, or \`--source architect-diagnosis\`. Validation-Only Flow does not request code-diff.
469
495
 
470
496
  ## Request
471
497
 
@@ -500,6 +526,7 @@ import argparse
500
526
  import hashlib
501
527
  import json
502
528
  import os
529
+ import re
503
530
  import subprocess
504
531
  import sys
505
532
  import urllib.error
@@ -528,7 +555,10 @@ SOURCE_ARTIFACTS = {
528
555
  ".ai/vcm/handoffs/test-report.md",
529
556
  "docs/TESTING.md",
530
557
  ],
531
- "code-diff": [],
558
+ "code-diff": [
559
+ ".ai/vcm/handoffs/test-report.md",
560
+ ".ai/vcm/gate-reviews/validation-adequacy-review.md",
561
+ ],
532
562
  }
533
563
  CODE_DIFF_SOURCE_ARTIFACTS = {
534
564
  "coder": [
@@ -703,11 +733,14 @@ def code_diff_sources(gate_record: dict, source: str | None, code_diff: dict) ->
703
733
  def source_artifacts(gate: str, sources: list[str] | None) -> list[str]:
704
734
  if gate != "code-diff":
705
735
  return SOURCE_ARTIFACTS[gate]
706
- return list(dict.fromkeys(
736
+ return list(dict.fromkeys([
737
+ *SOURCE_ARTIFACTS["code-diff"],
738
+ *(
707
739
  artifact
708
740
  for source in (sources or [])
709
741
  for artifact in CODE_DIFF_SOURCE_ARTIFACTS.get(source, [])
710
- ))
742
+ ),
743
+ ]))
711
744
 
712
745
 
713
746
  def input_hash(root: Path, gate: str, sources: list[str] | None = None, gate_record=None) -> str:
@@ -787,6 +820,31 @@ def core_input_status(root: Path, gate: str) -> tuple[str, str] | None:
787
820
  return (core_artifact, "ready")
788
821
 
789
822
 
823
+ def code_diff_prerequisite_error(root: Path, index: dict) -> str | None:
824
+ report_path = root / ".ai/vcm/handoffs/test-report.md"
825
+ try:
826
+ report = report_path.read_text()
827
+ except OSError:
828
+ return "code-diff requires completed Tester validation. .ai/vcm/handoffs/test-report.md is missing."
829
+ result = re.search(r"^\\s*Test Result\\s*:\\s*(pass|fail|incomplete)\\s*$", report, re.IGNORECASE | re.MULTILINE)
830
+ if result is None:
831
+ return "code-diff requires completed Tester validation. Test Result must be exactly pass or fail."
832
+ if result.group(1).lower() == "incomplete":
833
+ return "code-diff requires completed Tester validation. Test Result is incomplete."
834
+
835
+ validation = index.get("gates", {}).get("validation-adequacy", {})
836
+ if not isinstance(validation, dict) or not validation.get("required", False):
837
+ return None
838
+ if validation.get("status") in ("skipped", "overridden"):
839
+ return None
840
+ if validation.get("status") != "completed" or validation.get("decision") != "approve":
841
+ return "code-diff requires the validation-adequacy Gate to complete successfully for the current Tester evidence."
842
+ current_hash = input_hash(root, "validation-adequacy")
843
+ if not validation.get("inputHash") or validation.get("inputHash") != current_hash:
844
+ return "code-diff requires a current validation-adequacy approval; code or test evidence changed after the recorded approval."
845
+ return None
846
+
847
+
790
848
  def request_id(gate: str) -> str:
791
849
  stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
792
850
  return f"{stamp}-{gate}-{uuid.uuid4().hex[:8]}"
@@ -883,6 +941,37 @@ def local_request(gate: str, source: str | None) -> int:
883
941
  print_result("not_required", gate=gate, message=f"{core_status[0]} is {core_status[1]}.")
884
942
  return 0
885
943
 
944
+ if gate == "code-diff":
945
+ prerequisite_error = code_diff_prerequisite_error(root, index)
946
+ if prerequisite_error:
947
+ gate_record = index["gates"].setdefault(gate, {})
948
+ gate_record.update({
949
+ "required": True,
950
+ "status": "failed",
951
+ "decision": None,
952
+ "error": prerequisite_error,
953
+ "exceptionReason": None,
954
+ "requestId": None,
955
+ "requestPath": None,
956
+ "inputHash": None,
957
+ "baseCommit": None,
958
+ "headCommit": None,
959
+ "commits": None,
960
+ "changedFiles": None,
961
+ "diffStat": None,
962
+ "requestedAt": None,
963
+ "startedAt": None,
964
+ "completedAt": now_iso(),
965
+ "callbackStatus": "not_sent",
966
+ "callbackError": None,
967
+ "updatedAt": now_iso(),
968
+ })
969
+ if index.get("activeGate") == gate:
970
+ index["activeGate"] = None
971
+ write_json(index_path, index)
972
+ print_result("failed_to_start", gate=gate, reason=prerequisite_error)
973
+ return 2
974
+
886
975
  gate_record = index["gates"].get(gate, {})
887
976
  code_diff = {}
888
977
  if gate == "code-diff":