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.
- 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 +322 -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 +110 -49
- package/dist/backend/templates/harness/claude-root.js +45 -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 +237 -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 +103 -34
- 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 +22 -17
- 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 +6 -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,25 @@ 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"
|
|
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/
|
|
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
|
|
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
|
|
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
|
-
|
|
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 === "
|
|
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
|
|
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 =
|
|
844
|
+
const evidence = getSourceArtifacts(gate, codeDiffSource)
|
|
578
845
|
.map((relativePath) => `- ${relativePath}`)
|
|
579
846
|
.join("\n");
|
|
580
|
-
const gitLine = gate === "architecture-plan"
|
|
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
|
|
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
|
|
726
|
-
const
|
|
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
|
-
|
|
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
|
});
|