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.
Files changed (27) hide show
  1. package/README.md +25 -12
  2. package/dist/backend/adapters/git-adapter.js +21 -0
  3. package/dist/backend/api/gate-review-routes.js +5 -0
  4. package/dist/backend/services/auto-memory-service.js +115 -81
  5. package/dist/backend/services/ccr-integration-service.js +2 -1
  6. package/dist/backend/services/claude-hook-service.js +11 -0
  7. package/dist/backend/services/gate-review-service.js +354 -107
  8. package/dist/backend/services/harness-feedback-service.js +6 -3
  9. package/dist/backend/services/session-service.js +42 -52
  10. package/dist/backend/templates/handoff.js +17 -0
  11. package/dist/backend/templates/harness/architect-agent.js +5 -2
  12. package/dist/backend/templates/harness/claude-root.js +8 -0
  13. package/dist/backend/templates/harness/coder-agent.js +1 -1
  14. package/dist/backend/templates/harness/gate-review.js +2 -0
  15. package/dist/backend/templates/harness/harness-engineer-agent.js +14 -9
  16. package/dist/backend/templates/harness/tester-agent.js +1 -1
  17. package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +2 -0
  18. package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +3 -1
  19. package/dist/backend/templates/harness/vcm-route-message-skill.js +1 -1
  20. package/dist/shared/types/session.js +1 -0
  21. package/dist-frontend/assets/index-DN9kwoOt.css +32 -0
  22. package/dist-frontend/assets/{index-Cmr9W7-n.js → index-DhLRhgSV.js} +35 -35
  23. package/dist-frontend/index.html +2 -2
  24. package/package.json +1 -1
  25. package/scripts/harness-tools/vcm-bash-guard +62 -8
  26. package/dist/backend/services/memory-review-validation.js +0 -286
  27. 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 Set();
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" && !options.force) {
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 = `${context.taskRepoRoot}:${context.taskSlug}:${gate}`;
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
- activeRuns.add(runKey);
474
+ const controller = new AbortController();
475
+ activeRuns.set(runKey, controller);
469
476
  try {
470
477
  const timestamp = now();
471
- await updateGateRecord(context, gate, {
472
- status: "running",
473
- startedAt: timestamp,
474
- updatedAt: timestamp
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 publishLatestGateReport(deps.fs, context.taskRepoRoot, gate, parsed.content);
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
- activeRuns.delete(runKey);
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
- }, { clearActiveGate: true });
525
- await notifyArchitecturePlanDisposition(context, gate, parsed.decision === "approve");
526
- await callbackProjectManager(context, gate, "completed", parsed.decision, parsed.reportPath);
527
- }
528
- catch (error) {
529
- const timestamp = now();
530
- const message = errorMessage(error);
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: timestamp,
601
+ completedAt,
533
602
  error: message
534
603
  });
535
- activeRuns.delete(runKey);
536
- await updateGateRecord(context, gate, {
604
+ const next = applyGateState(index, gate, {
537
605
  status: "failed",
538
606
  error: message,
539
- completedAt: timestamp,
607
+ completedAt,
540
608
  callbackStatus: "not_sent",
541
609
  callbackError: undefined,
542
- updatedAt: timestamp
543
- }, { clearActiveGate: true });
544
- await notifyArchitecturePlanDisposition(context, gate, false);
545
- await callbackProjectManager(context, gate, "failed", undefined, reportPathForRequest(requestId), message);
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 updateGateRecord(context, gate, patch, options = {}) {
575
- const index = await loadIndex(deps.fs, context, now());
576
- const next = applyGateState(index, gate, patch, now(), options.clearActiveGate);
577
- await saveIndex(deps.fs, context.taskRepoRoot, next);
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 updateGateRecord(context, gate, {
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 updateGateRecord(context, gate, {
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 updateGateRecord(context, gate, {
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 currentSettings = await deps.appSettings.getGateReviewSettings(repoRoot, taskSlug);
645
- const requiredGates = new Set(currentSettings.requiredGates);
646
- for (const [gate, enabled] of Object.entries(input?.gates ?? {})) {
647
- if (!isGateReviewGate(gate)) {
648
- continue;
649
- }
650
- if (enabled) {
651
- requiredGates.add(gate);
652
- }
653
- else {
654
- requiredGates.delete(gate);
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
- await deps.appSettings.updateGateReviewSettings(repoRoot, taskSlug, [...requiredGates]);
658
- const nextContext = await getContext(repoRoot, taskSlug);
659
- const index = await loadIndex(deps.fs, nextContext, now());
660
- await saveIndex(deps.fs, nextContext.taskRepoRoot, {
661
- ...index,
662
- updatedAt: now()
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 current = await loadIndex(deps.fs, context, now());
678
- if (current.gates[gate].status === "running") {
679
- throw new VcmError({
680
- code: "GATE_REVIEW_RUNNING",
681
- message: "Cannot skip a running Gate review gate.",
682
- statusCode: 409,
683
- hint: "Wait for the gate review to finish, then choose retry, skip, or override."
684
- });
685
- }
686
- const index = await updateGateRecord(context, gate, {
687
- status: "skipped",
688
- decision: undefined,
689
- exceptionReason: input.reason,
690
- error: undefined,
691
- completedAt: now(),
692
- callbackStatus: "not_sent",
693
- callbackError: undefined,
694
- updatedAt: now()
695
- }, { clearActiveGate: true });
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 current = await loadIndex(deps.fs, context, now());
704
- if (current.gates[gate].status === "running") {
705
- throw new VcmError({
706
- code: "GATE_REVIEW_RUNNING",
707
- message: "Cannot override a running Gate review gate.",
708
- statusCode: 409,
709
- hint: "Wait for the gate review to finish, then choose retry, skip, or override."
710
- });
711
- }
712
- const index = await updateGateRecord(context, gate, {
713
- status: "overridden",
714
- decision: "approve",
715
- exceptionReason: input.reason,
716
- error: undefined,
717
- completedAt: now(),
718
- callbackStatus: "not_sent",
719
- callbackError: undefined,
720
- updatedAt: now()
721
- }, { clearActiveGate: true });
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 waitForGateReport(fs, taskRepoRoot, gate, requestId, timestamp, intervalMs) {
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
  "",