vibe-coding-master 0.6.20 → 0.6.22

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 +322 -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 +110 -49
  17. package/dist/backend/templates/harness/claude-root.js +45 -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 +237 -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 +103 -34
  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 +22 -17
  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 +6 -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,25 @@ 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"
33
+ ],
34
+ "architect-diagnosis": [
35
+ ".ai/vcm/handoffs/architecture-diagnosis.md"
29
36
  ]
30
37
  };
31
38
  const CORE_INPUT_ARTIFACTS = {
32
39
  "architecture-plan": ".ai/vcm/handoffs/architecture-plan.md",
33
- "validation-adequacy": ".ai/vcm/handoffs/review-report.md"
40
+ "validation-adequacy": ".ai/vcm/handoffs/test-report.md"
34
41
  };
35
42
  const VALID_SEVERITIES = new Set(["critical", "high", "medium", "low"]);
36
43
  export function createGateReviewService(deps) {
@@ -54,6 +61,11 @@ export function createGateReviewService(deps) {
54
61
  const context = await getContext(repoRoot, taskSlug);
55
62
  let index = await loadIndex(deps.fs, context, now());
56
63
  const record = index.gates[gate];
64
+ const requestedCodeDiffSource = options.codeDiffSource
65
+ ?? (options.force ? record.codeDiffSource : undefined);
66
+ const codeDiffSource = gate === "code-diff" && isCodeDiffSource(requestedCodeDiffSource)
67
+ ? requestedCodeDiffSource
68
+ : undefined;
57
69
  if (!context.config.enabled) {
58
70
  index = applyGateState(index, gate, {
59
71
  status: "disabled",
@@ -84,6 +96,68 @@ export function createGateReviewService(deps) {
84
96
  if (record.status === "running" && !options.force) {
85
97
  return { status: "running", gate, record, message: "Gate review is already running." };
86
98
  }
99
+ if (gate === "code-diff" && !codeDiffSource) {
100
+ const message = "code-diff requires --source coder, --source architect-debug, or --source architect-diagnosis.";
101
+ index = applyGateState(index, gate, {
102
+ status: "failed",
103
+ decision: undefined,
104
+ error: message,
105
+ codeDiffSource: undefined,
106
+ requestId: undefined,
107
+ requestPath: undefined,
108
+ inputHash: undefined,
109
+ baseCommit: undefined,
110
+ headCommit: undefined,
111
+ commits: undefined,
112
+ changedFiles: undefined,
113
+ diffStat: undefined,
114
+ requestedAt: undefined,
115
+ startedAt: undefined,
116
+ completedAt: now(),
117
+ callbackStatus: "not_sent",
118
+ callbackError: undefined
119
+ }, now(), true);
120
+ await saveIndex(deps.fs, context.taskRepoRoot, index);
121
+ return {
122
+ status: "failed_to_start",
123
+ gate,
124
+ record: index.gates[gate],
125
+ message
126
+ };
127
+ }
128
+ if (gate === "code-diff") {
129
+ const dirtyStatus = await getDirtyCodeDiffStatus(deps.runner, context.taskRepoRoot);
130
+ if (dirtyStatus) {
131
+ const message = `code-diff requires committed inputs; commit or clean these changes first: ${dirtyStatus}`;
132
+ index = applyGateState(index, gate, {
133
+ status: "failed",
134
+ decision: undefined,
135
+ error: message,
136
+ exceptionReason: undefined,
137
+ requestId: undefined,
138
+ requestPath: undefined,
139
+ inputHash: undefined,
140
+ baseCommit: undefined,
141
+ headCommit: undefined,
142
+ commits: undefined,
143
+ changedFiles: undefined,
144
+ diffStat: undefined,
145
+ codeDiffSource,
146
+ requestedAt: undefined,
147
+ startedAt: undefined,
148
+ completedAt: now(),
149
+ callbackStatus: "not_sent",
150
+ callbackError: undefined
151
+ }, now(), true);
152
+ await saveIndex(deps.fs, context.taskRepoRoot, index);
153
+ return {
154
+ status: "failed_to_start",
155
+ gate,
156
+ record: index.gates[gate],
157
+ message
158
+ };
159
+ }
160
+ }
87
161
  const coreInput = await readCoreInputArtifact(deps.fs, context.taskRepoRoot, gate);
88
162
  if (coreInput && coreInput.status !== "ready") {
89
163
  index = applyGateState(index, gate, {
@@ -108,7 +182,39 @@ export function createGateReviewService(deps) {
108
182
  message: `${coreInput.path} is ${coreInput.status}.`
109
183
  };
110
184
  }
111
- const inputHash = await computeInputHash(deps, context.taskRepoRoot, gate);
185
+ const codeDiffInput = gate === "code-diff"
186
+ ? await resolveCodeDiffInput(deps, context, record)
187
+ : undefined;
188
+ if (gate === "code-diff" && !codeDiffInput) {
189
+ index = applyGateState(index, gate, {
190
+ status: "not_required",
191
+ decision: undefined,
192
+ error: undefined,
193
+ exceptionReason: undefined,
194
+ requestId: undefined,
195
+ requestPath: undefined,
196
+ inputHash: undefined,
197
+ baseCommit: undefined,
198
+ headCommit: undefined,
199
+ commits: undefined,
200
+ changedFiles: undefined,
201
+ diffStat: undefined,
202
+ codeDiffSource,
203
+ requestedAt: undefined,
204
+ startedAt: undefined,
205
+ completedAt: undefined,
206
+ callbackStatus: "not_sent",
207
+ callbackError: undefined
208
+ }, now(), true);
209
+ await saveIndex(deps.fs, context.taskRepoRoot, index);
210
+ return {
211
+ status: "not_required",
212
+ gate,
213
+ record: index.gates[gate],
214
+ message: "No new commits to review."
215
+ };
216
+ }
217
+ const inputHash = await computeInputHash(deps, context.taskRepoRoot, gate, codeDiffInput, codeDiffSource);
112
218
  if (!options.force
113
219
  && record.status === "completed"
114
220
  && record.decision === "approve"
@@ -134,6 +240,12 @@ export function createGateReviewService(deps) {
134
240
  requestPath,
135
241
  promptPath,
136
242
  inputHash,
243
+ baseCommit: codeDiffInput?.baseCommit,
244
+ headCommit: codeDiffInput?.headCommit,
245
+ commits: codeDiffInput?.commits,
246
+ changedFiles: codeDiffInput?.changedFiles,
247
+ diffStat: codeDiffInput?.diffStat,
248
+ codeDiffSource,
137
249
  requestedAt: timestamp,
138
250
  startedAt: undefined,
139
251
  completedAt: undefined,
@@ -157,11 +269,13 @@ export function createGateReviewService(deps) {
157
269
  status: "requested",
158
270
  requestedAt: timestamp,
159
271
  inputHash,
272
+ codeDiffSource,
273
+ codeDiff: codeDiffInput,
160
274
  reportPath: nextRecord.reportPath,
161
275
  promptPath: nextRecord.promptPath
162
276
  });
163
277
  await saveIndex(deps.fs, context.taskRepoRoot, index);
164
- void runGateReview(context, gate, requestId).catch(() => {
278
+ void runGateReview(context, gate, requestId, codeDiffInput, codeDiffSource).catch(() => {
165
279
  // runGateReview records failures in the persisted gate state.
166
280
  });
167
281
  return {
@@ -171,7 +285,7 @@ export function createGateReviewService(deps) {
171
285
  message: "Gate review started."
172
286
  };
173
287
  }
174
- async function runGateReview(context, gate, requestId) {
288
+ async function runGateReview(context, gate, requestId, codeDiffInput, codeDiffSource) {
175
289
  const runKey = `${context.taskRepoRoot}:${context.taskSlug}:${gate}`;
176
290
  if (activeRuns.has(runKey)) {
177
291
  return;
@@ -188,7 +302,7 @@ export function createGateReviewService(deps) {
188
302
  await updateRequestStatus(deps.fs, context, requestId, "running", { startedAt: timestamp });
189
303
  const reviewDir = resolveRepoPath(context.taskRepoRoot, GATE_REVIEW_DIR);
190
304
  const agentPath = resolveRepoPath(context.repoRoot, GATE_REVIEW_AGENT_PATH);
191
- const prompt = buildGatePrompt(context, gate, requestId);
305
+ const prompt = buildGatePrompt(context, gate, requestId, codeDiffInput, codeDiffSource);
192
306
  await deps.fs.ensureDir(reviewDir);
193
307
  await deps.fs.ensureDir(resolveRepoPath(context.taskRepoRoot, REQUESTS_DIR));
194
308
  await deps.fs.writeText(resolveRepoPath(context.taskRepoRoot, promptPathForRequest(requestId)), prompt);
@@ -374,8 +488,10 @@ export function createGateReviewService(deps) {
374
488
  });
375
489
  return loadIndex(deps.fs, nextContext, now());
376
490
  },
377
- requestReviewGate(repoRoot, taskSlug, gate) {
378
- return requestReviewGateInternal(repoRoot, taskSlug, gate);
491
+ requestReviewGate(repoRoot, taskSlug, gate, input) {
492
+ return requestReviewGateInternal(repoRoot, taskSlug, gate, {
493
+ codeDiffSource: input?.codeDiffSource
494
+ });
379
495
  },
380
496
  retryReviewGate(repoRoot, taskSlug, gate) {
381
497
  return requestReviewGateInternal(repoRoot, taskSlug, gate, { force: true });
@@ -476,6 +592,12 @@ function normalizeIndex(raw, config, timestamp) {
476
592
  requestId: typeof existing?.requestId === "string" ? existing.requestId : undefined,
477
593
  requestPath: typeof existing?.requestPath === "string" ? existing.requestPath : undefined,
478
594
  inputHash: typeof existing?.inputHash === "string" ? existing.inputHash : undefined,
595
+ baseCommit: typeof existing?.baseCommit === "string" ? existing.baseCommit : undefined,
596
+ headCommit: typeof existing?.headCommit === "string" ? existing.headCommit : undefined,
597
+ commits: Array.isArray(existing?.commits) ? existing.commits.filter(isString) : undefined,
598
+ changedFiles: Array.isArray(existing?.changedFiles) ? existing.changedFiles.filter(isString) : undefined,
599
+ diffStat: typeof existing?.diffStat === "string" ? existing.diffStat : undefined,
600
+ codeDiffSource: isCodeDiffSource(existing?.codeDiffSource) ? existing.codeDiffSource : undefined,
479
601
  summary: typeof existing?.summary === "string" ? existing.summary : undefined,
480
602
  findings: Array.isArray(existing?.findings) ? existing.findings.filter(isFinding) : undefined,
481
603
  error: typeof existing?.error === "string" ? existing.error : undefined,
@@ -521,7 +643,130 @@ function applyGateState(index, gate, patch, timestamp, clearActiveGate = false)
521
643
  async function saveIndex(fs, taskRepoRoot, index) {
522
644
  await fs.writeJsonAtomic(getIndexPath(taskRepoRoot), index);
523
645
  }
524
- async function computeInputHash(deps, taskRepoRoot, gate) {
646
+ async function getDirtyCodeDiffStatus(runner, taskRepoRoot) {
647
+ const status = splitLines(await commandStdout(runner, taskRepoRoot, ["status", "--porcelain=v1"]));
648
+ if (status.length === 0) {
649
+ return undefined;
650
+ }
651
+ const visible = status.slice(0, 8).join("; ");
652
+ return status.length > 8
653
+ ? `${visible}; ... ${status.length - 8} more`
654
+ : visible;
655
+ }
656
+ async function resolveCodeDiffInput(deps, context, record) {
657
+ const headCommit = (await commandStdout(deps.runner, context.taskRepoRoot, ["rev-parse", "HEAD"])).trim();
658
+ if (!headCommit) {
659
+ throw new VcmError({
660
+ code: "GATE_REVIEW_CODE_DIFF_HEAD_MISSING",
661
+ message: "Unable to resolve current task worktree HEAD for code-diff gate.",
662
+ statusCode: 409
663
+ });
664
+ }
665
+ const baseCommit = await resolveCodeDiffBaseCommit(deps, context, record, headCommit);
666
+ if (!baseCommit || baseCommit === headCommit) {
667
+ return undefined;
668
+ }
669
+ const range = `${baseCommit}..${headCommit}`;
670
+ const commits = splitLines(await commandStdout(deps.runner, context.taskRepoRoot, [
671
+ "log",
672
+ "--oneline",
673
+ "--reverse",
674
+ range
675
+ ]));
676
+ if (commits.length === 0) {
677
+ return undefined;
678
+ }
679
+ const changedFiles = splitLines(await commandStdout(deps.runner, context.taskRepoRoot, [
680
+ "diff",
681
+ "--name-only",
682
+ "--find-renames",
683
+ range
684
+ ]));
685
+ const diffStat = await commandStdout(deps.runner, context.taskRepoRoot, [
686
+ "diff",
687
+ "--stat",
688
+ "--find-renames",
689
+ range
690
+ ]);
691
+ const diff = await commandStdout(deps.runner, context.taskRepoRoot, [
692
+ "diff",
693
+ "--binary",
694
+ "--find-renames",
695
+ range
696
+ ]);
697
+ const diffHash = createHash("sha256").update(diff).digest("hex");
698
+ return {
699
+ baseCommit,
700
+ headCommit,
701
+ commits,
702
+ changedFiles,
703
+ diffStat,
704
+ diffHash
705
+ };
706
+ }
707
+ async function resolveCodeDiffBaseCommit(deps, context, record, headCommit) {
708
+ if (record.gate === "code-diff"
709
+ && record.status === "completed"
710
+ && record.decision === "request_changes"
711
+ && record.baseCommit
712
+ && await isAncestor(deps.runner, context.taskRepoRoot, record.baseCommit, headCommit)) {
713
+ return record.baseCommit;
714
+ }
715
+ if (record.gate === "code-diff"
716
+ && record.status === "completed"
717
+ && record.decision === "approve"
718
+ && record.headCommit
719
+ && await isAncestor(deps.runner, context.taskRepoRoot, record.headCommit, headCommit)) {
720
+ return record.headCommit;
721
+ }
722
+ const rootHead = (await commandStdout(deps.runner, context.repoRoot, ["rev-parse", "HEAD"])).trim();
723
+ if (rootHead
724
+ && rootHead !== headCommit
725
+ && await isAncestor(deps.runner, context.taskRepoRoot, rootHead, headCommit)) {
726
+ return rootHead;
727
+ }
728
+ const rootBranch = (await commandStdout(deps.runner, context.repoRoot, [
729
+ "rev-parse",
730
+ "--abbrev-ref",
731
+ "HEAD"
732
+ ])).trim();
733
+ if (rootBranch && rootBranch !== "HEAD") {
734
+ const mergeBase = (await commandStdout(deps.runner, context.taskRepoRoot, [
735
+ "merge-base",
736
+ "HEAD",
737
+ rootBranch
738
+ ])).trim();
739
+ if (mergeBase
740
+ && mergeBase !== headCommit
741
+ && await isAncestor(deps.runner, context.taskRepoRoot, mergeBase, headCommit)) {
742
+ return mergeBase;
743
+ }
744
+ }
745
+ const upstream = (await commandStdout(deps.runner, context.taskRepoRoot, [
746
+ "rev-parse",
747
+ "--abbrev-ref",
748
+ "--symbolic-full-name",
749
+ "@{upstream}"
750
+ ])).trim();
751
+ if (upstream) {
752
+ const mergeBase = (await commandStdout(deps.runner, context.taskRepoRoot, [
753
+ "merge-base",
754
+ "HEAD",
755
+ upstream
756
+ ])).trim();
757
+ if (mergeBase
758
+ && mergeBase !== headCommit
759
+ && await isAncestor(deps.runner, context.taskRepoRoot, mergeBase, headCommit)) {
760
+ return mergeBase;
761
+ }
762
+ }
763
+ return headCommit;
764
+ }
765
+ async function isAncestor(runner, cwd, ancestor, descendant) {
766
+ const result = await runner.run("git", ["merge-base", "--is-ancestor", ancestor, descendant], { cwd });
767
+ return result.exitCode === 0;
768
+ }
769
+ async function computeInputHash(deps, taskRepoRoot, gate, codeDiffInput, codeDiffSource) {
525
770
  const digest = createHash("sha256");
526
771
  const coreArtifact = CORE_INPUT_ARTIFACTS[gate];
527
772
  if (coreArtifact) {
@@ -533,9 +778,11 @@ async function computeInputHash(deps, taskRepoRoot, gate) {
533
778
  "CLAUDE.md",
534
779
  ".claude/agents/gate-reviewer.md",
535
780
  ".claude/skills/vcm-gate-review/SKILL.md",
536
- ".ai/tools/request-gate-review"
781
+ ".ai/tools/request-gate-review",
782
+ "docs/CODING_STANDARDS.md"
537
783
  ];
538
- for (const relativePath of [...common, ...SOURCE_ARTIFACTS[gate]]) {
784
+ const sourceArtifacts = getSourceArtifacts(gate, codeDiffSource);
785
+ for (const relativePath of [...common, ...sourceArtifacts]) {
539
786
  digest.update(relativePath);
540
787
  const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
541
788
  if (await deps.fs.pathExists(absolutePath)) {
@@ -545,7 +792,21 @@ async function computeInputHash(deps, taskRepoRoot, gate) {
545
792
  digest.update("<missing>");
546
793
  }
547
794
  }
548
- if (gate === "architecture-plan" || gate === "final-diff") {
795
+ if (gate === "code-diff" && codeDiffInput) {
796
+ digest.update("codeDiffSource");
797
+ digest.update(codeDiffSource ?? "<missing>");
798
+ digest.update("baseCommit");
799
+ digest.update(codeDiffInput.baseCommit);
800
+ digest.update("headCommit");
801
+ digest.update(codeDiffInput.headCommit);
802
+ digest.update("commits");
803
+ digest.update(codeDiffInput.commits.join("\n"));
804
+ digest.update("changedFiles");
805
+ digest.update(codeDiffInput.changedFiles.join("\n"));
806
+ digest.update("diffHash");
807
+ digest.update(codeDiffInput.diffHash);
808
+ }
809
+ if (gate === "architecture-plan") {
549
810
  digest.update(await commandStdout(deps.runner, taskRepoRoot, ["status", "--porcelain=v1"]));
550
811
  digest.update(await commandStdout(deps.runner, taskRepoRoot, ["diff", "--binary"]));
551
812
  digest.update(await commandStdout(deps.runner, taskRepoRoot, ["diff", "--cached", "--binary"]));
@@ -571,15 +832,39 @@ async function commandStdout(runner, cwd, args) {
571
832
  const result = await runner.run("git", args, { cwd });
572
833
  return result.exitCode === 0 ? result.stdout : "";
573
834
  }
574
- function buildGatePrompt(context, gate, requestId) {
835
+ function splitLines(value) {
836
+ return value
837
+ .split(/\r?\n/)
838
+ .map((line) => line.trim())
839
+ .filter(Boolean);
840
+ }
841
+ function buildGatePrompt(context, gate, requestId, codeDiffInput, codeDiffSource) {
575
842
  const reportPath = reportPathForGate(gate);
576
843
  const absoluteReportPath = resolveRepoPath(context.taskRepoRoot, reportPath);
577
- const evidence = SOURCE_ARTIFACTS[gate]
844
+ const evidence = getSourceArtifacts(gate, codeDiffSource)
578
845
  .map((relativePath) => `- ${relativePath}`)
579
846
  .join("\n");
580
- const gitLine = gate === "architecture-plan" || gate === "final-diff"
847
+ const gitLine = gate === "architecture-plan"
581
848
  ? "\nDiff: inspect git status/diff in Worktree."
582
849
  : "";
850
+ const codeDiffSection = gate === "code-diff" && codeDiffInput
851
+ ? `
852
+
853
+ Code Diff Input:
854
+ 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}
856
+ Base commit: ${codeDiffInput.baseCommit}
857
+ Head commit: ${codeDiffInput.headCommit}
858
+ Commits:
859
+ ${codeDiffInput.commits.map((line) => `- ${line}`).join("\n")}
860
+ Changed files:
861
+ ${codeDiffInput.changedFiles.length > 0 ? codeDiffInput.changedFiles.map((line) => `- ${line}`).join("\n") : "- <none>"}
862
+ Diff commands:
863
+ - git diff --stat --find-renames ${codeDiffInput.baseCommit}..${codeDiffInput.headCommit}
864
+ - git diff --find-renames ${codeDiffInput.baseCommit}..${codeDiffInput.headCommit}
865
+ - git show --stat --oneline <commit>
866
+ Review only this commit range.`
867
+ : "";
583
868
  return `[VCM GATE REVIEW]
584
869
  Task: ${context.taskSlug}
585
870
  Worktree: ${context.taskRepoRoot}
@@ -588,7 +873,7 @@ Request: ${requestId}
588
873
  Report: ${absoluteReportPath}
589
874
 
590
875
  Evidence:
591
- ${evidence}${gitLine}
876
+ ${evidence}${gitLine}${codeDiffSection}
592
877
 
593
878
  Write only Report. Start exactly:
594
879
  Gate: ${gate}
@@ -707,7 +992,7 @@ async function readJsonOrNull(fs, targetPath) {
707
992
  }
708
993
  }
709
994
  function matchField(content, field) {
710
- const match = content.match(new RegExp(`^\\s*${escapeRegex(field)}\\s*:\\s*(.+?)\\s*$`, "mi"));
995
+ const match = content.match(new RegExp(`^\\s*(?:[-*]\\s*)?${escapeRegex(field)}\\s*:\\s*(.+?)\\s*$`, "mi"));
711
996
  return match?.[1]?.trim();
712
997
  }
713
998
  function extractSummary(content) {
@@ -722,8 +1007,9 @@ function extractFindings(content) {
722
1007
  const findings = [];
723
1008
  const blocks = content.split(/\n(?=#{2,4}\s+|-+\s*severity\s*:|severity\s*:)/i);
724
1009
  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();
1010
+ const heading = block.match(/^#{2,4}\s+(critical|high|medium|low)\s*:\s*(.+)$/im);
1011
+ const severity = normalizeSeverity(matchField(block, "severity") ?? heading?.[1]);
1012
+ const title = matchField(block, "title") ?? heading?.[2]?.trim() ?? block.match(/^#{2,4}\s+(.+)$/m)?.[1]?.trim();
727
1013
  if (!severity || !title) {
728
1014
  continue;
729
1015
  }
@@ -740,6 +1026,15 @@ function extractFindings(content) {
740
1026
  }
741
1027
  return findings;
742
1028
  }
1029
+ function getSourceArtifacts(gate, codeDiffSource) {
1030
+ if (gate !== "code-diff") {
1031
+ return SOURCE_ARTIFACTS[gate];
1032
+ }
1033
+ return codeDiffSource ? CODE_DIFF_SOURCE_ARTIFACTS[codeDiffSource] : [];
1034
+ }
1035
+ export function isCodeDiffSource(value) {
1036
+ return typeof value === "string" && CODE_DIFF_SOURCES.includes(value);
1037
+ }
743
1038
  function normalizeGateStatus(value) {
744
1039
  return typeof value === "string" && [
745
1040
  "disabled",
@@ -775,6 +1070,9 @@ function isFinding(value) {
775
1070
  const candidate = value;
776
1071
  return Boolean(normalizeSeverity(candidate.severity) && candidate.title);
777
1072
  }
1073
+ function isString(value) {
1074
+ return typeof value === "string";
1075
+ }
778
1076
  function parsePositiveInteger(value) {
779
1077
  if (!value) {
780
1078
  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
  });