vibe-coding-master 0.6.20 → 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.
- package/README.md +5 -5
- package/dist/backend/api/artifact-routes.js +2 -2
- package/dist/backend/api/gate-review-routes.js +1 -1
- package/dist/backend/api/task-routes.js +1 -1
- package/dist/backend/cli/install-vcm-harness.js +66 -20
- package/dist/backend/services/app-settings-service.js +11 -2
- package/dist/backend/services/artifact-service.js +9 -8
- package/dist/backend/services/gate-review-service.js +319 -24
- package/dist/backend/services/harness-feedback-service.js +8 -3
- package/dist/backend/services/harness-service.js +100 -13
- package/dist/backend/services/job-guard-service.js +2 -2
- package/dist/backend/services/message-service.js +12 -0
- package/dist/backend/services/session-service.js +18 -11
- package/dist/backend/services/status-service.js +1 -4
- package/dist/backend/templates/handoff.js +105 -17
- package/dist/backend/templates/harness/architect-agent.js +51 -27
- package/dist/backend/templates/harness/claude-root.js +43 -15
- package/dist/backend/templates/harness/coder-agent.js +66 -47
- package/dist/backend/templates/harness/coder-worker-agent.js +45 -17
- package/dist/backend/templates/harness/gate-review.js +229 -39
- package/dist/backend/templates/harness/harness-engineer-agent.js +39 -12
- package/dist/backend/templates/harness/project-coding-standards.js +73 -0
- package/dist/backend/templates/harness/project-glossary.js +30 -0
- package/dist/backend/templates/harness/project-known-issues.js +33 -0
- package/dist/backend/templates/harness/project-manager-agent.js +92 -29
- package/dist/backend/templates/harness/pull-request-template.js +6 -4
- package/dist/backend/templates/harness/tester-agent.js +82 -0
- package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +19 -15
- package/dist/backend/templates/harness/vcm-harness-bootstrap-skill.js +19 -2
- package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +1 -1
- package/dist/backend/templates/harness/vcm-report-harness-issue-skill.js +18 -12
- package/dist/backend/templates/harness/vcm-route-message-skill.js +48 -8
- package/dist/backend/templates/message-envelope.js +2 -5
- package/dist/shared/constants.js +3 -3
- package/dist/shared/types/gate-review.js +5 -1
- package/dist/shared/validation/artifact-check.js +64 -10
- package/dist-frontend/assets/{index-C5E5jLp-.js → index-DYBg_qYS.js} +4 -4
- package/dist-frontend/index.html +1 -1
- package/package.json +1 -1
- package/scripts/verify-package.mjs +1 -1
- 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/
|
|
22
|
+
".ai/vcm/handoffs/test-report.md"
|
|
23
23
|
],
|
|
24
|
-
"
|
|
24
|
+
"code-diff": []
|
|
25
|
+
};
|
|
26
|
+
const CODE_DIFF_SOURCE_ARTIFACTS = {
|
|
27
|
+
coder: [
|
|
25
28
|
".ai/vcm/handoffs/architecture-plan.md",
|
|
26
|
-
".ai/vcm/handoffs/
|
|
27
|
-
|
|
28
|
-
|
|
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/
|
|
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
|
|
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
|
|
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
|
-
|
|
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 === "
|
|
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
|
|
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 =
|
|
841
|
+
const evidence = getSourceArtifacts(gate, codeDiffSource)
|
|
578
842
|
.map((relativePath) => `- ${relativePath}`)
|
|
579
843
|
.join("\n");
|
|
580
|
-
const gitLine = gate === "architecture-plan"
|
|
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
|
|
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
|
|
726
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
});
|