vibe-coding-master 0.7.4 → 0.7.6

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 (57) hide show
  1. package/README.md +19 -16
  2. package/dist/backend/adapters/git-adapter.js +15 -0
  3. package/dist/backend/api/artifact-routes.js +3 -0
  4. package/dist/backend/api/harness-routes.js +50 -27
  5. package/dist/backend/api/runtime-state-routes.js +7 -3
  6. package/dist/backend/api/task-routes.js +36 -4
  7. package/dist/backend/api/translation-routes.js +11 -2
  8. package/dist/backend/api/translation-worker-routes.js +37 -9
  9. package/dist/backend/cli/install-vcm-harness.js +40 -2
  10. package/dist/backend/gateway/gateway-service.js +34 -17
  11. package/dist/backend/server.js +12 -3
  12. package/dist/backend/services/artifact-service.js +5 -1
  13. package/dist/backend/services/auto-memory-service.js +156 -81
  14. package/dist/backend/services/claude-hook-service.js +50 -35
  15. package/dist/backend/services/command-dispatcher.js +1 -1
  16. package/dist/backend/services/gate-review-service.js +335 -31
  17. package/dist/backend/services/harness-feedback-service.js +19 -8
  18. package/dist/backend/services/harness-service.js +112 -34
  19. package/dist/backend/services/message-service.js +39 -2
  20. package/dist/backend/services/round-service.js +10 -121
  21. package/dist/backend/services/runtime-coordinator-service.js +18 -10
  22. package/dist/backend/services/runtime-recovery-service.js +1 -2
  23. package/dist/backend/services/session-service.js +36 -98
  24. package/dist/backend/services/status-service.js +1 -0
  25. package/dist/backend/services/task-close-service.js +12 -27
  26. package/dist/backend/services/task-workflow-service.js +228 -0
  27. package/dist/backend/services/translation-worker-service.js +14 -7
  28. package/dist/backend/templates/handoff.js +128 -1
  29. package/dist/backend/templates/harness/architect-agent.js +85 -22
  30. package/dist/backend/templates/harness/claude-root.js +25 -29
  31. package/dist/backend/templates/harness/coder-agent.js +5 -7
  32. package/dist/backend/templates/harness/coder-worker-agent.js +3 -3
  33. package/dist/backend/templates/harness/gate-review.js +292 -65
  34. package/dist/backend/templates/harness/harness-engineer-agent.js +8 -8
  35. package/dist/backend/templates/harness/memory-block.js +69 -0
  36. package/dist/backend/templates/harness/project-known-issues.js +1 -0
  37. package/dist/backend/templates/harness/project-manager-agent.js +217 -75
  38. package/dist/backend/templates/harness/role-memory.js +9 -12
  39. package/dist/backend/templates/harness/tester-agent.js +8 -4
  40. package/dist/backend/templates/harness/vcm-architecture-interview-skill.js +82 -0
  41. package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +4 -3
  42. package/dist/backend/templates/harness/vcm-harness-bootstrap-skill.js +14 -3
  43. package/dist/backend/templates/harness/vcm-propose-memory-skill.js +2 -2
  44. package/dist/backend/templates/harness/vcm-route-message-skill.js +5 -0
  45. package/dist/backend/templates/harness/vcm-task-state-skill.js +110 -0
  46. package/dist/backend/templates/message-envelope.js +1 -1
  47. package/dist/shared/constants.js +0 -10
  48. package/dist/shared/types/workflow.js +1 -0
  49. package/dist/shared/validation/artifact-check.js +41 -1
  50. package/dist-frontend/assets/index-BO2AuF-q.js +97 -0
  51. package/dist-frontend/assets/index-C2etsYlK.css +32 -0
  52. package/dist-frontend/index.html +2 -2
  53. package/package.json +1 -1
  54. package/scripts/harness-tools/check-durable-docs +298 -0
  55. package/scripts/verify-package.mjs +1 -0
  56. package/dist-frontend/assets/index-DCb-S6Ls.css +0 -32
  57. package/dist-frontend/assets/index-NTlycxx9.js +0 -97
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import path from "node:path";
3
3
  import { CODE_DIFF_SOURCES, GATE_REVIEW_GATES } from "../../shared/types/gate-review.js";
4
+ import { checkMarkdownArtifact } from "../../shared/validation/artifact-check.js";
4
5
  import { VcmError } from "../errors.js";
5
6
  import { resolveRepoPath } from "../adapters/filesystem.js";
6
7
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
@@ -13,13 +14,54 @@ const GATE_REVIEWER_ROLE = "gate-reviewer";
13
14
  const DEFAULT_REPORT_POLL_INTERVAL_MS = 1000;
14
15
  const DEFAULT_REPORT_TIMEOUT_MS = 30 * 60 * 1000;
15
16
  const activeRuns = new Set();
17
+ const ARCHITECTURE_ANALYSIS_FIELDS = [
18
+ "Evidence Read",
19
+ "Architecture Brief Fit",
20
+ "End-To-End Flow",
21
+ "Scope Fit",
22
+ "Code Reality",
23
+ "Ownership",
24
+ "Data Flow",
25
+ "Lifecycle",
26
+ "Invariants",
27
+ "Boundaries And Public Surface",
28
+ "Failure Model",
29
+ "Coder Readiness"
30
+ ];
31
+ const VALIDATION_ANALYSIS_FIELDS = [
32
+ "Evidence Read",
33
+ "Changed Behavior And Risk",
34
+ "Coverage Mapping",
35
+ "Baseline Coverage",
36
+ "Integration And E2E Coverage",
37
+ "Boundary And Failure Coverage",
38
+ "Public Contract Coverage",
39
+ "Test Integrity",
40
+ "Skips And Gaps",
41
+ "Validation Readiness"
42
+ ];
43
+ const CODE_DIFF_ANALYSIS_FIELDS = [
44
+ "Commit Range And Sources",
45
+ "Evidence Read",
46
+ "Changed Files And Symbols",
47
+ "Changed Behavior",
48
+ "Source Evidence Fit",
49
+ "Callers And Public Surface",
50
+ "State Lifecycle And Failure Paths",
51
+ "Coding Standards",
52
+ "Baseline Test Integrity",
53
+ "Generated Context And Durable Docs",
54
+ "Code Readiness"
55
+ ];
16
56
  const SOURCE_ARTIFACTS = {
17
57
  "architecture-plan": [
58
+ ".ai/vcm/handoffs/architecture-brief.md",
18
59
  ".ai/vcm/handoffs/architecture-plan.md"
19
60
  ],
20
61
  "validation-adequacy": [
21
62
  ".ai/vcm/handoffs/architecture-plan.md",
22
- ".ai/vcm/handoffs/test-report.md"
63
+ ".ai/vcm/handoffs/test-report.md",
64
+ "docs/TESTING.md"
23
65
  ],
24
66
  "code-diff": []
25
67
  };
@@ -29,7 +71,8 @@ const CODE_DIFF_SOURCE_ARTIFACTS = {
29
71
  ".ai/vcm/handoffs/coder-completion.md"
30
72
  ],
31
73
  "architect-debug": [
32
- ".ai/vcm/handoffs/role-commands/architect.md"
74
+ ".ai/vcm/handoffs/role-commands/architect.md",
75
+ ".ai/vcm/handoffs/architect-debug.md"
33
76
  ],
34
77
  "architect-diagnosis": [
35
78
  ".ai/vcm/handoffs/architecture-diagnosis.md"
@@ -103,6 +146,7 @@ export function createGateReviewService(deps) {
103
146
  decision: undefined,
104
147
  error: message,
105
148
  codeDiffSource: undefined,
149
+ codeDiffSources: undefined,
106
150
  requestId: undefined,
107
151
  requestPath: undefined,
108
152
  inputHash: undefined,
@@ -143,6 +187,7 @@ export function createGateReviewService(deps) {
143
187
  changedFiles: undefined,
144
188
  diffStat: undefined,
145
189
  codeDiffSource,
190
+ codeDiffSources: codeDiffSource ? [codeDiffSource] : undefined,
146
191
  requestedAt: undefined,
147
192
  startedAt: undefined,
148
193
  completedAt: now(),
@@ -158,6 +203,32 @@ export function createGateReviewService(deps) {
158
203
  };
159
204
  }
160
205
  }
206
+ if (gate === "architecture-plan") {
207
+ const architectureBriefError = await readArchitectureBriefError(deps.fs, context.taskRepoRoot);
208
+ if (architectureBriefError) {
209
+ index = applyGateState(index, gate, {
210
+ status: "failed",
211
+ decision: undefined,
212
+ error: architectureBriefError,
213
+ exceptionReason: undefined,
214
+ requestId: undefined,
215
+ requestPath: undefined,
216
+ inputHash: undefined,
217
+ requestedAt: undefined,
218
+ startedAt: undefined,
219
+ completedAt: now(),
220
+ callbackStatus: "not_sent",
221
+ callbackError: undefined
222
+ }, now(), true);
223
+ await saveIndex(deps.fs, context.taskRepoRoot, index);
224
+ return {
225
+ status: "failed_to_start",
226
+ gate,
227
+ record: index.gates[gate],
228
+ message: architectureBriefError
229
+ };
230
+ }
231
+ }
161
232
  const coreInput = await readCoreInputArtifact(deps.fs, context.taskRepoRoot, gate);
162
233
  if (coreInput && coreInput.status !== "ready") {
163
234
  index = applyGateState(index, gate, {
@@ -200,6 +271,7 @@ export function createGateReviewService(deps) {
200
271
  changedFiles: undefined,
201
272
  diffStat: undefined,
202
273
  codeDiffSource,
274
+ codeDiffSources: codeDiffSource ? [codeDiffSource] : undefined,
203
275
  requestedAt: undefined,
204
276
  startedAt: undefined,
205
277
  completedAt: undefined,
@@ -214,7 +286,10 @@ export function createGateReviewService(deps) {
214
286
  message: "No new commits to review."
215
287
  };
216
288
  }
217
- const inputHash = await computeInputHash(deps, context.taskRepoRoot, gate, codeDiffInput, codeDiffSource);
289
+ const codeDiffSources = gate === "code-diff" && codeDiffInput && codeDiffSource
290
+ ? resolveCodeDiffSources(record, codeDiffInput, codeDiffSource)
291
+ : undefined;
292
+ const inputHash = await computeInputHash(deps, context.taskRepoRoot, gate, codeDiffInput, codeDiffSources);
218
293
  if (!options.force
219
294
  && record.status === "completed"
220
295
  && record.decision === "approve"
@@ -246,6 +321,7 @@ export function createGateReviewService(deps) {
246
321
  changedFiles: codeDiffInput?.changedFiles,
247
322
  diffStat: codeDiffInput?.diffStat,
248
323
  codeDiffSource,
324
+ codeDiffSources,
249
325
  requestedAt: timestamp,
250
326
  startedAt: undefined,
251
327
  completedAt: undefined,
@@ -270,12 +346,13 @@ export function createGateReviewService(deps) {
270
346
  requestedAt: timestamp,
271
347
  inputHash,
272
348
  codeDiffSource,
349
+ codeDiffSources,
273
350
  codeDiff: codeDiffInput,
274
351
  reportPath: nextRecord.reportPath,
275
352
  promptPath: nextRecord.promptPath
276
353
  });
277
354
  await saveIndex(deps.fs, context.taskRepoRoot, index);
278
- void runGateReview(context, gate, requestId, codeDiffInput, codeDiffSource).catch(() => {
355
+ void runGateReview(context, gate, requestId, codeDiffInput, codeDiffSources).catch(() => {
279
356
  // runGateReview records failures in the persisted gate state.
280
357
  });
281
358
  return {
@@ -285,7 +362,7 @@ export function createGateReviewService(deps) {
285
362
  message: "Gate review started."
286
363
  };
287
364
  }
288
- async function runGateReview(context, gate, requestId, codeDiffInput, codeDiffSource) {
365
+ async function runGateReview(context, gate, requestId, codeDiffInput, codeDiffSources) {
289
366
  const runKey = `${context.taskRepoRoot}:${context.taskSlug}:${gate}`;
290
367
  if (activeRuns.has(runKey)) {
291
368
  return;
@@ -302,7 +379,7 @@ export function createGateReviewService(deps) {
302
379
  await updateRequestStatus(deps.fs, context, requestId, "running", { startedAt: timestamp });
303
380
  const reviewDir = resolveRepoPath(context.taskRepoRoot, GATE_REVIEW_DIR);
304
381
  const agentPath = resolveRepoPath(context.repoRoot, GATE_REVIEW_AGENT_PATH);
305
- const prompt = buildGatePrompt(context, gate, requestId, codeDiffInput, codeDiffSource);
382
+ const prompt = buildGatePrompt(context, gate, requestId, codeDiffInput, codeDiffSources);
306
383
  await deps.fs.ensureDir(reviewDir);
307
384
  await deps.fs.ensureDir(resolveRepoPath(context.taskRepoRoot, REQUESTS_DIR));
308
385
  await deps.fs.writeText(resolveRepoPath(context.taskRepoRoot, promptPathForRequest(requestId)), prompt);
@@ -333,6 +410,12 @@ export function createGateReviewService(deps) {
333
410
  const completedAt = now();
334
411
  await recordGateReviewerTurnStop(context, gateTurnStarted);
335
412
  gateTurnStarted = false;
413
+ await updateRequestStatus(deps.fs, context, requestId, "completed", {
414
+ completedAt,
415
+ decision: parsed.decision,
416
+ reportPath: parsed.reportPath
417
+ });
418
+ activeRuns.delete(runKey);
336
419
  await updateGateRecord(context, gate, {
337
420
  status: "completed",
338
421
  decision: parsed.decision,
@@ -344,11 +427,6 @@ export function createGateReviewService(deps) {
344
427
  callbackError: undefined,
345
428
  updatedAt: completedAt
346
429
  }, { clearActiveGate: true });
347
- await updateRequestStatus(deps.fs, context, requestId, "completed", {
348
- completedAt,
349
- decision: parsed.decision,
350
- reportPath: parsed.reportPath
351
- });
352
430
  await callbackProjectManager(context, gate, "completed", parsed.decision, parsed.reportPath);
353
431
  }
354
432
  catch (error) {
@@ -356,6 +434,11 @@ export function createGateReviewService(deps) {
356
434
  const message = errorMessage(error);
357
435
  await recordGateReviewerTurnStop(context, gateTurnStarted);
358
436
  gateTurnStarted = false;
437
+ await updateRequestStatus(deps.fs, context, requestId, "failed", {
438
+ completedAt: timestamp,
439
+ error: message
440
+ });
441
+ activeRuns.delete(runKey);
359
442
  await updateGateRecord(context, gate, {
360
443
  status: "failed",
361
444
  error: message,
@@ -364,10 +447,6 @@ export function createGateReviewService(deps) {
364
447
  callbackError: undefined,
365
448
  updatedAt: timestamp
366
449
  }, { clearActiveGate: true });
367
- await updateRequestStatus(deps.fs, context, requestId, "failed", {
368
- completedAt: timestamp,
369
- error: message
370
- });
371
450
  await callbackProjectManager(context, gate, "failed", undefined, reportPathForGate(gate), message);
372
451
  }
373
452
  finally {
@@ -598,6 +677,7 @@ function normalizeIndex(raw, config, timestamp) {
598
677
  changedFiles: Array.isArray(existing?.changedFiles) ? existing.changedFiles.filter(isString) : undefined,
599
678
  diffStat: typeof existing?.diffStat === "string" ? existing.diffStat : undefined,
600
679
  codeDiffSource: isCodeDiffSource(existing?.codeDiffSource) ? existing.codeDiffSource : undefined,
680
+ codeDiffSources: normalizeCodeDiffSources(existing?.codeDiffSources, existing?.codeDiffSource),
601
681
  summary: typeof existing?.summary === "string" ? existing.summary : undefined,
602
682
  findings: Array.isArray(existing?.findings) ? existing.findings.filter(isFinding) : undefined,
603
683
  error: typeof existing?.error === "string" ? existing.error : undefined,
@@ -766,23 +846,25 @@ async function isAncestor(runner, cwd, ancestor, descendant) {
766
846
  const result = await runner.run("git", ["merge-base", "--is-ancestor", ancestor, descendant], { cwd });
767
847
  return result.exitCode === 0;
768
848
  }
769
- async function computeInputHash(deps, taskRepoRoot, gate, codeDiffInput, codeDiffSource) {
849
+ async function computeInputHash(deps, taskRepoRoot, gate, codeDiffInput, codeDiffSources) {
770
850
  const digest = createHash("sha256");
771
851
  const coreArtifact = CORE_INPUT_ARTIFACTS[gate];
772
852
  if (coreArtifact) {
773
853
  digest.update(coreArtifact);
774
854
  digest.update(await deps.fs.readText(resolveRepoPath(taskRepoRoot, coreArtifact)));
775
- return digest.digest("hex");
776
855
  }
777
856
  const common = [
778
857
  "CLAUDE.md",
858
+ ".claude/agents/architect.md",
859
+ ".claude/agents/coder.md",
779
860
  ".claude/agents/gate-reviewer.md",
861
+ ".claude/agents/tester.md",
780
862
  ".claude/skills/vcm-gate-review/SKILL.md",
781
863
  ".ai/tools/request-gate-review",
782
864
  "docs/CODING_STANDARDS.md"
783
865
  ];
784
- const sourceArtifacts = getSourceArtifacts(gate, codeDiffSource);
785
- for (const relativePath of [...common, ...sourceArtifacts]) {
866
+ const sourceArtifacts = getSourceArtifacts(gate, codeDiffSources);
867
+ for (const relativePath of new Set([...common, ...sourceArtifacts].filter((item) => item !== coreArtifact))) {
786
868
  digest.update(relativePath);
787
869
  const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
788
870
  if (await deps.fs.pathExists(absolutePath)) {
@@ -793,8 +875,8 @@ async function computeInputHash(deps, taskRepoRoot, gate, codeDiffInput, codeDif
793
875
  }
794
876
  }
795
877
  if (gate === "code-diff" && codeDiffInput) {
796
- digest.update("codeDiffSource");
797
- digest.update(codeDiffSource ?? "<missing>");
878
+ digest.update("codeDiffSources");
879
+ digest.update(codeDiffSources?.join("\n") ?? "<missing>");
798
880
  digest.update("baseCommit");
799
881
  digest.update(codeDiffInput.baseCommit);
800
882
  digest.update("headCommit");
@@ -807,9 +889,44 @@ async function computeInputHash(deps, taskRepoRoot, gate, codeDiffInput, codeDif
807
889
  digest.update(codeDiffInput.diffHash);
808
890
  }
809
891
  if (gate === "architecture-plan") {
810
- digest.update(await commandStdout(deps.runner, taskRepoRoot, ["status", "--porcelain=v1"]));
811
- digest.update(await commandStdout(deps.runner, taskRepoRoot, ["diff", "--binary"]));
812
- digest.update(await commandStdout(deps.runner, taskRepoRoot, ["diff", "--cached", "--binary"]));
892
+ const evidencePathspec = ["--", ".", ":(exclude).ai/vcm/**"];
893
+ digest.update("head");
894
+ digest.update(await commandStdout(deps.runner, taskRepoRoot, ["rev-parse", "HEAD"]));
895
+ digest.update("workingDiff");
896
+ digest.update(await commandStdout(deps.runner, taskRepoRoot, ["diff", "--binary", ...evidencePathspec]));
897
+ digest.update("stagedDiff");
898
+ digest.update(await commandStdout(deps.runner, taskRepoRoot, ["diff", "--cached", "--binary", ...evidencePathspec]));
899
+ const untracked = splitLines(await commandStdout(deps.runner, taskRepoRoot, [
900
+ "ls-files",
901
+ "--others",
902
+ "--exclude-standard",
903
+ ...evidencePathspec
904
+ ]));
905
+ for (const relativePath of untracked) {
906
+ digest.update("untracked");
907
+ digest.update(relativePath);
908
+ digest.update(await commandStdout(deps.runner, taskRepoRoot, ["hash-object", "--", relativePath]));
909
+ }
910
+ }
911
+ if (gate === "validation-adequacy") {
912
+ const evidencePathspec = ["--", ".", ":(exclude).ai/vcm/**", ":(exclude)docs/**"];
913
+ digest.update("trackedEvidence");
914
+ digest.update(await commandStdout(deps.runner, taskRepoRoot, ["ls-files", "-s", ...evidencePathspec]));
915
+ digest.update("workingEvidence");
916
+ digest.update(await commandStdout(deps.runner, taskRepoRoot, ["diff", "--binary", ...evidencePathspec]));
917
+ digest.update("stagedEvidence");
918
+ digest.update(await commandStdout(deps.runner, taskRepoRoot, ["diff", "--cached", "--binary", ...evidencePathspec]));
919
+ const untracked = splitLines(await commandStdout(deps.runner, taskRepoRoot, [
920
+ "ls-files",
921
+ "--others",
922
+ "--exclude-standard",
923
+ ...evidencePathspec
924
+ ]));
925
+ for (const relativePath of untracked) {
926
+ digest.update("untrackedEvidence");
927
+ digest.update(relativePath);
928
+ digest.update(await commandStdout(deps.runner, taskRepoRoot, ["hash-object", "--", relativePath]));
929
+ }
813
930
  }
814
931
  return digest.digest("hex");
815
932
  }
@@ -828,6 +945,22 @@ async function readCoreInputArtifact(fs, taskRepoRoot, gate) {
828
945
  }
829
946
  return { path: relativePath, status: "ready" };
830
947
  }
948
+ async function readArchitectureBriefError(fs, taskRepoRoot) {
949
+ const relativePath = ".ai/vcm/handoffs/architecture-brief.md";
950
+ const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
951
+ if (!await fs.pathExists(absolutePath)) {
952
+ return `${relativePath} is missing. Complete Architect Interview before architecture planning.`;
953
+ }
954
+ const content = await fs.readText(absolutePath);
955
+ const check = checkMarkdownArtifact("architecture-brief", relativePath, content);
956
+ if (check.status !== "ok") {
957
+ return `${relativePath} is incomplete. Complete and confirm Architect Interview before requesting architecture-plan review.`;
958
+ }
959
+ if (!/^\s*Architecture Brief Status\s*:\s*confirmed\s*$/im.test(content)) {
960
+ return `${relativePath} is not confirmed. Obtain explicit user confirmation before architecture planning.`;
961
+ }
962
+ return undefined;
963
+ }
831
964
  async function commandStdout(runner, cwd, args) {
832
965
  const result = await runner.run("git", args, { cwd });
833
966
  return result.exitCode === 0 ? result.stdout : "";
@@ -838,21 +971,30 @@ function splitLines(value) {
838
971
  .map((line) => line.trim())
839
972
  .filter(Boolean);
840
973
  }
841
- function buildGatePrompt(context, gate, requestId, codeDiffInput, codeDiffSource) {
974
+ function buildGatePrompt(context, gate, requestId, codeDiffInput, codeDiffSources) {
842
975
  const reportPath = reportPathForGate(gate);
843
976
  const absoluteReportPath = resolveRepoPath(context.taskRepoRoot, reportPath);
844
- const evidence = getSourceArtifacts(gate, codeDiffSource)
977
+ const evidence = getSourceArtifacts(gate, codeDiffSources)
845
978
  .map((relativePath) => `- ${relativePath}`)
846
979
  .join("\n");
847
980
  const gitLine = gate === "architecture-plan"
848
981
  ? "\nDiff: inspect git status/diff in Worktree."
849
982
  : "";
983
+ const architectureContract = gate === "architecture-plan"
984
+ ? "\n\nComplete every Architecture Analysis field required by the Gate Reviewer role with concrete current-worktree evidence before deciding."
985
+ : "";
986
+ const validationContract = gate === "validation-adequacy"
987
+ ? "\n\nComplete every Validation Analysis field required by the Gate Reviewer role with concrete current-worktree production and test evidence before deciding."
988
+ : "";
989
+ const codeDiffContract = gate === "code-diff"
990
+ ? "\n\nComplete every Code Diff Analysis field required by the Gate Reviewer role with concrete evidence from the named commit range before deciding."
991
+ : "";
850
992
  const codeDiffSection = gate === "code-diff" && codeDiffInput
851
993
  ? `
852
994
 
853
995
  Code Diff Input:
854
996
  This code-diff gate reviews the new commits from one PM route flow, not the whole task and not one terminal turn.
855
- Code source: ${codeDiffSource}
997
+ Code sources: ${codeDiffSources?.join(" -> ") ?? "<missing>"}
856
998
  Base commit: ${codeDiffInput.baseCommit}
857
999
  Head commit: ${codeDiffInput.headCommit}
858
1000
  Commits:
@@ -873,7 +1015,7 @@ Request: ${requestId}
873
1015
  Report: ${absoluteReportPath}
874
1016
 
875
1017
  Evidence:
876
- ${evidence}${gitLine}${codeDiffSection}
1018
+ ${evidence}${gitLine}${architectureContract}${validationContract}${codeDiffContract}${codeDiffSection}
877
1019
 
878
1020
  Write only Report. Start exactly:
879
1021
  Gate: ${gate}
@@ -940,17 +1082,155 @@ async function parseGateReport(fs, taskRepoRoot, gate, requestId, timestamp) {
940
1082
  statusCode: 500
941
1083
  });
942
1084
  }
1085
+ const findings = extractFindings(content);
1086
+ if (gate === "architecture-plan") {
1087
+ validateArchitectureAnalysis(content);
1088
+ }
1089
+ if (gate === "validation-adequacy") {
1090
+ validateValidationAnalysis(content);
1091
+ if (decision === "approve") {
1092
+ await validateValidationApprovalInput(fs, taskRepoRoot);
1093
+ }
1094
+ }
1095
+ if (gate === "code-diff") {
1096
+ validateCodeDiffAnalysis(content);
1097
+ }
1098
+ if (decision === "request_changes") {
1099
+ validateRequestChangeFindings(findings);
1100
+ if (gate === "code-diff") {
1101
+ validateCodeDiffFindings(findings);
1102
+ }
1103
+ }
943
1104
  return {
944
1105
  gate,
945
1106
  requestId: parsedRequest,
946
1107
  decision,
947
1108
  summary: extractSummary(content),
948
- findings: extractFindings(content),
1109
+ findings,
949
1110
  reportPath,
950
1111
  content,
951
1112
  parsedAt: timestamp
952
1113
  };
953
1114
  }
1115
+ function validateArchitectureAnalysis(content) {
1116
+ const section = extractMarkdownSection(content, "Architecture Analysis");
1117
+ if (!section) {
1118
+ throw new VcmError({
1119
+ code: "GATE_REVIEW_ARCHITECTURE_ANALYSIS_MISSING",
1120
+ message: "Architecture-plan review must contain a non-empty Architecture Analysis section.",
1121
+ statusCode: 500
1122
+ });
1123
+ }
1124
+ const missingFields = ARCHITECTURE_ANALYSIS_FIELDS.filter((field) => !matchField(section, field));
1125
+ if (missingFields.length > 0) {
1126
+ throw new VcmError({
1127
+ code: "GATE_REVIEW_ARCHITECTURE_ANALYSIS_INCOMPLETE",
1128
+ message: `Architecture Analysis is missing required evidence: ${missingFields.join(", ")}.`,
1129
+ statusCode: 500
1130
+ });
1131
+ }
1132
+ }
1133
+ function validateValidationAnalysis(content) {
1134
+ const section = extractMarkdownSection(content, "Validation Analysis");
1135
+ if (!section) {
1136
+ throw new VcmError({
1137
+ code: "GATE_REVIEW_VALIDATION_ANALYSIS_MISSING",
1138
+ message: "Validation-adequacy review must contain a non-empty Validation Analysis section.",
1139
+ statusCode: 500
1140
+ });
1141
+ }
1142
+ const missingFields = VALIDATION_ANALYSIS_FIELDS.filter((field) => !matchField(section, field));
1143
+ if (missingFields.length > 0) {
1144
+ throw new VcmError({
1145
+ code: "GATE_REVIEW_VALIDATION_ANALYSIS_INCOMPLETE",
1146
+ message: `Validation Analysis is missing required evidence: ${missingFields.join(", ")}.`,
1147
+ statusCode: 500
1148
+ });
1149
+ }
1150
+ }
1151
+ function validateCodeDiffAnalysis(content) {
1152
+ const section = extractMarkdownSection(content, "Code Diff Analysis");
1153
+ if (!section) {
1154
+ throw new VcmError({
1155
+ code: "GATE_REVIEW_CODE_DIFF_ANALYSIS_MISSING",
1156
+ message: "Code-diff review must contain a non-empty Code Diff Analysis section.",
1157
+ statusCode: 500
1158
+ });
1159
+ }
1160
+ const missingFields = CODE_DIFF_ANALYSIS_FIELDS.filter((field) => !matchField(section, field));
1161
+ if (missingFields.length > 0) {
1162
+ throw new VcmError({
1163
+ code: "GATE_REVIEW_CODE_DIFF_ANALYSIS_INCOMPLETE",
1164
+ message: `Code Diff Analysis is missing required evidence: ${missingFields.join(", ")}.`,
1165
+ statusCode: 500
1166
+ });
1167
+ }
1168
+ }
1169
+ async function validateValidationApprovalInput(fs, taskRepoRoot) {
1170
+ const relativePath = CORE_INPUT_ARTIFACTS["validation-adequacy"];
1171
+ if (!relativePath) {
1172
+ return;
1173
+ }
1174
+ const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
1175
+ const content = await fs.pathExists(absolutePath) ? await fs.readText(absolutePath) : null;
1176
+ const check = checkMarkdownArtifact("test-report", relativePath, content);
1177
+ const testResult = content ? matchField(content, "Test Result")?.toLowerCase() : undefined;
1178
+ if (check.status === "ok" && testResult === "pass") {
1179
+ return;
1180
+ }
1181
+ const details = [
1182
+ check.status !== "ok" ? `status=${check.status}` : "",
1183
+ check.missingHeadings.length > 0 ? `missing headings: ${check.missingHeadings.join(", ")}` : "",
1184
+ check.invalidFields.length > 0 ? check.invalidFields.join(" ") : "",
1185
+ check.hasPlaceholder ? "contains placeholders" : "",
1186
+ testResult !== "pass" ? "Test Result must be pass before approval." : ""
1187
+ ].filter(Boolean).join("; ");
1188
+ throw new VcmError({
1189
+ code: "GATE_REVIEW_VALIDATION_INPUT_INCOMPLETE",
1190
+ message: `Validation-adequacy cannot approve incomplete Tester evidence in ${relativePath}. ${details}`,
1191
+ statusCode: 500
1192
+ });
1193
+ }
1194
+ function validateRequestChangeFindings(findings) {
1195
+ if (findings.length === 0) {
1196
+ throw new VcmError({
1197
+ code: "GATE_REVIEW_FINDINGS_MISSING",
1198
+ message: "A request_changes decision must contain at least one structured finding.",
1199
+ statusCode: 500
1200
+ });
1201
+ }
1202
+ const incomplete = findings.find((finding) => (!finding.evidence.trim()
1203
+ || !finding.expected.trim()
1204
+ || !finding.gap.trim()
1205
+ || !finding.risk.trim()));
1206
+ if (incomplete) {
1207
+ throw new VcmError({
1208
+ code: "GATE_REVIEW_FINDING_INCOMPLETE",
1209
+ message: `Finding ${incomplete.title} must contain Evidence, Expected, Gap, and Risk.`,
1210
+ statusCode: 500
1211
+ });
1212
+ }
1213
+ }
1214
+ function validateCodeDiffFindings(findings) {
1215
+ const incomplete = findings.find((finding) => !finding.file?.trim() || !finding.location?.trim());
1216
+ if (incomplete) {
1217
+ throw new VcmError({
1218
+ code: "GATE_REVIEW_CODE_DIFF_FINDING_LOCATION_MISSING",
1219
+ message: `Code-diff finding ${incomplete.title} must contain File and Line Or Symbol.`,
1220
+ statusCode: 500
1221
+ });
1222
+ }
1223
+ }
1224
+ function extractMarkdownSection(content, heading) {
1225
+ const match = new RegExp(`^##\\s+${escapeRegex(heading)}\\s*$`, "im").exec(content);
1226
+ if (!match || match.index === undefined) {
1227
+ return undefined;
1228
+ }
1229
+ const remainder = content.slice(match.index + match[0].length);
1230
+ const nextHeading = remainder.search(/^##\s+/m);
1231
+ const section = (nextHeading >= 0 ? remainder.slice(0, nextHeading) : remainder).trim();
1232
+ return section || undefined;
1233
+ }
954
1234
  async function updateRequestStatus(fs, context, requestId, status, patch) {
955
1235
  const requestPath = resolveRepoPath(context.taskRepoRoot, path.posix.join(REQUESTS_DIR, `${requestId}.json`));
956
1236
  const current = await readJsonOrNull(fs, requestPath) ?? {
@@ -1018,6 +1298,7 @@ function extractFindings(content) {
1018
1298
  title,
1019
1299
  file: matchField(block, "file"),
1020
1300
  line: parsePositiveInteger(matchField(block, "line")),
1301
+ location: matchField(block, "line or symbol"),
1021
1302
  evidence: matchField(block, "evidence") ?? "",
1022
1303
  expected: matchField(block, "expected") ?? "",
1023
1304
  gap: matchField(block, "gap") ?? "",
@@ -1026,11 +1307,30 @@ function extractFindings(content) {
1026
1307
  }
1027
1308
  return findings;
1028
1309
  }
1029
- function getSourceArtifacts(gate, codeDiffSource) {
1310
+ function getSourceArtifacts(gate, codeDiffSources) {
1030
1311
  if (gate !== "code-diff") {
1031
1312
  return SOURCE_ARTIFACTS[gate];
1032
1313
  }
1033
- return codeDiffSource ? CODE_DIFF_SOURCE_ARTIFACTS[codeDiffSource] : [];
1314
+ return [...new Set((codeDiffSources ?? []).flatMap((source) => CODE_DIFF_SOURCE_ARTIFACTS[source]))];
1315
+ }
1316
+ function resolveCodeDiffSources(record, codeDiffInput, currentSource) {
1317
+ const continuingRecordedRange = record.baseCommit === codeDiffInput.baseCommit
1318
+ && ((record.status === "completed" && record.decision === "request_changes")
1319
+ || record.status === "failed");
1320
+ if (!continuingRecordedRange) {
1321
+ return [currentSource];
1322
+ }
1323
+ return [...new Set([
1324
+ ...(normalizeCodeDiffSources(record.codeDiffSources, record.codeDiffSource) ?? []),
1325
+ currentSource
1326
+ ])];
1327
+ }
1328
+ function normalizeCodeDiffSources(sources, source) {
1329
+ const normalized = Array.isArray(sources) ? sources.filter(isCodeDiffSource) : [];
1330
+ if (normalized.length === 0 && isCodeDiffSource(source)) {
1331
+ normalized.push(source);
1332
+ }
1333
+ return normalized.length > 0 ? [...new Set(normalized)] : undefined;
1034
1334
  }
1035
1335
  export function isCodeDiffSource(value) {
1036
1336
  return typeof value === "string" && CODE_DIFF_SOURCES.includes(value);
@@ -1119,6 +1419,10 @@ function errorMessage(error) {
1119
1419
  function isPendingReportError(error) {
1120
1420
  return error instanceof VcmError && [
1121
1421
  "GATE_REVIEW_DECISION_MISSING",
1422
+ "GATE_REVIEW_ARCHITECTURE_ANALYSIS_MISSING",
1423
+ "GATE_REVIEW_ARCHITECTURE_ANALYSIS_INCOMPLETE",
1424
+ "GATE_REVIEW_FINDINGS_MISSING",
1425
+ "GATE_REVIEW_FINDING_INCOMPLETE",
1122
1426
  "GATE_REVIEW_REPORT_GATE_MISMATCH",
1123
1427
  "GATE_REVIEW_REPORT_MISSING",
1124
1428
  "GATE_REVIEW_REPORT_STALE"
@@ -76,8 +76,8 @@ export function createHarnessFeedbackService(deps) {
76
76
  async function assertHarnessEngineerAvailable(_repoRoot) {
77
77
  return undefined;
78
78
  }
79
- async function ensureIdleHarnessEngineer(repoRoot, taskSlug) {
80
- const existing = await deps.sessionService.getProjectHarnessEngineerSession(repoRoot);
79
+ async function getIdleHarnessEngineer(repoRoot, taskSlug) {
80
+ const existing = await deps.sessionService.getRoleSession(repoRoot, taskSlug, "harness-engineer");
81
81
  if (existing?.status === "running" && existing.activityStatus === "running") {
82
82
  throw new VcmError({
83
83
  code: "HARNESS_ENGINEER_BUSY",
@@ -86,12 +86,23 @@ export function createHarnessFeedbackService(deps) {
86
86
  hint: "Wait for the current Harness Engineer turn to finish, then retry."
87
87
  });
88
88
  }
89
- const session = await deps.sessionService.ensureProjectHarnessEngineerSession(repoRoot, {
90
- taskSlug,
91
- cols: 120,
92
- rows: 32
93
- });
94
- if (session.status !== "running" || session.activityStatus === "running" || !deps.runtime.getSession(session.id)) {
89
+ const input = { cols: 120, rows: 32 };
90
+ const session = existing?.status === "running"
91
+ ? existing
92
+ : existing?.claudeSessionId
93
+ ? await deps.sessionService.resumeRoleSession(repoRoot, taskSlug, "harness-engineer", input)
94
+ : await deps.sessionService.startRoleSession(repoRoot, taskSlug, "harness-engineer", input);
95
+ if (session.status !== "running" || session.activityStatus === "running") {
96
+ return undefined;
97
+ }
98
+ if (!deps.runtime.getSession(session.id)) {
99
+ return undefined;
100
+ }
101
+ return session;
102
+ }
103
+ async function ensureIdleHarnessEngineer(repoRoot, taskSlug) {
104
+ const session = await getIdleHarnessEngineer(repoRoot, taskSlug);
105
+ if (!session) {
95
106
  throw new VcmError({
96
107
  code: "HARNESS_ENGINEER_BUSY",
97
108
  message: "Harness Engineer is busy or unavailable.",