vibe-coding-master 0.6.19 → 0.6.21

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 (41) hide show
  1. package/README.md +5 -5
  2. package/dist/backend/api/artifact-routes.js +2 -2
  3. package/dist/backend/api/gate-review-routes.js +1 -1
  4. package/dist/backend/api/task-routes.js +1 -1
  5. package/dist/backend/cli/install-vcm-harness.js +66 -20
  6. package/dist/backend/services/app-settings-service.js +11 -2
  7. package/dist/backend/services/artifact-service.js +9 -8
  8. package/dist/backend/services/gate-review-service.js +319 -24
  9. package/dist/backend/services/harness-feedback-service.js +8 -3
  10. package/dist/backend/services/harness-service.js +100 -13
  11. package/dist/backend/services/job-guard-service.js +2 -2
  12. package/dist/backend/services/message-service.js +12 -0
  13. package/dist/backend/services/session-service.js +18 -11
  14. package/dist/backend/services/status-service.js +1 -4
  15. package/dist/backend/templates/handoff.js +105 -17
  16. package/dist/backend/templates/harness/architect-agent.js +51 -27
  17. package/dist/backend/templates/harness/claude-root.js +43 -15
  18. package/dist/backend/templates/harness/coder-agent.js +66 -47
  19. package/dist/backend/templates/harness/coder-worker-agent.js +45 -17
  20. package/dist/backend/templates/harness/gate-review.js +229 -39
  21. package/dist/backend/templates/harness/harness-engineer-agent.js +39 -12
  22. package/dist/backend/templates/harness/project-coding-standards.js +73 -0
  23. package/dist/backend/templates/harness/project-glossary.js +30 -0
  24. package/dist/backend/templates/harness/project-known-issues.js +33 -0
  25. package/dist/backend/templates/harness/project-manager-agent.js +97 -27
  26. package/dist/backend/templates/harness/pull-request-template.js +6 -4
  27. package/dist/backend/templates/harness/tester-agent.js +82 -0
  28. package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +19 -15
  29. package/dist/backend/templates/harness/vcm-harness-bootstrap-skill.js +19 -2
  30. package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +1 -1
  31. package/dist/backend/templates/harness/vcm-report-harness-issue-skill.js +18 -12
  32. package/dist/backend/templates/harness/vcm-route-message-skill.js +48 -8
  33. package/dist/backend/templates/message-envelope.js +2 -5
  34. package/dist/shared/constants.js +3 -3
  35. package/dist/shared/types/gate-review.js +5 -1
  36. package/dist/shared/validation/artifact-check.js +64 -10
  37. package/dist-frontend/assets/{index-C5E5jLp-.js → index-DYBg_qYS.js} +4 -4
  38. package/dist-frontend/index.html +1 -1
  39. package/package.json +1 -1
  40. package/scripts/verify-package.mjs +1 -1
  41. package/dist/backend/templates/harness/reviewer-agent.js +0 -60
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import path from "node:path";
3
- import { GATE_REVIEW_GATES } from "../../shared/types/gate-review.js";
3
+ import { CODE_DIFF_SOURCES, GATE_REVIEW_GATES } from "../../shared/types/gate-review.js";
4
4
  import { VcmError } from "../errors.js";
5
5
  import { resolveRepoPath } from "../adapters/filesystem.js";
6
6
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
@@ -19,18 +19,22 @@ const SOURCE_ARTIFACTS = {
19
19
  ],
20
20
  "validation-adequacy": [
21
21
  ".ai/vcm/handoffs/architecture-plan.md",
22
- ".ai/vcm/handoffs/review-report.md"
22
+ ".ai/vcm/handoffs/test-report.md"
23
23
  ],
24
- "final-diff": [
24
+ "code-diff": []
25
+ };
26
+ const CODE_DIFF_SOURCE_ARTIFACTS = {
27
+ coder: [
25
28
  ".ai/vcm/handoffs/architecture-plan.md",
26
- ".ai/vcm/handoffs/review-report.md",
27
- ".ai/vcm/handoffs/docs-sync-report.md",
28
- ".ai/vcm/handoffs/final-acceptance.md"
29
+ ".ai/vcm/handoffs/coder-completion.md"
30
+ ],
31
+ "architect-debug": [
32
+ ".ai/vcm/handoffs/role-commands/architect.md"
29
33
  ]
30
34
  };
31
35
  const CORE_INPUT_ARTIFACTS = {
32
36
  "architecture-plan": ".ai/vcm/handoffs/architecture-plan.md",
33
- "validation-adequacy": ".ai/vcm/handoffs/review-report.md"
37
+ "validation-adequacy": ".ai/vcm/handoffs/test-report.md"
34
38
  };
35
39
  const VALID_SEVERITIES = new Set(["critical", "high", "medium", "low"]);
36
40
  export function createGateReviewService(deps) {
@@ -54,6 +58,11 @@ export function createGateReviewService(deps) {
54
58
  const context = await getContext(repoRoot, taskSlug);
55
59
  let index = await loadIndex(deps.fs, context, now());
56
60
  const record = index.gates[gate];
61
+ const requestedCodeDiffSource = options.codeDiffSource
62
+ ?? (options.force ? record.codeDiffSource : undefined);
63
+ const codeDiffSource = gate === "code-diff" && isCodeDiffSource(requestedCodeDiffSource)
64
+ ? requestedCodeDiffSource
65
+ : undefined;
57
66
  if (!context.config.enabled) {
58
67
  index = applyGateState(index, gate, {
59
68
  status: "disabled",
@@ -84,6 +93,68 @@ export function createGateReviewService(deps) {
84
93
  if (record.status === "running" && !options.force) {
85
94
  return { status: "running", gate, record, message: "Gate review is already running." };
86
95
  }
96
+ if (gate === "code-diff" && !codeDiffSource) {
97
+ const message = "code-diff requires --source coder or --source architect-debug.";
98
+ index = applyGateState(index, gate, {
99
+ status: "failed",
100
+ decision: undefined,
101
+ error: message,
102
+ codeDiffSource: undefined,
103
+ requestId: undefined,
104
+ requestPath: undefined,
105
+ inputHash: undefined,
106
+ baseCommit: undefined,
107
+ headCommit: undefined,
108
+ commits: undefined,
109
+ changedFiles: undefined,
110
+ diffStat: undefined,
111
+ requestedAt: undefined,
112
+ startedAt: undefined,
113
+ completedAt: now(),
114
+ callbackStatus: "not_sent",
115
+ callbackError: undefined
116
+ }, now(), true);
117
+ await saveIndex(deps.fs, context.taskRepoRoot, index);
118
+ return {
119
+ status: "failed_to_start",
120
+ gate,
121
+ record: index.gates[gate],
122
+ message
123
+ };
124
+ }
125
+ if (gate === "code-diff") {
126
+ const dirtyStatus = await getDirtyCodeDiffStatus(deps.runner, context.taskRepoRoot);
127
+ if (dirtyStatus) {
128
+ const message = `code-diff requires committed inputs; commit or clean these changes first: ${dirtyStatus}`;
129
+ index = applyGateState(index, gate, {
130
+ status: "failed",
131
+ decision: undefined,
132
+ error: message,
133
+ exceptionReason: undefined,
134
+ requestId: undefined,
135
+ requestPath: undefined,
136
+ inputHash: undefined,
137
+ baseCommit: undefined,
138
+ headCommit: undefined,
139
+ commits: undefined,
140
+ changedFiles: undefined,
141
+ diffStat: undefined,
142
+ codeDiffSource,
143
+ requestedAt: undefined,
144
+ startedAt: undefined,
145
+ completedAt: now(),
146
+ callbackStatus: "not_sent",
147
+ callbackError: undefined
148
+ }, now(), true);
149
+ await saveIndex(deps.fs, context.taskRepoRoot, index);
150
+ return {
151
+ status: "failed_to_start",
152
+ gate,
153
+ record: index.gates[gate],
154
+ message
155
+ };
156
+ }
157
+ }
87
158
  const coreInput = await readCoreInputArtifact(deps.fs, context.taskRepoRoot, gate);
88
159
  if (coreInput && coreInput.status !== "ready") {
89
160
  index = applyGateState(index, gate, {
@@ -108,7 +179,39 @@ export function createGateReviewService(deps) {
108
179
  message: `${coreInput.path} is ${coreInput.status}.`
109
180
  };
110
181
  }
111
- const inputHash = await computeInputHash(deps, context.taskRepoRoot, gate);
182
+ const codeDiffInput = gate === "code-diff"
183
+ ? await resolveCodeDiffInput(deps, context, record)
184
+ : undefined;
185
+ if (gate === "code-diff" && !codeDiffInput) {
186
+ index = applyGateState(index, gate, {
187
+ status: "not_required",
188
+ decision: undefined,
189
+ error: undefined,
190
+ exceptionReason: undefined,
191
+ requestId: undefined,
192
+ requestPath: undefined,
193
+ inputHash: undefined,
194
+ baseCommit: undefined,
195
+ headCommit: undefined,
196
+ commits: undefined,
197
+ changedFiles: undefined,
198
+ diffStat: undefined,
199
+ codeDiffSource,
200
+ requestedAt: undefined,
201
+ startedAt: undefined,
202
+ completedAt: undefined,
203
+ callbackStatus: "not_sent",
204
+ callbackError: undefined
205
+ }, now(), true);
206
+ await saveIndex(deps.fs, context.taskRepoRoot, index);
207
+ return {
208
+ status: "not_required",
209
+ gate,
210
+ record: index.gates[gate],
211
+ message: "No new commits to review."
212
+ };
213
+ }
214
+ const inputHash = await computeInputHash(deps, context.taskRepoRoot, gate, codeDiffInput, codeDiffSource);
112
215
  if (!options.force
113
216
  && record.status === "completed"
114
217
  && record.decision === "approve"
@@ -134,6 +237,12 @@ export function createGateReviewService(deps) {
134
237
  requestPath,
135
238
  promptPath,
136
239
  inputHash,
240
+ baseCommit: codeDiffInput?.baseCommit,
241
+ headCommit: codeDiffInput?.headCommit,
242
+ commits: codeDiffInput?.commits,
243
+ changedFiles: codeDiffInput?.changedFiles,
244
+ diffStat: codeDiffInput?.diffStat,
245
+ codeDiffSource,
137
246
  requestedAt: timestamp,
138
247
  startedAt: undefined,
139
248
  completedAt: undefined,
@@ -157,11 +266,13 @@ export function createGateReviewService(deps) {
157
266
  status: "requested",
158
267
  requestedAt: timestamp,
159
268
  inputHash,
269
+ codeDiffSource,
270
+ codeDiff: codeDiffInput,
160
271
  reportPath: nextRecord.reportPath,
161
272
  promptPath: nextRecord.promptPath
162
273
  });
163
274
  await saveIndex(deps.fs, context.taskRepoRoot, index);
164
- void runGateReview(context, gate, requestId).catch(() => {
275
+ void runGateReview(context, gate, requestId, codeDiffInput, codeDiffSource).catch(() => {
165
276
  // runGateReview records failures in the persisted gate state.
166
277
  });
167
278
  return {
@@ -171,7 +282,7 @@ export function createGateReviewService(deps) {
171
282
  message: "Gate review started."
172
283
  };
173
284
  }
174
- async function runGateReview(context, gate, requestId) {
285
+ async function runGateReview(context, gate, requestId, codeDiffInput, codeDiffSource) {
175
286
  const runKey = `${context.taskRepoRoot}:${context.taskSlug}:${gate}`;
176
287
  if (activeRuns.has(runKey)) {
177
288
  return;
@@ -188,7 +299,7 @@ export function createGateReviewService(deps) {
188
299
  await updateRequestStatus(deps.fs, context, requestId, "running", { startedAt: timestamp });
189
300
  const reviewDir = resolveRepoPath(context.taskRepoRoot, GATE_REVIEW_DIR);
190
301
  const agentPath = resolveRepoPath(context.repoRoot, GATE_REVIEW_AGENT_PATH);
191
- const prompt = buildGatePrompt(context, gate, requestId);
302
+ const prompt = buildGatePrompt(context, gate, requestId, codeDiffInput, codeDiffSource);
192
303
  await deps.fs.ensureDir(reviewDir);
193
304
  await deps.fs.ensureDir(resolveRepoPath(context.taskRepoRoot, REQUESTS_DIR));
194
305
  await deps.fs.writeText(resolveRepoPath(context.taskRepoRoot, promptPathForRequest(requestId)), prompt);
@@ -374,8 +485,10 @@ export function createGateReviewService(deps) {
374
485
  });
375
486
  return loadIndex(deps.fs, nextContext, now());
376
487
  },
377
- requestReviewGate(repoRoot, taskSlug, gate) {
378
- return requestReviewGateInternal(repoRoot, taskSlug, gate);
488
+ requestReviewGate(repoRoot, taskSlug, gate, input) {
489
+ return requestReviewGateInternal(repoRoot, taskSlug, gate, {
490
+ codeDiffSource: input?.codeDiffSource
491
+ });
379
492
  },
380
493
  retryReviewGate(repoRoot, taskSlug, gate) {
381
494
  return requestReviewGateInternal(repoRoot, taskSlug, gate, { force: true });
@@ -476,6 +589,12 @@ function normalizeIndex(raw, config, timestamp) {
476
589
  requestId: typeof existing?.requestId === "string" ? existing.requestId : undefined,
477
590
  requestPath: typeof existing?.requestPath === "string" ? existing.requestPath : undefined,
478
591
  inputHash: typeof existing?.inputHash === "string" ? existing.inputHash : undefined,
592
+ baseCommit: typeof existing?.baseCommit === "string" ? existing.baseCommit : undefined,
593
+ headCommit: typeof existing?.headCommit === "string" ? existing.headCommit : undefined,
594
+ commits: Array.isArray(existing?.commits) ? existing.commits.filter(isString) : undefined,
595
+ changedFiles: Array.isArray(existing?.changedFiles) ? existing.changedFiles.filter(isString) : undefined,
596
+ diffStat: typeof existing?.diffStat === "string" ? existing.diffStat : undefined,
597
+ codeDiffSource: isCodeDiffSource(existing?.codeDiffSource) ? existing.codeDiffSource : undefined,
479
598
  summary: typeof existing?.summary === "string" ? existing.summary : undefined,
480
599
  findings: Array.isArray(existing?.findings) ? existing.findings.filter(isFinding) : undefined,
481
600
  error: typeof existing?.error === "string" ? existing.error : undefined,
@@ -521,7 +640,130 @@ function applyGateState(index, gate, patch, timestamp, clearActiveGate = false)
521
640
  async function saveIndex(fs, taskRepoRoot, index) {
522
641
  await fs.writeJsonAtomic(getIndexPath(taskRepoRoot), index);
523
642
  }
524
- async function computeInputHash(deps, taskRepoRoot, gate) {
643
+ async function getDirtyCodeDiffStatus(runner, taskRepoRoot) {
644
+ const status = splitLines(await commandStdout(runner, taskRepoRoot, ["status", "--porcelain=v1"]));
645
+ if (status.length === 0) {
646
+ return undefined;
647
+ }
648
+ const visible = status.slice(0, 8).join("; ");
649
+ return status.length > 8
650
+ ? `${visible}; ... ${status.length - 8} more`
651
+ : visible;
652
+ }
653
+ async function resolveCodeDiffInput(deps, context, record) {
654
+ const headCommit = (await commandStdout(deps.runner, context.taskRepoRoot, ["rev-parse", "HEAD"])).trim();
655
+ if (!headCommit) {
656
+ throw new VcmError({
657
+ code: "GATE_REVIEW_CODE_DIFF_HEAD_MISSING",
658
+ message: "Unable to resolve current task worktree HEAD for code-diff gate.",
659
+ statusCode: 409
660
+ });
661
+ }
662
+ const baseCommit = await resolveCodeDiffBaseCommit(deps, context, record, headCommit);
663
+ if (!baseCommit || baseCommit === headCommit) {
664
+ return undefined;
665
+ }
666
+ const range = `${baseCommit}..${headCommit}`;
667
+ const commits = splitLines(await commandStdout(deps.runner, context.taskRepoRoot, [
668
+ "log",
669
+ "--oneline",
670
+ "--reverse",
671
+ range
672
+ ]));
673
+ if (commits.length === 0) {
674
+ return undefined;
675
+ }
676
+ const changedFiles = splitLines(await commandStdout(deps.runner, context.taskRepoRoot, [
677
+ "diff",
678
+ "--name-only",
679
+ "--find-renames",
680
+ range
681
+ ]));
682
+ const diffStat = await commandStdout(deps.runner, context.taskRepoRoot, [
683
+ "diff",
684
+ "--stat",
685
+ "--find-renames",
686
+ range
687
+ ]);
688
+ const diff = await commandStdout(deps.runner, context.taskRepoRoot, [
689
+ "diff",
690
+ "--binary",
691
+ "--find-renames",
692
+ range
693
+ ]);
694
+ const diffHash = createHash("sha256").update(diff).digest("hex");
695
+ return {
696
+ baseCommit,
697
+ headCommit,
698
+ commits,
699
+ changedFiles,
700
+ diffStat,
701
+ diffHash
702
+ };
703
+ }
704
+ async function resolveCodeDiffBaseCommit(deps, context, record, headCommit) {
705
+ if (record.gate === "code-diff"
706
+ && record.status === "completed"
707
+ && record.decision === "request_changes"
708
+ && record.baseCommit
709
+ && await isAncestor(deps.runner, context.taskRepoRoot, record.baseCommit, headCommit)) {
710
+ return record.baseCommit;
711
+ }
712
+ if (record.gate === "code-diff"
713
+ && record.status === "completed"
714
+ && record.decision === "approve"
715
+ && record.headCommit
716
+ && await isAncestor(deps.runner, context.taskRepoRoot, record.headCommit, headCommit)) {
717
+ return record.headCommit;
718
+ }
719
+ const rootHead = (await commandStdout(deps.runner, context.repoRoot, ["rev-parse", "HEAD"])).trim();
720
+ if (rootHead
721
+ && rootHead !== headCommit
722
+ && await isAncestor(deps.runner, context.taskRepoRoot, rootHead, headCommit)) {
723
+ return rootHead;
724
+ }
725
+ const rootBranch = (await commandStdout(deps.runner, context.repoRoot, [
726
+ "rev-parse",
727
+ "--abbrev-ref",
728
+ "HEAD"
729
+ ])).trim();
730
+ if (rootBranch && rootBranch !== "HEAD") {
731
+ const mergeBase = (await commandStdout(deps.runner, context.taskRepoRoot, [
732
+ "merge-base",
733
+ "HEAD",
734
+ rootBranch
735
+ ])).trim();
736
+ if (mergeBase
737
+ && mergeBase !== headCommit
738
+ && await isAncestor(deps.runner, context.taskRepoRoot, mergeBase, headCommit)) {
739
+ return mergeBase;
740
+ }
741
+ }
742
+ const upstream = (await commandStdout(deps.runner, context.taskRepoRoot, [
743
+ "rev-parse",
744
+ "--abbrev-ref",
745
+ "--symbolic-full-name",
746
+ "@{upstream}"
747
+ ])).trim();
748
+ if (upstream) {
749
+ const mergeBase = (await commandStdout(deps.runner, context.taskRepoRoot, [
750
+ "merge-base",
751
+ "HEAD",
752
+ upstream
753
+ ])).trim();
754
+ if (mergeBase
755
+ && mergeBase !== headCommit
756
+ && await isAncestor(deps.runner, context.taskRepoRoot, mergeBase, headCommit)) {
757
+ return mergeBase;
758
+ }
759
+ }
760
+ return headCommit;
761
+ }
762
+ async function isAncestor(runner, cwd, ancestor, descendant) {
763
+ const result = await runner.run("git", ["merge-base", "--is-ancestor", ancestor, descendant], { cwd });
764
+ return result.exitCode === 0;
765
+ }
766
+ async function computeInputHash(deps, taskRepoRoot, gate, codeDiffInput, codeDiffSource) {
525
767
  const digest = createHash("sha256");
526
768
  const coreArtifact = CORE_INPUT_ARTIFACTS[gate];
527
769
  if (coreArtifact) {
@@ -533,9 +775,11 @@ async function computeInputHash(deps, taskRepoRoot, gate) {
533
775
  "CLAUDE.md",
534
776
  ".claude/agents/gate-reviewer.md",
535
777
  ".claude/skills/vcm-gate-review/SKILL.md",
536
- ".ai/tools/request-gate-review"
778
+ ".ai/tools/request-gate-review",
779
+ "docs/CODING_STANDARDS.md"
537
780
  ];
538
- for (const relativePath of [...common, ...SOURCE_ARTIFACTS[gate]]) {
781
+ const sourceArtifacts = getSourceArtifacts(gate, codeDiffSource);
782
+ for (const relativePath of [...common, ...sourceArtifacts]) {
539
783
  digest.update(relativePath);
540
784
  const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
541
785
  if (await deps.fs.pathExists(absolutePath)) {
@@ -545,7 +789,21 @@ async function computeInputHash(deps, taskRepoRoot, gate) {
545
789
  digest.update("<missing>");
546
790
  }
547
791
  }
548
- if (gate === "architecture-plan" || gate === "final-diff") {
792
+ if (gate === "code-diff" && codeDiffInput) {
793
+ digest.update("codeDiffSource");
794
+ digest.update(codeDiffSource ?? "<missing>");
795
+ digest.update("baseCommit");
796
+ digest.update(codeDiffInput.baseCommit);
797
+ digest.update("headCommit");
798
+ digest.update(codeDiffInput.headCommit);
799
+ digest.update("commits");
800
+ digest.update(codeDiffInput.commits.join("\n"));
801
+ digest.update("changedFiles");
802
+ digest.update(codeDiffInput.changedFiles.join("\n"));
803
+ digest.update("diffHash");
804
+ digest.update(codeDiffInput.diffHash);
805
+ }
806
+ if (gate === "architecture-plan") {
549
807
  digest.update(await commandStdout(deps.runner, taskRepoRoot, ["status", "--porcelain=v1"]));
550
808
  digest.update(await commandStdout(deps.runner, taskRepoRoot, ["diff", "--binary"]));
551
809
  digest.update(await commandStdout(deps.runner, taskRepoRoot, ["diff", "--cached", "--binary"]));
@@ -571,15 +829,39 @@ async function commandStdout(runner, cwd, args) {
571
829
  const result = await runner.run("git", args, { cwd });
572
830
  return result.exitCode === 0 ? result.stdout : "";
573
831
  }
574
- function buildGatePrompt(context, gate, requestId) {
832
+ function splitLines(value) {
833
+ return value
834
+ .split(/\r?\n/)
835
+ .map((line) => line.trim())
836
+ .filter(Boolean);
837
+ }
838
+ function buildGatePrompt(context, gate, requestId, codeDiffInput, codeDiffSource) {
575
839
  const reportPath = reportPathForGate(gate);
576
840
  const absoluteReportPath = resolveRepoPath(context.taskRepoRoot, reportPath);
577
- const evidence = SOURCE_ARTIFACTS[gate]
841
+ const evidence = getSourceArtifacts(gate, codeDiffSource)
578
842
  .map((relativePath) => `- ${relativePath}`)
579
843
  .join("\n");
580
- const gitLine = gate === "architecture-plan" || gate === "final-diff"
844
+ const gitLine = gate === "architecture-plan"
581
845
  ? "\nDiff: inspect git status/diff in Worktree."
582
846
  : "";
847
+ const codeDiffSection = gate === "code-diff" && codeDiffInput
848
+ ? `
849
+
850
+ Code Diff Input:
851
+ This code-diff gate reviews the new commits from one PM route flow, not the whole task and not one terminal turn.
852
+ Code source: ${codeDiffSource}
853
+ Base commit: ${codeDiffInput.baseCommit}
854
+ Head commit: ${codeDiffInput.headCommit}
855
+ Commits:
856
+ ${codeDiffInput.commits.map((line) => `- ${line}`).join("\n")}
857
+ Changed files:
858
+ ${codeDiffInput.changedFiles.length > 0 ? codeDiffInput.changedFiles.map((line) => `- ${line}`).join("\n") : "- <none>"}
859
+ Diff commands:
860
+ - git diff --stat --find-renames ${codeDiffInput.baseCommit}..${codeDiffInput.headCommit}
861
+ - git diff --find-renames ${codeDiffInput.baseCommit}..${codeDiffInput.headCommit}
862
+ - git show --stat --oneline <commit>
863
+ Review only this commit range.`
864
+ : "";
583
865
  return `[VCM GATE REVIEW]
584
866
  Task: ${context.taskSlug}
585
867
  Worktree: ${context.taskRepoRoot}
@@ -588,7 +870,7 @@ Request: ${requestId}
588
870
  Report: ${absoluteReportPath}
589
871
 
590
872
  Evidence:
591
- ${evidence}${gitLine}
873
+ ${evidence}${gitLine}${codeDiffSection}
592
874
 
593
875
  Write only Report. Start exactly:
594
876
  Gate: ${gate}
@@ -707,7 +989,7 @@ async function readJsonOrNull(fs, targetPath) {
707
989
  }
708
990
  }
709
991
  function matchField(content, field) {
710
- const match = content.match(new RegExp(`^\\s*${escapeRegex(field)}\\s*:\\s*(.+?)\\s*$`, "mi"));
992
+ const match = content.match(new RegExp(`^\\s*(?:[-*]\\s*)?${escapeRegex(field)}\\s*:\\s*(.+?)\\s*$`, "mi"));
711
993
  return match?.[1]?.trim();
712
994
  }
713
995
  function extractSummary(content) {
@@ -722,8 +1004,9 @@ function extractFindings(content) {
722
1004
  const findings = [];
723
1005
  const blocks = content.split(/\n(?=#{2,4}\s+|-+\s*severity\s*:|severity\s*:)/i);
724
1006
  for (const block of blocks) {
725
- const severity = normalizeSeverity(matchField(block, "severity"));
726
- const title = matchField(block, "title") ?? block.match(/^#{2,4}\s+(.+)$/m)?.[1]?.trim();
1007
+ const heading = block.match(/^#{2,4}\s+(critical|high|medium|low)\s*:\s*(.+)$/im);
1008
+ const severity = normalizeSeverity(matchField(block, "severity") ?? heading?.[1]);
1009
+ const title = matchField(block, "title") ?? heading?.[2]?.trim() ?? block.match(/^#{2,4}\s+(.+)$/m)?.[1]?.trim();
727
1010
  if (!severity || !title) {
728
1011
  continue;
729
1012
  }
@@ -740,6 +1023,15 @@ function extractFindings(content) {
740
1023
  }
741
1024
  return findings;
742
1025
  }
1026
+ function getSourceArtifacts(gate, codeDiffSource) {
1027
+ if (gate !== "code-diff") {
1028
+ return SOURCE_ARTIFACTS[gate];
1029
+ }
1030
+ return codeDiffSource ? CODE_DIFF_SOURCE_ARTIFACTS[codeDiffSource] : [];
1031
+ }
1032
+ export function isCodeDiffSource(value) {
1033
+ return typeof value === "string" && CODE_DIFF_SOURCES.includes(value);
1034
+ }
743
1035
  function normalizeGateStatus(value) {
744
1036
  return typeof value === "string" && [
745
1037
  "disabled",
@@ -775,6 +1067,9 @@ function isFinding(value) {
775
1067
  const candidate = value;
776
1068
  return Boolean(normalizeSeverity(candidate.severity) && candidate.title);
777
1069
  }
1070
+ function isString(value) {
1071
+ return typeof value === "string";
1072
+ }
778
1073
  function parsePositiveInteger(value) {
779
1074
  if (!value) {
780
1075
  return undefined;
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import path from "node:path";
3
- import { checkMarkdownArtifact } from "../../shared/validation/artifact-check.js";
3
+ import { checkMarkdownArtifact, readArtifactSectionValue } from "../../shared/validation/artifact-check.js";
4
4
  import { resolveRepoPath, toRepoRelativePath } from "../adapters/filesystem.js";
5
5
  import { VcmError } from "../errors.js";
6
6
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
@@ -47,10 +47,15 @@ export function createHarnessFeedbackService(deps) {
47
47
  const finalAcceptanceAbsolutePath = resolveRepoPath(input.taskRepoRoot, finalAcceptancePath);
48
48
  const finalAcceptanceContent = await readAbsoluteOptionalText(finalAcceptanceAbsolutePath);
49
49
  const finalAcceptanceCheck = checkMarkdownArtifact("final-acceptance", finalAcceptancePath, finalAcceptanceContent ?? null);
50
- if (finalAcceptanceCheck.status !== "ok" || !finalAcceptanceContent) {
50
+ const finalAcceptanceDecision = finalAcceptanceContent
51
+ ? readArtifactSectionValue(finalAcceptanceContent, "Decision")?.toLowerCase()
52
+ : undefined;
53
+ if (finalAcceptanceCheck.status !== "ok"
54
+ || !finalAcceptanceContent
55
+ || (finalAcceptanceDecision !== "accepted" && finalAcceptanceDecision !== "accepted-with-known-risks")) {
51
56
  throw new VcmError({
52
57
  code: "TASK_FINAL_ACCEPTANCE_NOT_READY",
53
- message: "Task final acceptance is not complete yet.",
58
+ message: "Task Harness Retrospective requires a completed code-change flow.",
54
59
  statusCode: 409,
55
60
  hint: `${finalAcceptancePath} must pass the final-acceptance artifact check before Task Harness Retrospective can start.`
56
61
  });