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