vibe-coding-master 0.7.33 → 0.7.35
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 +25 -12
- package/dist/backend/adapters/git-adapter.js +21 -0
- package/dist/backend/api/gate-review-routes.js +5 -0
- package/dist/backend/services/auto-memory-service.js +115 -81
- package/dist/backend/services/ccr-integration-service.js +2 -1
- package/dist/backend/services/claude-hook-service.js +11 -0
- package/dist/backend/services/gate-review-service.js +354 -107
- package/dist/backend/services/harness-feedback-service.js +6 -3
- package/dist/backend/services/session-service.js +42 -52
- package/dist/backend/templates/handoff.js +17 -0
- package/dist/backend/templates/harness/architect-agent.js +5 -2
- package/dist/backend/templates/harness/claude-root.js +8 -0
- package/dist/backend/templates/harness/coder-agent.js +1 -1
- package/dist/backend/templates/harness/gate-review.js +2 -0
- package/dist/backend/templates/harness/harness-engineer-agent.js +14 -9
- package/dist/backend/templates/harness/tester-agent.js +1 -1
- package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +2 -0
- package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +3 -1
- package/dist/backend/templates/harness/vcm-route-message-skill.js +1 -1
- package/dist/shared/types/session.js +1 -0
- package/dist-frontend/assets/index-DN9kwoOt.css +32 -0
- package/dist-frontend/assets/{index-Cmr9W7-n.js → index-DhLRhgSV.js} +35 -35
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/scripts/harness-tools/vcm-bash-guard +62 -8
- package/dist/backend/services/memory-review-validation.js +0 -286
- package/dist-frontend/assets/index-CguKU8Q5.css +0 -32
|
@@ -12,7 +12,8 @@ const REQUESTS_DIR = ".ai/vcm/gate-reviews/requests";
|
|
|
12
12
|
const GATE_REVIEW_VERSION = 1;
|
|
13
13
|
const REVIEWER_ROLE = "reviewer";
|
|
14
14
|
const DEFAULT_REPORT_POLL_INTERVAL_MS = 1000;
|
|
15
|
-
const activeRuns = new
|
|
15
|
+
const activeRuns = new Map();
|
|
16
|
+
const gateStateLocks = new Map();
|
|
16
17
|
const ARCHITECTURE_ANALYSIS_FIELDS = [
|
|
17
18
|
"Evidence Read",
|
|
18
19
|
"Architecture Brief Fit",
|
|
@@ -111,6 +112,9 @@ export function createGateReviewService(deps) {
|
|
|
111
112
|
}
|
|
112
113
|
async function requestReviewGateInternal(repoRoot, taskSlug, gate, options = {}) {
|
|
113
114
|
const context = await getContext(repoRoot, taskSlug);
|
|
115
|
+
return withGateStateLock(context, () => requestReviewGateLocked(context, gate, options));
|
|
116
|
+
}
|
|
117
|
+
async function requestReviewGateLocked(context, gate, options) {
|
|
114
118
|
let index = await loadIndex(deps.fs, context, now());
|
|
115
119
|
const record = index.gates[gate];
|
|
116
120
|
const requestedCodeDiffSource = options.codeDiffSource
|
|
@@ -147,7 +151,7 @@ export function createGateReviewService(deps) {
|
|
|
147
151
|
message: `Gate review is already running for ${index.activeGate}.`
|
|
148
152
|
};
|
|
149
153
|
}
|
|
150
|
-
if (record.status === "running"
|
|
154
|
+
if (record.status === "running") {
|
|
151
155
|
return { status: "running", gate, record, message: "Gate review is already running." };
|
|
152
156
|
}
|
|
153
157
|
if (gate === "code-diff" && !codeDiffSource) {
|
|
@@ -401,6 +405,7 @@ export function createGateReviewService(deps) {
|
|
|
401
405
|
const requestPath = path.posix.join(REQUESTS_DIR, `${requestId}.json`);
|
|
402
406
|
const promptPath = path.posix.join(REQUESTS_DIR, `${requestId}.prompt.md`);
|
|
403
407
|
const requestReportPath = reportPathForRequest(requestId);
|
|
408
|
+
const inputSnapshots = await captureGateInputSnapshots(deps.fs, context.taskRepoRoot, requestId, getSourceArtifacts(gate, codeDiffSources));
|
|
404
409
|
const nextRecord = {
|
|
405
410
|
...record,
|
|
406
411
|
status: "running",
|
|
@@ -444,13 +449,14 @@ export function createGateReviewService(deps) {
|
|
|
444
449
|
codeDiffSource,
|
|
445
450
|
codeDiffSources,
|
|
446
451
|
codeDiff: codeDiffInput,
|
|
452
|
+
inputSnapshots,
|
|
447
453
|
reportPath: requestReportPath,
|
|
448
454
|
latestReportPath: nextRecord.reportPath,
|
|
449
455
|
promptPath: nextRecord.promptPath
|
|
450
456
|
});
|
|
451
457
|
await saveIndex(deps.fs, context.taskRepoRoot, index);
|
|
452
458
|
await notifyArchitecturePlanDisposition(context, gate, false);
|
|
453
|
-
void runGateReview(context, gate, requestId, codeDiffInput, codeDiffSources).catch(() => {
|
|
459
|
+
void runGateReview(context, gate, requestId, codeDiffInput, codeDiffSources, inputSnapshots).catch(() => {
|
|
454
460
|
// runGateReview records failures in the persisted gate state.
|
|
455
461
|
});
|
|
456
462
|
return {
|
|
@@ -460,26 +466,26 @@ export function createGateReviewService(deps) {
|
|
|
460
466
|
message: "Gate review started."
|
|
461
467
|
};
|
|
462
468
|
}
|
|
463
|
-
async function runGateReview(context, gate, requestId, codeDiffInput, codeDiffSources) {
|
|
464
|
-
const runKey =
|
|
469
|
+
async function runGateReview(context, gate, requestId, codeDiffInput, codeDiffSources, inputSnapshots = []) {
|
|
470
|
+
const runKey = gateRunKey(context, gate, requestId);
|
|
465
471
|
if (activeRuns.has(runKey)) {
|
|
466
472
|
return;
|
|
467
473
|
}
|
|
468
|
-
|
|
474
|
+
const controller = new AbortController();
|
|
475
|
+
activeRuns.set(runKey, controller);
|
|
469
476
|
try {
|
|
470
477
|
const timestamp = now();
|
|
471
|
-
await
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
});
|
|
476
|
-
await updateRequestStatus(deps.fs, context, requestId, "running", { startedAt: timestamp });
|
|
478
|
+
if (!(await markGateRunStarted(context, gate, requestId, timestamp))) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
throwIfGateRunCancelled(controller.signal);
|
|
477
482
|
const reviewDir = resolveRepoPath(context.taskRepoRoot, GATE_REVIEW_DIR);
|
|
478
483
|
const agentPath = resolveRepoPath(context.repoRoot, REVIEWER_AGENT_PATH);
|
|
479
|
-
const prompt = buildGatePrompt(context, gate, requestId, codeDiffInput, codeDiffSources);
|
|
484
|
+
const prompt = buildGatePrompt(context, gate, requestId, codeDiffInput, codeDiffSources, inputSnapshots);
|
|
480
485
|
await deps.fs.ensureDir(reviewDir);
|
|
481
486
|
await deps.fs.ensureDir(resolveRepoPath(context.taskRepoRoot, REQUESTS_DIR));
|
|
482
487
|
await deps.fs.writeText(resolveRepoPath(context.taskRepoRoot, promptPathForRequest(requestId)), prompt);
|
|
488
|
+
throwIfGateRunCancelled(controller.signal);
|
|
483
489
|
if (!(await deps.fs.pathExists(agentPath))) {
|
|
484
490
|
throw new VcmError({
|
|
485
491
|
code: "GATE_REVIEW_AGENT_MISSING",
|
|
@@ -489,6 +495,7 @@ export function createGateReviewService(deps) {
|
|
|
489
495
|
});
|
|
490
496
|
}
|
|
491
497
|
const session = await ensureReviewerSession(context);
|
|
498
|
+
throwIfGateRunCancelled(controller.signal);
|
|
492
499
|
await submitTerminalInput(deps.runtime, session.id, prompt);
|
|
493
500
|
await deps.sessionService.markRoleActivityRunning(context.repoRoot, context.taskSlug, REVIEWER_ROLE, session.id);
|
|
494
501
|
await deps.roundService.recordRoleTurnEvent({
|
|
@@ -499,9 +506,63 @@ export function createGateReviewService(deps) {
|
|
|
499
506
|
role: REVIEWER_ROLE,
|
|
500
507
|
eventName: "UserPromptSubmit"
|
|
501
508
|
});
|
|
502
|
-
const parsed = await waitForGateReport(deps.fs, context.taskRepoRoot, gate, requestId, now(), reportPollIntervalMs);
|
|
503
|
-
await
|
|
509
|
+
const parsed = await waitForGateReport(deps.fs, context.taskRepoRoot, gate, requestId, now(), reportPollIntervalMs, controller.signal);
|
|
510
|
+
const completedRecord = await completeGateRun(context, gate, requestId, parsed);
|
|
511
|
+
if (!completedRecord) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
await notifyArchitecturePlanDisposition(context, gate, parsed.decision === "approve");
|
|
515
|
+
await callbackProjectManager(context, gate, "completed", parsed.decision, parsed.reportPath, undefined, requestId, completedRecord.inputHash);
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
if (isGateRunCancelled(error)) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const message = errorMessage(error);
|
|
522
|
+
const failedRecord = await failGateRun(context, gate, requestId, message);
|
|
523
|
+
if (!failedRecord) {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
await notifyArchitecturePlanDisposition(context, gate, false);
|
|
527
|
+
await callbackProjectManager(context, gate, "failed", undefined, reportPathForRequest(requestId), message, requestId, failedRecord.inputHash);
|
|
528
|
+
}
|
|
529
|
+
finally {
|
|
530
|
+
if (activeRuns.get(runKey) === controller) {
|
|
531
|
+
activeRuns.delete(runKey);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
async function markGateRunStarted(context, gate, requestId, timestamp) {
|
|
536
|
+
return withGateStateLock(context, async () => {
|
|
537
|
+
const index = await loadIndex(deps.fs, context, timestamp);
|
|
538
|
+
if (!isCurrentRunningRequest(index, gate, requestId)) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
await updateRequestStatus(deps.fs, context, requestId, "running", { startedAt: timestamp });
|
|
542
|
+
const next = applyGateState(index, gate, {
|
|
543
|
+
status: "running",
|
|
544
|
+
startedAt: timestamp,
|
|
545
|
+
updatedAt: timestamp
|
|
546
|
+
}, timestamp);
|
|
547
|
+
await saveIndex(deps.fs, context.taskRepoRoot, next);
|
|
548
|
+
return true;
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
async function completeGateRun(context, gate, requestId, parsed) {
|
|
552
|
+
return withGateStateLock(context, async () => {
|
|
504
553
|
const completedAt = now();
|
|
554
|
+
const index = await loadIndex(deps.fs, context, completedAt);
|
|
555
|
+
if (!isCurrentRunningRequest(index, gate, requestId)) {
|
|
556
|
+
await updateRequestStatus(deps.fs, context, requestId, "stale_completion", {
|
|
557
|
+
completedAt,
|
|
558
|
+
decision: parsed.decision,
|
|
559
|
+
summary: parsed.summary,
|
|
560
|
+
findings: parsed.findings,
|
|
561
|
+
reportPath: parsed.reportPath
|
|
562
|
+
});
|
|
563
|
+
return undefined;
|
|
564
|
+
}
|
|
565
|
+
await publishLatestGateReport(deps.fs, context.taskRepoRoot, gate, parsed.content);
|
|
505
566
|
await updateRequestStatus(deps.fs, context, requestId, "completed", {
|
|
506
567
|
completedAt,
|
|
507
568
|
decision: parsed.decision,
|
|
@@ -510,8 +571,7 @@ export function createGateReviewService(deps) {
|
|
|
510
571
|
reportPath: parsed.reportPath,
|
|
511
572
|
latestReportPath: reportPathForGate(gate)
|
|
512
573
|
});
|
|
513
|
-
|
|
514
|
-
await updateGateRecord(context, gate, {
|
|
574
|
+
const next = applyGateState(index, gate, {
|
|
515
575
|
status: "completed",
|
|
516
576
|
decision: parsed.decision,
|
|
517
577
|
summary: parsed.summary,
|
|
@@ -521,32 +581,37 @@ export function createGateReviewService(deps) {
|
|
|
521
581
|
callbackStatus: "not_sent",
|
|
522
582
|
callbackError: undefined,
|
|
523
583
|
updatedAt: completedAt
|
|
524
|
-
},
|
|
525
|
-
await
|
|
526
|
-
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
584
|
+
}, completedAt, true);
|
|
585
|
+
await saveIndex(deps.fs, context.taskRepoRoot, next);
|
|
586
|
+
return next.gates[gate];
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
async function failGateRun(context, gate, requestId, message) {
|
|
590
|
+
return withGateStateLock(context, async () => {
|
|
591
|
+
const completedAt = now();
|
|
592
|
+
const index = await loadIndex(deps.fs, context, completedAt);
|
|
593
|
+
if (!isCurrentRunningRequest(index, gate, requestId)) {
|
|
594
|
+
await updateRequestStatus(deps.fs, context, requestId, "stale_completion", {
|
|
595
|
+
completedAt,
|
|
596
|
+
error: message
|
|
597
|
+
});
|
|
598
|
+
return undefined;
|
|
599
|
+
}
|
|
531
600
|
await updateRequestStatus(deps.fs, context, requestId, "failed", {
|
|
532
|
-
completedAt
|
|
601
|
+
completedAt,
|
|
533
602
|
error: message
|
|
534
603
|
});
|
|
535
|
-
|
|
536
|
-
await updateGateRecord(context, gate, {
|
|
604
|
+
const next = applyGateState(index, gate, {
|
|
537
605
|
status: "failed",
|
|
538
606
|
error: message,
|
|
539
|
-
completedAt
|
|
607
|
+
completedAt,
|
|
540
608
|
callbackStatus: "not_sent",
|
|
541
609
|
callbackError: undefined,
|
|
542
|
-
updatedAt:
|
|
543
|
-
},
|
|
544
|
-
await
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
finally {
|
|
548
|
-
activeRuns.delete(runKey);
|
|
549
|
-
}
|
|
610
|
+
updatedAt: completedAt
|
|
611
|
+
}, completedAt, true);
|
|
612
|
+
await saveIndex(deps.fs, context.taskRepoRoot, next);
|
|
613
|
+
return next.gates[gate];
|
|
614
|
+
});
|
|
550
615
|
}
|
|
551
616
|
async function ensureReviewerSession(context) {
|
|
552
617
|
const existing = await deps.sessionService.getRoleSession(context.repoRoot, context.taskSlug, REVIEWER_ROLE);
|
|
@@ -571,16 +636,13 @@ export function createGateReviewService(deps) {
|
|
|
571
636
|
model: "default"
|
|
572
637
|
});
|
|
573
638
|
}
|
|
574
|
-
async function
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
return next;
|
|
579
|
-
}
|
|
580
|
-
async function callbackProjectManager(context, gate, status, decision, reportPath, error) {
|
|
639
|
+
async function callbackProjectManager(context, gate, status, decision, reportPath, error, requestId, inputHash) {
|
|
640
|
+
if (!(await gateStateMatches(context, gate, status, requestId))) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
581
643
|
const session = await deps.sessionService.getRoleSession(context.repoRoot, context.taskSlug, "project-manager");
|
|
582
644
|
if (!session || session.status !== "running") {
|
|
583
|
-
await
|
|
645
|
+
await updateGateCallbackState(context, gate, status, requestId, {
|
|
584
646
|
callbackStatus: "skipped",
|
|
585
647
|
callbackError: "project-manager session is not running",
|
|
586
648
|
updatedAt: now()
|
|
@@ -593,7 +655,9 @@ export function createGateReviewService(deps) {
|
|
|
593
655
|
status,
|
|
594
656
|
decision,
|
|
595
657
|
reportPath,
|
|
596
|
-
error
|
|
658
|
+
error,
|
|
659
|
+
requestId,
|
|
660
|
+
inputHash
|
|
597
661
|
});
|
|
598
662
|
try {
|
|
599
663
|
await submitTerminalInput(deps.runtime, session.id, prompt);
|
|
@@ -606,20 +670,40 @@ export function createGateReviewService(deps) {
|
|
|
606
670
|
role: "project-manager",
|
|
607
671
|
eventName: "UserPromptSubmit"
|
|
608
672
|
});
|
|
609
|
-
await
|
|
673
|
+
await updateGateCallbackState(context, gate, status, requestId, {
|
|
610
674
|
callbackStatus: "sent",
|
|
611
675
|
callbackError: undefined,
|
|
612
676
|
updatedAt: now()
|
|
613
677
|
});
|
|
614
678
|
}
|
|
615
679
|
catch (caught) {
|
|
616
|
-
await
|
|
680
|
+
await updateGateCallbackState(context, gate, status, requestId, {
|
|
617
681
|
callbackStatus: "failed",
|
|
618
682
|
callbackError: errorMessage(caught),
|
|
619
683
|
updatedAt: now()
|
|
620
684
|
});
|
|
621
685
|
}
|
|
622
686
|
}
|
|
687
|
+
async function gateStateMatches(context, gate, expectedStatus, requestId) {
|
|
688
|
+
return withGateStateLock(context, async () => {
|
|
689
|
+
const index = await loadIndex(deps.fs, context, now());
|
|
690
|
+
const record = index.gates[gate];
|
|
691
|
+
return record.status === expectedStatus
|
|
692
|
+
&& (requestId === undefined || record.requestId === requestId);
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
async function updateGateCallbackState(context, gate, expectedStatus, requestId, patch) {
|
|
696
|
+
await withGateStateLock(context, async () => {
|
|
697
|
+
const timestamp = now();
|
|
698
|
+
const index = await loadIndex(deps.fs, context, timestamp);
|
|
699
|
+
const record = index.gates[gate];
|
|
700
|
+
if (record.status !== expectedStatus || (requestId !== undefined && record.requestId !== requestId)) {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const next = applyGateState(index, gate, patch, timestamp);
|
|
704
|
+
await saveIndex(deps.fs, context.taskRepoRoot, next);
|
|
705
|
+
});
|
|
706
|
+
}
|
|
623
707
|
async function notifyArchitecturePlanDisposition(context, gate, accepted) {
|
|
624
708
|
if (gate !== "architecture-plan" || !deps.onArchitecturePlanDisposition) {
|
|
625
709
|
return;
|
|
@@ -641,27 +725,44 @@ export function createGateReviewService(deps) {
|
|
|
641
725
|
return loadIndex(deps.fs, context, now());
|
|
642
726
|
},
|
|
643
727
|
async updateSettings(repoRoot, taskSlug, input) {
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
728
|
+
const context = await getContext(repoRoot, taskSlug);
|
|
729
|
+
return withGateStateLock(context, async () => {
|
|
730
|
+
const timestamp = now();
|
|
731
|
+
const currentIndex = await loadIndex(deps.fs, context, timestamp);
|
|
732
|
+
const currentSettings = await deps.appSettings.getGateReviewSettings(repoRoot, taskSlug);
|
|
733
|
+
const requiredGates = new Set(currentSettings.requiredGates);
|
|
734
|
+
for (const [gate, enabled] of Object.entries(input?.gates ?? {})) {
|
|
735
|
+
if (!isGateReviewGate(gate)) {
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
if (!enabled && currentIndex.gates[gate].status === "running") {
|
|
739
|
+
throw new VcmError({
|
|
740
|
+
code: "GATE_REVIEW_RUNNING",
|
|
741
|
+
message: `Cannot disable ${gate} while its Gate Review request is running.`,
|
|
742
|
+
statusCode: 409,
|
|
743
|
+
hint: "Cancel the running request first, then disable this Gate."
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
if (enabled) {
|
|
747
|
+
requiredGates.add(gate);
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
requiredGates.delete(gate);
|
|
751
|
+
}
|
|
655
752
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
753
|
+
const settings = await deps.appSettings.updateGateReviewSettings(repoRoot, taskSlug, [...requiredGates]);
|
|
754
|
+
const nextContext = {
|
|
755
|
+
...context,
|
|
756
|
+
config: loadRuntimeConfig(settings)
|
|
757
|
+
};
|
|
758
|
+
const index = await loadIndex(deps.fs, nextContext, timestamp);
|
|
759
|
+
const next = {
|
|
760
|
+
...index,
|
|
761
|
+
updatedAt: timestamp
|
|
762
|
+
};
|
|
763
|
+
await saveIndex(deps.fs, nextContext.taskRepoRoot, next);
|
|
764
|
+
return next;
|
|
663
765
|
});
|
|
664
|
-
return loadIndex(deps.fs, nextContext, now());
|
|
665
766
|
},
|
|
666
767
|
requestReviewGate(repoRoot, taskSlug, gate, input) {
|
|
667
768
|
return requestReviewGateInternal(repoRoot, taskSlug, gate, {
|
|
@@ -671,28 +772,89 @@ export function createGateReviewService(deps) {
|
|
|
671
772
|
retryReviewGate(repoRoot, taskSlug, gate) {
|
|
672
773
|
return requestReviewGateInternal(repoRoot, taskSlug, gate, { force: true });
|
|
673
774
|
},
|
|
775
|
+
async cancelReviewGate(repoRoot, taskSlug, gate, input) {
|
|
776
|
+
const context = await getContext(repoRoot, taskSlug);
|
|
777
|
+
assertCancelRequest(input);
|
|
778
|
+
return withGateStateLock(context, async () => {
|
|
779
|
+
const timestamp = now();
|
|
780
|
+
const index = await loadIndex(deps.fs, context, timestamp);
|
|
781
|
+
const record = index.gates[gate];
|
|
782
|
+
if (record.status !== "running") {
|
|
783
|
+
throw new VcmError({
|
|
784
|
+
code: "GATE_REVIEW_NOT_RUNNING",
|
|
785
|
+
message: `Gate review ${gate} is not running.`,
|
|
786
|
+
statusCode: 409
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
if (record.requestId !== input.requestId) {
|
|
790
|
+
throw new VcmError({
|
|
791
|
+
code: "GATE_REVIEW_REQUEST_MISMATCH",
|
|
792
|
+
message: `Gate review ${gate} is running request ${record.requestId ?? "unknown"}, not ${input.requestId}.`,
|
|
793
|
+
statusCode: 409,
|
|
794
|
+
hint: "Refresh Gate Review state before cancelling the current request."
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
activeRuns.get(gateRunKey(context, gate, input.requestId))?.abort();
|
|
798
|
+
await deps.sessionService.restartRoleSession(context.repoRoot, context.taskSlug, REVIEWER_ROLE);
|
|
799
|
+
await updateRequestStatus(deps.fs, context, input.requestId, "cancelled", {
|
|
800
|
+
completedAt: timestamp,
|
|
801
|
+
cancelReason: input.reason.trim()
|
|
802
|
+
});
|
|
803
|
+
const next = applyGateState(index, gate, {
|
|
804
|
+
status: "pending",
|
|
805
|
+
decision: undefined,
|
|
806
|
+
summary: undefined,
|
|
807
|
+
findings: undefined,
|
|
808
|
+
error: undefined,
|
|
809
|
+
exceptionReason: undefined,
|
|
810
|
+
requestId: undefined,
|
|
811
|
+
requestPath: undefined,
|
|
812
|
+
inputHash: undefined,
|
|
813
|
+
baseCommit: undefined,
|
|
814
|
+
headCommit: undefined,
|
|
815
|
+
commits: undefined,
|
|
816
|
+
changedFiles: undefined,
|
|
817
|
+
diffStat: undefined,
|
|
818
|
+
codeDiffSource: undefined,
|
|
819
|
+
codeDiffSources: undefined,
|
|
820
|
+
requestedAt: undefined,
|
|
821
|
+
startedAt: undefined,
|
|
822
|
+
completedAt: undefined,
|
|
823
|
+
callbackStatus: "not_sent",
|
|
824
|
+
callbackError: undefined,
|
|
825
|
+
updatedAt: timestamp
|
|
826
|
+
}, timestamp, true);
|
|
827
|
+
await saveIndex(deps.fs, context.taskRepoRoot, next);
|
|
828
|
+
return next;
|
|
829
|
+
});
|
|
830
|
+
},
|
|
674
831
|
async skipReviewGate(repoRoot, taskSlug, gate, input) {
|
|
675
832
|
const context = await getContext(repoRoot, taskSlug);
|
|
676
833
|
assertExceptionReason(input.reason);
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
834
|
+
const index = await withGateStateLock(context, async () => {
|
|
835
|
+
const timestamp = now();
|
|
836
|
+
const current = await loadIndex(deps.fs, context, timestamp);
|
|
837
|
+
if (current.gates[gate].status === "running") {
|
|
838
|
+
throw new VcmError({
|
|
839
|
+
code: "GATE_REVIEW_RUNNING",
|
|
840
|
+
message: "Cannot skip a running Gate review gate.",
|
|
841
|
+
statusCode: 409,
|
|
842
|
+
hint: "Cancel the running request first, then choose retry, skip, or override."
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
const next = applyGateState(current, gate, {
|
|
846
|
+
status: "skipped",
|
|
847
|
+
decision: undefined,
|
|
848
|
+
exceptionReason: input.reason,
|
|
849
|
+
error: undefined,
|
|
850
|
+
completedAt: timestamp,
|
|
851
|
+
callbackStatus: "not_sent",
|
|
852
|
+
callbackError: undefined,
|
|
853
|
+
updatedAt: timestamp
|
|
854
|
+
}, timestamp, true);
|
|
855
|
+
await saveIndex(deps.fs, context.taskRepoRoot, next);
|
|
856
|
+
return next;
|
|
857
|
+
});
|
|
696
858
|
await notifyArchitecturePlanDisposition(context, gate, true);
|
|
697
859
|
await callbackProjectManager(context, gate, "skipped", undefined, index.gates[gate].reportPath);
|
|
698
860
|
return loadIndex(deps.fs, context, now());
|
|
@@ -700,25 +862,30 @@ export function createGateReviewService(deps) {
|
|
|
700
862
|
async overrideReviewGate(repoRoot, taskSlug, gate, input) {
|
|
701
863
|
const context = await getContext(repoRoot, taskSlug);
|
|
702
864
|
assertExceptionReason(input.reason);
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
865
|
+
const index = await withGateStateLock(context, async () => {
|
|
866
|
+
const timestamp = now();
|
|
867
|
+
const current = await loadIndex(deps.fs, context, timestamp);
|
|
868
|
+
if (current.gates[gate].status === "running") {
|
|
869
|
+
throw new VcmError({
|
|
870
|
+
code: "GATE_REVIEW_RUNNING",
|
|
871
|
+
message: "Cannot override a running Gate review gate.",
|
|
872
|
+
statusCode: 409,
|
|
873
|
+
hint: "Cancel the running request first, then choose retry, skip, or override."
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
const next = applyGateState(current, gate, {
|
|
877
|
+
status: "overridden",
|
|
878
|
+
decision: "approve",
|
|
879
|
+
exceptionReason: input.reason,
|
|
880
|
+
error: undefined,
|
|
881
|
+
completedAt: timestamp,
|
|
882
|
+
callbackStatus: "not_sent",
|
|
883
|
+
callbackError: undefined,
|
|
884
|
+
updatedAt: timestamp
|
|
885
|
+
}, timestamp, true);
|
|
886
|
+
await saveIndex(deps.fs, context.taskRepoRoot, next);
|
|
887
|
+
return next;
|
|
888
|
+
});
|
|
722
889
|
await notifyArchitecturePlanDisposition(context, gate, true);
|
|
723
890
|
await callbackProjectManager(context, gate, "overridden", "approve", index.gates[gate].reportPath);
|
|
724
891
|
return loadIndex(deps.fs, context, now());
|
|
@@ -818,6 +985,27 @@ function applyGateState(index, gate, patch, timestamp, clearActiveGate = false)
|
|
|
818
985
|
updatedAt: timestamp
|
|
819
986
|
};
|
|
820
987
|
}
|
|
988
|
+
function isCurrentRunningRequest(index, gate, requestId) {
|
|
989
|
+
const record = index.gates[gate];
|
|
990
|
+
return record.status === "running" && record.requestId === requestId;
|
|
991
|
+
}
|
|
992
|
+
function gateRunKey(context, gate, requestId) {
|
|
993
|
+
return `${context.taskRepoRoot}:${context.taskSlug}:${gate}:${requestId}`;
|
|
994
|
+
}
|
|
995
|
+
async function withGateStateLock(context, run) {
|
|
996
|
+
const key = getIndexPath(context.taskRepoRoot);
|
|
997
|
+
const previous = gateStateLocks.get(key) ?? Promise.resolve();
|
|
998
|
+
const next = previous.catch(() => undefined).then(run);
|
|
999
|
+
gateStateLocks.set(key, next);
|
|
1000
|
+
try {
|
|
1001
|
+
return await next;
|
|
1002
|
+
}
|
|
1003
|
+
finally {
|
|
1004
|
+
if (gateStateLocks.get(key) === next) {
|
|
1005
|
+
gateStateLocks.delete(key);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
821
1009
|
async function saveIndex(fs, taskRepoRoot, index) {
|
|
822
1010
|
await fs.writeJsonAtomic(getIndexPath(taskRepoRoot), index);
|
|
823
1011
|
}
|
|
@@ -1130,12 +1318,22 @@ function splitLines(value) {
|
|
|
1130
1318
|
.map((line) => line.trim())
|
|
1131
1319
|
.filter(Boolean);
|
|
1132
1320
|
}
|
|
1133
|
-
function buildGatePrompt(context, gate, requestId, codeDiffInput, codeDiffSources) {
|
|
1321
|
+
function buildGatePrompt(context, gate, requestId, codeDiffInput, codeDiffSources, inputSnapshots = []) {
|
|
1134
1322
|
const reportPath = reportPathForRequest(requestId);
|
|
1135
1323
|
const absoluteReportPath = resolveRepoPath(context.taskRepoRoot, reportPath);
|
|
1136
1324
|
const evidence = getSourceArtifacts(gate, codeDiffSources)
|
|
1137
1325
|
.map((relativePath) => `- ${relativePath}`)
|
|
1138
1326
|
.join("\n");
|
|
1327
|
+
const capturedEvidence = inputSnapshots.length > 0
|
|
1328
|
+
? `
|
|
1329
|
+
|
|
1330
|
+
Captured Task Evidence:
|
|
1331
|
+
${inputSnapshots.map((snapshot) => snapshot.status === "captured"
|
|
1332
|
+
? `- ${snapshot.sourcePath} -> ${snapshot.snapshotPath}`
|
|
1333
|
+
: `- ${snapshot.sourcePath} -> <missing at request time>`).join("\n")}
|
|
1334
|
+
|
|
1335
|
+
Use each captured snapshot as the immutable handoff or prior-Gate input for this request. Do not substitute a later rewritten live artifact.`
|
|
1336
|
+
: "";
|
|
1139
1337
|
const gitLine = gate === "architecture-plan"
|
|
1140
1338
|
? "\nDiff: inspect git status/diff in Worktree."
|
|
1141
1339
|
: "";
|
|
@@ -1174,7 +1372,7 @@ Request: ${requestId}
|
|
|
1174
1372
|
Report: ${absoluteReportPath}
|
|
1175
1373
|
|
|
1176
1374
|
Evidence:
|
|
1177
|
-
${evidence}${gitLine}${architectureContract}${validationContract}${codeDiffContract}${codeDiffSection}
|
|
1375
|
+
${evidence}${capturedEvidence}${gitLine}${architectureContract}${validationContract}${codeDiffContract}${codeDiffSection}
|
|
1178
1376
|
|
|
1179
1377
|
Write only Report. Start exactly:
|
|
1180
1378
|
Gate: ${gate}
|
|
@@ -1183,9 +1381,28 @@ Decision: approve|request_changes
|
|
|
1183
1381
|
Summary: <one or two sentences>
|
|
1184
1382
|
[/VCM GATE REVIEW]`;
|
|
1185
1383
|
}
|
|
1186
|
-
async function
|
|
1384
|
+
async function captureGateInputSnapshots(fs, taskRepoRoot, requestId, sourcePaths) {
|
|
1385
|
+
const snapshots = [];
|
|
1386
|
+
for (const sourcePath of new Set(sourcePaths.filter(isTaskEvidencePath))) {
|
|
1387
|
+
const absoluteSourcePath = resolveRepoPath(taskRepoRoot, sourcePath);
|
|
1388
|
+
if (!(await fs.pathExists(absoluteSourcePath))) {
|
|
1389
|
+
snapshots.push({ sourcePath, status: "missing" });
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
const snapshotPath = inputSnapshotPathForRequest(requestId, sourcePath);
|
|
1393
|
+
await fs.writeText(resolveRepoPath(taskRepoRoot, snapshotPath), await fs.readText(absoluteSourcePath));
|
|
1394
|
+
snapshots.push({ sourcePath, snapshotPath, status: "captured" });
|
|
1395
|
+
}
|
|
1396
|
+
return snapshots;
|
|
1397
|
+
}
|
|
1398
|
+
function isTaskEvidencePath(sourcePath) {
|
|
1399
|
+
return sourcePath.startsWith(".ai/vcm/handoffs/")
|
|
1400
|
+
|| sourcePath.startsWith(".ai/vcm/gate-reviews/");
|
|
1401
|
+
}
|
|
1402
|
+
async function waitForGateReport(fs, taskRepoRoot, gate, requestId, timestamp, intervalMs, signal) {
|
|
1187
1403
|
const reportPath = reportPathForRequest(requestId);
|
|
1188
1404
|
while (true) {
|
|
1405
|
+
throwIfGateRunCancelled(signal);
|
|
1189
1406
|
try {
|
|
1190
1407
|
return await parseGateReport(fs, taskRepoRoot, gate, requestId, timestamp, reportPath);
|
|
1191
1408
|
}
|
|
@@ -1427,6 +1644,10 @@ function reportPathForRequest(requestId) {
|
|
|
1427
1644
|
function promptPathForRequest(requestId) {
|
|
1428
1645
|
return path.posix.join(REQUESTS_DIR, `${requestId}.prompt.md`);
|
|
1429
1646
|
}
|
|
1647
|
+
function inputSnapshotPathForRequest(requestId, sourcePath) {
|
|
1648
|
+
const taskEvidencePath = sourcePath.replace(/^\.ai\/vcm\//, "");
|
|
1649
|
+
return path.posix.join(REQUESTS_DIR, `${requestId}.inputs`, taskEvidencePath);
|
|
1650
|
+
}
|
|
1430
1651
|
function promptPathForGate(gate) {
|
|
1431
1652
|
return path.posix.join(GATE_REVIEW_DIR, "prompts", `${gate}-gate.md`);
|
|
1432
1653
|
}
|
|
@@ -1576,6 +1797,30 @@ function assertExceptionReason(reason) {
|
|
|
1576
1797
|
});
|
|
1577
1798
|
}
|
|
1578
1799
|
}
|
|
1800
|
+
function assertCancelRequest(input) {
|
|
1801
|
+
if (!input?.requestId?.trim()) {
|
|
1802
|
+
throw new VcmError({
|
|
1803
|
+
code: "GATE_REVIEW_REQUEST_ID_REQUIRED",
|
|
1804
|
+
message: "The current Gate Review request ID is required.",
|
|
1805
|
+
statusCode: 400
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
assertExceptionReason(input.reason);
|
|
1809
|
+
}
|
|
1810
|
+
class GateReviewRunCancelledError extends Error {
|
|
1811
|
+
constructor() {
|
|
1812
|
+
super("Gate Review run was cancelled.");
|
|
1813
|
+
this.name = "GateReviewRunCancelledError";
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
function throwIfGateRunCancelled(signal) {
|
|
1817
|
+
if (signal.aborted) {
|
|
1818
|
+
throw new GateReviewRunCancelledError();
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
function isGateRunCancelled(error) {
|
|
1822
|
+
return error instanceof GateReviewRunCancelledError;
|
|
1823
|
+
}
|
|
1579
1824
|
function renderProjectManagerCallback(input) {
|
|
1580
1825
|
const lines = [
|
|
1581
1826
|
"[VCM GATE REVIEW CALLBACK]",
|
|
@@ -1583,6 +1828,8 @@ function renderProjectManagerCallback(input) {
|
|
|
1583
1828
|
`gate: ${input.gate}`,
|
|
1584
1829
|
`status: ${input.status}`,
|
|
1585
1830
|
`decision: ${input.decision ?? "none"}`,
|
|
1831
|
+
`request_id: ${input.requestId ?? "none"}`,
|
|
1832
|
+
`input_hash: ${input.inputHash ?? "none"}`,
|
|
1586
1833
|
`report: ${input.reportPath}`,
|
|
1587
1834
|
...(input.error ? [`error: ${input.error}`] : []),
|
|
1588
1835
|
"",
|