vibe-coding-master 0.9.1 → 0.9.3

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 (39) hide show
  1. package/README.md +11 -5
  2. package/dist/backend/adapters/command-runner.js +1 -0
  3. package/dist/backend/adapters/filesystem.js +5 -2
  4. package/dist/backend/api/artifact-routes.js +0 -3
  5. package/dist/backend/api/task-routes.js +0 -1
  6. package/dist/backend/cli/install-vcm-harness.js +9 -2
  7. package/dist/backend/role-tool-policy.js +10 -1
  8. package/dist/backend/server.js +2 -0
  9. package/dist/backend/services/artifact-service.js +1 -16
  10. package/dist/backend/services/docs-sync-check.js +32 -0
  11. package/dist/backend/services/gate-review-service.js +10 -2
  12. package/dist/backend/services/harness-service.js +3 -2
  13. package/dist/backend/services/lsp-plugin.js +24 -5
  14. package/dist/backend/services/managed-artifact-validation.js +3 -1
  15. package/dist/backend/services/session-service.js +2 -2
  16. package/dist/backend/services/status-service.js +0 -1
  17. package/dist/backend/services/translation-worker-service.js +44 -6
  18. package/dist/backend/services/workflow-control-service.js +103 -165
  19. package/dist/backend/templates/handoff.js +2 -48
  20. package/dist/backend/templates/harness/architect-agent.js +43 -24
  21. package/dist/backend/templates/harness/architect-evidence-worker-agent.js +6 -3
  22. package/dist/backend/templates/harness/claude-root.js +9 -9
  23. package/dist/backend/templates/harness/coder-agent.js +0 -1
  24. package/dist/backend/templates/harness/gate-review.js +83 -13
  25. package/dist/backend/templates/harness/project-manager-agent.js +33 -27
  26. package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +13 -14
  27. package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +2 -2
  28. package/dist/backend/templates/harness/vcm-workflow-review-skill.js +2 -2
  29. package/dist/shared/types/gate-review.js +4 -0
  30. package/dist/shared/types/workflow.js +0 -1
  31. package/dist/shared/validation/artifact-check.js +1 -27
  32. package/dist/shared/validation/artifact-contract.js +0 -2
  33. package/dist/shared/validation/artifact-registry.js +1 -18
  34. package/package.json +1 -1
  35. package/scripts/claude-plugins/vcm-lsp-bridge/vcm-lsp-proxy.mjs +273 -0
  36. package/scripts/harness-tools/check-docs-sync +246 -0
  37. package/scripts/harness-tools/check-durable-docs +24 -0
  38. package/scripts/harness-tools/run-long-check +3 -3
  39. package/scripts/verify-package.mjs +2 -0
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import path from "node:path";
3
3
  import { resolveRepoPath } from "../adapters/filesystem.js";
4
4
  import { VcmError } from "../errors.js";
5
+ import { CODE_DIFF_DOCS_COVERAGE_ERROR_PREFIX } from "../../shared/types/gate-review.js";
5
6
  import { WORKFLOW_EVIDENCE_ARTIFACTS, WORKFLOW_EVIDENCE_GATES, WORKFLOW_FLOWS } from "../../shared/types/workflow.js";
6
7
  import { checkMarkdownArtifact, readArtifactSectionContent } from "../../shared/validation/artifact-check.js";
7
8
  import { ARCHITECT_DEBUG_NORMAL_PLAN_DISPOSITION } from "../../shared/validation/artifact-contract.js";
@@ -147,7 +148,7 @@ export function createWorkflowControlService(deps) {
147
148
  throw workflowError("WORKFLOW_FOLLOW_UP_APPROVAL_NOT_REQUIRED", "This workflow transition is already legal and must not consume a post-validation follow-up approval.", "Set User-Approved Follow-Up Approval Text to none.");
148
149
  }
149
150
  if (!followUpAllowed) {
150
- throw workflowError("WORKFLOW_FOLLOW_UP_APPROVAL_INVALID", "User-approved follow-up work is legal only for a Tester dispatch after a fresh passing Test Report and successful validation-adequacy and code-diff Gates.", "Use the normal allowed transition, or ask the user only when optional Tester-owned work is proposed after validation is fully green.");
151
+ throw workflowError("WORKFLOW_FOLLOW_UP_APPROVAL_INVALID", "User-approved follow-up work is legal only for a Tester dispatch after a fresh passing Test Report and a successful validation-adequacy Gate (and, in a Debug or Diagnosis Branch, its code-diff Gate).", "Use the normal allowed transition, or ask the user only when optional Tester-owned work is proposed after validation is fully green.");
151
152
  }
152
153
  userApprovedFollowUp = createUserApprovedFollowUp(id(), current, candidate, effectiveFlow, baseHistoryHash, followUpApprovalText, now());
153
154
  followUpApprovalId = userApprovedFollowUp.id;
@@ -401,7 +402,6 @@ export function createWorkflowControlService(deps) {
401
402
  resolveUserInput,
402
403
  runWhileNotAwaitingUser,
403
404
  submitProgress,
404
- assertDocsArtifactAllowed: (input, kind, role) => withLock(statePath(input), () => assertDocsArtifactAllowed(input, kind, role)),
405
405
  assertRouteAuthorized: (input) => withLock(statePath(input), () => assertRouteAuthorized(input)),
406
406
  claimDispatch,
407
407
  releaseDispatch,
@@ -410,37 +410,6 @@ export function createWorkflowControlService(deps) {
410
410
  confirmDispatch,
411
411
  recoverTask
412
412
  };
413
- async function assertDocsArtifactAllowed(input, kind, role) {
414
- const state = await getState(input);
415
- failOnStateWarnings(state);
416
- const progress = await readProgress(deps.fs, input);
417
- const active = state.activeDispatch;
418
- if (progress.status !== "active" || !active || active.targetRole !== role)
419
- return;
420
- let expected;
421
- if (progress.flow === "docs-only") {
422
- expected = "docs-update-report";
423
- }
424
- else if (role === "architect" && (progress.flow === "code-change"
425
- || progress.flow === "architect-debug" || progress.flow === "architecture-diagnosis")) {
426
- const flow = progress.flow;
427
- const run = resolveFlowRun(state.flowRun, progress);
428
- const hadTester = progress.history.some((entry) => entry.sequence >= run.startedAtSequence
429
- && entry.sequence < active.sequence && entry.targetRole === "tester");
430
- if (!run.activeBranch && hadTester) {
431
- const source = run.resumedFromBranch ?? (flow === "code-change" ? "coder" : flow);
432
- const docsTarget = `${flow}/architect`;
433
- const allowed = await allowedAfterTester(deps.fs, input, state, flow, source, {
434
- successTarget: docsTarget
435
- });
436
- if (allowed.includes(docsTarget))
437
- expected = "docs-sync-report";
438
- }
439
- }
440
- if (expected && expected !== kind) {
441
- throw workflowError("WORKFLOW_DOCS_ARTIFACT_MISMATCH", `${kind} was not written: the active ${progress.flow} dispatch #${active.sequence} requires ${path.posix.join(input.handoffDir, `${expected}.md`)}.`, `Submit --kind ${expected} through .ai/tools/vcm-artifact. The two documentation reports are not interchangeable.`);
442
- }
443
- }
444
413
  async function requestUserInput(input, question) {
445
414
  return withLock(statePath(input), async () => {
446
415
  const state = await getState(input);
@@ -730,15 +699,14 @@ async function canRouteUserApprovedFollowUp(fs, input, state, current, effective
730
699
  return false;
731
700
  }
732
701
  const validation = await gateState(fs, input, "validation-adequacy");
702
+ if (!gatePassedForDispatch(state, effectiveFlow, "tester", "validation-adequacy", validation))
703
+ return false;
733
704
  const codeDiff = await gateState(fs, input, "code-diff");
734
- if (!gatePassedForDispatch(state, effectiveFlow, "tester", "validation-adequacy", validation)
735
- || !gatePassedForDispatch(state, effectiveFlow, "tester", "code-diff", codeDiff))
705
+ if (!gatePassedForDispatch(state, effectiveFlow, "tester", "code-diff", codeDiff))
736
706
  return false;
737
- const flowRun = resolveFlowRun(state.flowRun, current);
738
- const expectedExit = flowRun.activeBranch === effectiveFlow
739
- ? `${flowRun.rootFlow}/architect`
740
- : `${effectiveFlow}/architect`;
741
- return allowedTransitions.length === 1 && allowedTransitions[0] === expectedExit;
707
+ // Both Gates passed and no Final Acceptance follow-up is pending: the flow
708
+ // is waiting for Final Acceptance, the checkpoint the follow-up returns to.
709
+ return allowedTransitions.length === 0;
742
710
  }
743
711
  async function getAllowedTransitions(fs, input, state, current) {
744
712
  if (current.status === "completed")
@@ -775,9 +743,6 @@ async function getAllowedTransitions(fs, input, state, current) {
775
743
  return [];
776
744
  return [];
777
745
  }
778
- const coderDocsCorrection = await allowedActiveCoderDocsCorrection(fs, input, state, flow);
779
- if (coderDocsCorrection)
780
- return coderDocsCorrection;
781
746
  const segment = activeFlowSegment(current.history, flow, flowRun.startedAtSequence);
782
747
  if (flow === "code-change") {
783
748
  return allowedCodeChange(fs, input, state, segment, flowRun.resumedFromBranch);
@@ -799,20 +764,10 @@ function initialTransitions() {
799
764
  async function allowedCodeChange(fs, input, state, segment, resumedFromBranch) {
800
765
  const coderIndex = findLastIndex(segment, (entry) => entry.targetRole === "coder");
801
766
  const testerIndex = findLastIndex(segment, (entry) => entry.targetRole === "tester");
802
- if (resumedFromBranch && coderIndex < 0) {
803
- if (testerIndex < 0) {
804
- return allowedPostImplementationArchitect(fs, input, state, "code-change");
805
- }
806
- return allowedAfterTester(fs, input, state, "code-change", resumedFromBranch, {
807
- testerFailureFlow: "architect-debug",
808
- implementationFailureFlow: "architect-debug",
809
- successTarget: "code-change/architect"
810
- });
811
- }
812
- if (coderIndex < 0) {
767
+ if (coderIndex < 0 && !resumedFromBranch) {
813
768
  return (await allowedFreshArchitecturePlan(fs, input, state)) ?? ["code-change/architect"];
814
769
  }
815
- if (testerIndex < coderIndex) {
770
+ if (coderIndex >= 0 && testerIndex < coderIndex) {
816
771
  const coder = await artifactState(fs, input, "coder-completion.md", "coder-completion");
817
772
  if (!evidenceIsFresh(state, "code-change", "coder", "coder-completion.md", coder.hash)) {
818
773
  return ["code-change/coder"];
@@ -821,14 +776,13 @@ async function allowedCodeChange(fs, input, state, segment, resumedFromBranch) {
821
776
  return ["architect-debug/architect"];
822
777
  return coder.value === "ready_for_review" ? ["code-change/tester"] : ["code-change/coder"];
823
778
  }
779
+ // An Architect dispatch after validation (a code-diff docs correction or a
780
+ // Final Acceptance follow-up, including the first dispatch after a branch
781
+ // returns) is reviewed by an incremental code-diff Gate.
824
782
  if (segment.some((entry, index) => index > testerIndex && entry.targetRole === "architect")) {
825
- return allowedPostImplementationArchitect(fs, input, state, "code-change");
783
+ return allowedPostImplementationArchitect(fs, input, state, "code-change", "code-change");
826
784
  }
827
- return allowedAfterTester(fs, input, state, "code-change", "coder", {
828
- testerFailureFlow: "architect-debug",
829
- implementationFailureFlow: "architect-debug",
830
- successTarget: "code-change/architect"
831
- });
785
+ return allowedAfterTester(fs, input, state, "code-change", resumedFromBranch && coderIndex < 0 ? resumedFromBranch : "coder", { testerFailureFlow: "architect-debug", acceptanceFlow: "code-change" });
832
786
  }
833
787
  async function allowedFreshArchitecturePlan(fs, input, state, gateMustPostdate) {
834
788
  const baseline = matchingEvidenceBaseline(state, "code-change", "architect");
@@ -850,19 +804,19 @@ async function allowedFreshArchitecturePlan(fs, input, state, gateMustPostdate)
850
804
  async function allowedArchitectFix(fs, input, state, segment, source, resumedFromBranch, parentFlow) {
851
805
  const architectIndex = findLastIndex(segment, (entry) => entry.targetRole === "architect");
852
806
  const testerIndex = findLastIndex(segment, (entry) => entry.targetRole === "tester");
853
- if (resumedFromBranch && architectIndex >= 0 && testerIndex < 0) {
854
- return allowedPostImplementationArchitect(fs, input, state, source);
855
- }
807
+ // A branch that passes its own code-diff Gate has returned to its parent, so
808
+ // Final Acceptance and its follow-ups belong to the parent flow.
809
+ const acceptanceFlow = parentFlow ?? source;
856
810
  if (architectIndex < 0)
857
811
  return [`${source}/architect`];
858
- if (testerIndex < 0) {
859
- const artifact = source === "architect-debug"
860
- ? await artifactState(fs, input, "architect-debug.md", "architect-debug")
861
- : await artifactState(fs, input, "architecture-diagnosis.md", "architecture-diagnosis");
862
- const artifactName = source === "architect-debug" ? "architect-debug.md" : "architecture-diagnosis.md";
863
- if (!evidenceIsFresh(state, source, "architect", artifactName, artifact.hash)) {
812
+ const artifactName = source === "architect-debug" ? "architect-debug.md" : "architecture-diagnosis.md";
813
+ const artifact = source === "architect-debug"
814
+ ? await artifactState(fs, input, artifactName, "architect-debug")
815
+ : await artifactState(fs, input, artifactName, "architecture-diagnosis");
816
+ const artifactFresh = evidenceIsFresh(state, source, "architect", artifactName, artifact.hash);
817
+ if (testerIndex < 0 && !resumedFromBranch) {
818
+ if (!artifactFresh)
864
819
  return [`${source}/architect`];
865
- }
866
820
  if (source === "architect-debug" && artifact.disposition === ARCHITECT_DEBUG_NORMAL_PLAN_DISPOSITION) {
867
821
  return ["code-change/architect"];
868
822
  }
@@ -875,73 +829,57 @@ async function allowedArchitectFix(fs, input, state, segment, source, resumedFro
875
829
  return artifact.complete ? [`${source}/tester`] : [`${source}/architect`];
876
830
  }
877
831
  if (architectIndex > testerIndex) {
878
- const artifactName = source === "architect-debug" ? "architect-debug.md" : "architecture-diagnosis.md";
879
- const artifact = source === "architect-debug"
880
- ? await artifactState(fs, input, artifactName, "architect-debug")
881
- : await artifactState(fs, input, artifactName, "architecture-diagnosis");
882
- if (evidenceIsFresh(state, source, "architect", artifactName, artifact.hash)
883
- && artifact.disposition === "user clarification required") {
832
+ if (artifactFresh && artifact.disposition === "user clarification required") {
884
833
  return [`${source}/architect`];
885
834
  }
886
- if (evidenceIsFresh(state, source, "architect", artifactName, artifact.hash) && artifact.complete) {
835
+ // A new fix needs validation; any other post-validation Architect change
836
+ // is reviewed by an incremental code-diff Gate.
837
+ if (artifactFresh && artifact.complete)
887
838
  return [`${source}/tester`];
888
- }
889
- const docs = await artifactState(fs, input, "docs-sync-report.md", "docs-sync-report");
890
- return evidenceIsFresh(state, source, "architect", "docs-sync-report.md", docs.hash)
891
- && (docs.value === "synced" || docs.value === "unchanged")
892
- ? []
893
- : [`${source}/architect`];
839
+ return allowedPostImplementationArchitect(fs, input, state, source, acceptanceFlow);
894
840
  }
895
841
  return allowedAfterTester(fs, input, state, source, source, {
896
842
  testerFailureFlow: source === "architect-debug" ? "architecture-diagnosis" : undefined,
897
- implementationFailureFlow: source,
898
- successTarget: parentFlow ? `${parentFlow}/architect` : `${source}/architect`
843
+ acceptanceFlow
899
844
  });
900
845
  }
901
- async function allowedPostImplementationArchitect(fs, input, state, flow) {
902
- const docs = await artifactState(fs, input, "docs-sync-report.md", "docs-sync-report");
903
- const acceptance = await artifactState(fs, input, "final-acceptance.md", "final-acceptance");
846
+ async function allowedPostImplementationArchitect(fs, input, state, flow, acceptanceFlow) {
904
847
  if (flow === "code-change") {
905
848
  const planningTransition = await allowedFreshArchitecturePlan(fs, input, state, matchingEvidenceBaseline(state, "code-change", "architect")?.confirmedAt);
906
849
  if (planningTransition !== undefined)
907
850
  return planningTransition;
908
851
  }
909
- if (evidenceIsFresh(state, flow, "architect", "docs-sync-report.md", docs.hash) && docs.complete) {
910
- if (docs.value === "synced" || docs.value === "unchanged") {
911
- if (acceptance.value === "needs-coder-follow-up" && flow === "code-change")
912
- return ["code-change/coder"];
913
- if (acceptance.value === "needs-docs-sync")
914
- return [`${flow}/architect`];
915
- if (acceptance.value === "needs-architect-follow-up")
916
- return [`${flow}/architect`];
917
- return [];
918
- }
919
- if (docs.value === "blocked" && docs.correctionOwner && docs.correctionOwner !== "none") {
920
- return [`${flow}/${docs.correctionOwner}`];
921
- }
852
+ const codeDiff = await gateState(fs, input, "code-diff");
853
+ if (freshGateDecision(state, flow, "architect", "code-diff", codeDiff) === "request_changes") {
854
+ return codeDiffCorrectionTargets(codeDiff, flow);
855
+ }
856
+ if (gatePassedForDispatch(state, flow, "architect", "code-diff", codeDiff)) {
857
+ return finalAcceptanceFollowUpTargets(fs, input, state, flow, "architect", acceptanceFlow);
922
858
  }
923
859
  return [`${flow}/architect`];
924
860
  }
925
- async function allowedActiveCoderDocsCorrection(fs, input, state, flow) {
926
- const active = state.activeDispatch;
927
- if (!active || active.flow !== flow || active.targetRole !== "coder")
928
- return undefined;
929
- const docs = await artifactState(fs, input, "docs-sync-report.md", "docs-sync-report");
930
- if (!docs.complete
931
- || docs.value !== "blocked"
932
- || docs.correctionOwner !== "coder"
933
- || active.artifactHashes["docs-sync-report.md"] !== docs.hash)
934
- return undefined;
935
- const coder = await artifactState(fs, input, "coder-completion.md", "coder-completion");
936
- if (!evidenceIsFresh(state, flow, "coder", "coder-completion.md", coder.hash)
937
- || coder.value === "incomplete")
938
- return [`${flow}/coder`];
939
- if (coder.value === "ready_for_review")
940
- return [`${flow}/tester`];
941
- if (flow === "code-change")
942
- return ["architect-debug/architect"];
943
- if (flow === "architect-debug")
944
- return ["architecture-diagnosis/architect"];
861
+ // Routes a code-diff Gate rejection: any implementation finding goes to the
862
+ // flow's Architect fix path, docs-only findings (alone or with test-only ones)
863
+ // to Architect first, and test-only findings to Tester.
864
+ function codeDiffCorrectionTargets(codeDiff, flow) {
865
+ const scopes = (codeDiff?.findings ?? []).map((finding) => finding.scope);
866
+ if (scopes.length === 0 || scopes.some((scope) => scope !== "docs-only" && scope !== "test-only")) {
867
+ return [flow === "code-change" ? "architect-debug/architect" : `${flow}/architect`];
868
+ }
869
+ return scopes.includes("docs-only") ? [`${flow}/architect`] : [`${flow}/tester`];
870
+ }
871
+ // After a passed code-diff Gate, PM runs Final Acceptance. Only a Final
872
+ // Acceptance produced after the latest dispatch can route a follow-up.
873
+ async function finalAcceptanceFollowUpTargets(fs, input, state, flow, targetRole, acceptanceFlow) {
874
+ const acceptance = await artifactState(fs, input, "final-acceptance.md", "final-acceptance");
875
+ if (!acceptance.complete
876
+ || !evidenceProducedAfterActiveDispatch(state, flow, targetRole, "final-acceptance.md", acceptance.hash)) {
877
+ return [];
878
+ }
879
+ if (acceptance.value === "needs-coder-follow-up" && acceptanceFlow === "code-change")
880
+ return ["code-change/coder"];
881
+ if (acceptance.value === "needs-architect-follow-up")
882
+ return [`${acceptanceFlow}/architect`];
945
883
  return [];
946
884
  }
947
885
  async function allowedAfterTester(fs, input, state, currentFlow, codeSource, options) {
@@ -962,10 +900,14 @@ async function allowedAfterTester(fs, input, state, currentFlow, codeSource, opt
962
900
  return [];
963
901
  const codeDiff = await gateState(fs, input, "code-diff");
964
902
  if (freshGateDecision(state, currentFlow, "tester", "code-diff", codeDiff) === "request_changes") {
965
- const scopes = new Set(codeDiff?.findings?.map((finding) => finding.scope).filter(Boolean));
966
- return scopes.size === 1 && scopes.has("test-only")
967
- ? [`${currentFlow}/tester`]
968
- : options.implementationFailureFlow ? [`${options.implementationFailureFlow}/architect`] : [];
903
+ return codeDiffCorrectionTargets(codeDiff, currentFlow);
904
+ }
905
+ // The docs coverage check refused to start code-diff: Architect updates the
906
+ // owning docs, then PM requests code-diff again.
907
+ if (codeDiff?.status === "failed"
908
+ && codeDiff.error?.startsWith(CODE_DIFF_DOCS_COVERAGE_ERROR_PREFIX)
909
+ && gateRecordIsFresh(state, currentFlow, "tester", "code-diff", codeDiff)) {
910
+ return [`${currentFlow}/architect`];
969
911
  }
970
912
  if (!gatePassedForDispatch(state, currentFlow, "tester", "code-diff", codeDiff))
971
913
  return [];
@@ -973,7 +915,7 @@ async function allowedAfterTester(fs, input, state, currentFlow, codeSource, opt
973
915
  const reviewedSources = codeDiff?.codeDiffSources ?? (codeDiff?.codeDiffSource ? [codeDiff.codeDiffSource] : []);
974
916
  if (codeDiff?.status === "completed" && !reviewedSources.includes(gateCodeSource))
975
917
  return [];
976
- return [options.successTarget];
918
+ return finalAcceptanceFollowUpTargets(fs, input, state, currentFlow, "tester", options.acceptanceFlow);
977
919
  }
978
920
  async function artifactState(fs, input, fileName, kind) {
979
921
  const relative = path.posix.join(input.handoffDir, fileName);
@@ -994,7 +936,7 @@ async function artifactState(fs, input, fileName, kind) {
994
936
  value = readArtifactSectionContent(content, "Final Disposition")?.trim().toLowerCase();
995
937
  if (kind === "test-report")
996
938
  value = inline("Test Result");
997
- if (kind === "docs-update-report" || kind === "docs-sync-report" || kind === "final-acceptance") {
939
+ if (kind === "docs-update-report" || kind === "final-acceptance") {
998
940
  value = readArtifactSectionContent(content, "Decision")?.trim().toLowerCase();
999
941
  }
1000
942
  const infrastructure = kind === "test-report"
@@ -1007,19 +949,13 @@ async function artifactState(fs, input, fileName, kind) {
1007
949
  const disposition = kind === "architect-debug" || kind === "architecture-diagnosis"
1008
950
  ? readArtifactSectionContent(content, "Final Disposition")?.trim().toLowerCase()
1009
951
  : undefined;
1010
- const correctionOwner = kind === "docs-sync-report"
1011
- ? readArtifactSectionContent(content, "Correction Owner")?.trim().toLowerCase()
1012
- : undefined;
1013
952
  return {
1014
953
  complete: check.status === "ok",
1015
954
  hash: await artifactFingerprint(fs, absolute, content),
1016
955
  value,
1017
956
  infrastructure,
1018
957
  hasUserApprovedCoverageGap,
1019
- disposition,
1020
- correctionOwner: correctionOwner === "none" || asTargetRole(correctionOwner)
1021
- ? correctionOwner
1022
- : undefined
958
+ disposition
1023
959
  };
1024
960
  }
1025
961
  function hasSubstantiveArtifactSection(value) {
@@ -1213,7 +1149,7 @@ async function describeBlockedCheckpoint(fs, input, state, current) {
1213
1149
  if (baseline.flow === "architecture-diagnosis" && test.value === "fail") {
1214
1150
  return "Architecture Diagnosis stopped after the current Tester failure. Record the user's decision and use one exact Workflow Override if the user authorizes another Architect repair.";
1215
1151
  }
1216
- return "The latest Tester result is waiting for its current validation-adequacy or code-diff Gate result.";
1152
+ return "The latest Tester result is waiting for its current validation-adequacy or code-diff Gate result, or, after both Gates pass, for Final Acceptance.";
1217
1153
  }
1218
1154
  if (baseline.targetRole === "architect") {
1219
1155
  return "The latest Architect dispatch has not produced the fresh workflow artifact required for its next step.";
@@ -1228,19 +1164,18 @@ async function validateCompletion(fs, input, state, candidate) {
1228
1164
  throw workflowError("WORKFLOW_COMPLETION_INVALID", "An unstarted workflow cannot be completed.");
1229
1165
  }
1230
1166
  const flowRun = resolveFlowRun(state.flowRun, candidate);
1231
- if (flowRun.activeBranch === candidate.flow) {
1232
- throw workflowError("WORKFLOW_COMPLETION_INVALID", `${candidate.flow} is an active branch and must return to ${flowRun.rootFlow} before completion.`);
1233
- }
1234
- if (candidate.flow === "code-change" || candidate.flow === "architect-debug") {
1235
- await validateCompleteDeliveryEvidence(fs, input, state, candidate.flow);
1236
- return;
1237
- }
1238
- if (candidate.flow === "architecture-diagnosis") {
1167
+ // A Debug or Diagnosis Branch returns to its parent once it passes its own
1168
+ // code-diff Gate, so the parent's Final Acceptance completes the run there.
1169
+ const inBranch = flowRun.activeBranch === candidate.flow;
1170
+ if (candidate.flow === "architecture-diagnosis" && !inBranch) {
1239
1171
  const diagnosis = await artifactState(fs, input, "architecture-diagnosis.md", "architecture-diagnosis");
1240
1172
  if (diagnosis.disposition === "analysis completed"
1241
1173
  && evidenceProducedAfterActiveDispatch(state, candidate.flow, "architect", "architecture-diagnosis.md", diagnosis.hash))
1242
1174
  return;
1243
- await validateCompleteDeliveryEvidence(fs, input, state, candidate.flow);
1175
+ }
1176
+ if (candidate.flow === "code-change" || candidate.flow === "architect-debug"
1177
+ || candidate.flow === "architecture-diagnosis") {
1178
+ await validateCompleteDeliveryEvidence(fs, input, state, { ...candidate, status: "active" }, candidate.flow, flowRun);
1244
1179
  return;
1245
1180
  }
1246
1181
  if (candidate.flow === "docs-only") {
@@ -1265,29 +1200,28 @@ async function validateCompletion(fs, input, state, candidate) {
1265
1200
  }
1266
1201
  }
1267
1202
  }
1268
- async function validateCompleteDeliveryEvidence(fs, input, state, flow) {
1203
+ async function validateCompleteDeliveryEvidence(fs, input, state, current, flow, flowRun) {
1269
1204
  const baseline = state.activeDispatch;
1270
1205
  if (!baseline || baseline.flow !== flow) {
1271
1206
  throw workflowError("WORKFLOW_COMPLETION_INVALID", `The latest confirmed dispatch does not belong to the active ${flow} flow.`);
1272
1207
  }
1273
- if (baseline.targetRole !== "architect") {
1274
- throw workflowError("WORKFLOW_COMPLETION_INVALID", `The latest ${flow} dispatch is to ${baseline.targetRole}. Complete every required downstream validation and Gate step, then route Architect for docs sync before completing the flow.`);
1275
- }
1276
- const docs = await artifactState(fs, input, "docs-sync-report.md", "docs-sync-report");
1277
- if (!docs.complete || baseline.artifactHashes["docs-sync-report.md"] === docs.hash) {
1278
- const update = await artifactState(fs, input, "docs-update-report.md", "docs-update-report");
1279
- const wrongReport = update.hash !== MISSING_EVIDENCE_HASH
1280
- && baseline.artifactHashes["docs-update-report.md"] !== update.hash;
1281
- const reason = !docs.complete
1282
- ? "Docs Sync Report is missing or malformed for this complete delivery flow."
1283
- : "Docs Sync Report was not produced after the final Architect dispatch.";
1284
- throw workflowError("WORKFLOW_COMPLETION_INVALID", `${reason} Required file: ${path.posix.join(input.handoffDir, "docs-sync-report.md")}, produced after dispatch #${baseline.sequence} (${baseline.confirmedAt}).`
1285
- + (wrongReport
1286
- ? ` ${path.posix.join(input.handoffDir, "docs-update-report.md")} was updated after this dispatch, but cannot replace docs-sync-report.md.`
1287
- : ""), "Ask Architect to submit --kind docs-sync-report through .ai/tools/vcm-artifact with Decision: synced or unchanged and Correction Owner: none. Report blocked with its correction owner when synchronization is incomplete.");
1288
- }
1289
- if (docs.value !== "synced" && docs.value !== "unchanged") {
1290
- throw workflowError("WORKFLOW_COMPLETION_INVALID", `Docs Sync Report Decision must be synced or unchanged before completion; found ${docs.value ?? "missing"}.`);
1208
+ const validated = current.history.some((entry) => entry.sequence >= flowRun.startedAtSequence
1209
+ && entry.sequence < baseline.sequence && entry.targetRole === "tester");
1210
+ if (baseline.targetRole !== "tester" && !(baseline.targetRole === "architect" && validated)) {
1211
+ throw workflowError("WORKFLOW_COMPLETION_INVALID", `The latest ${flow} dispatch is to ${baseline.targetRole}. Complete Tester validation and the validation-adequacy and code-diff Gates before completing the flow.`);
1212
+ }
1213
+ if (baseline.targetRole === "tester") {
1214
+ const test = await artifactState(fs, input, "test-report.md", "test-report");
1215
+ const validation = await gateState(fs, input, "validation-adequacy");
1216
+ if (!evidenceProducedAfterActiveDispatch(state, flow, "tester", "test-report.md", test.hash)
1217
+ || !(test.value === "pass" || (test.value === "fail" && test.hasUserApprovedCoverageGap))
1218
+ || !gatePassedForDispatch(state, flow, "tester", "validation-adequacy", validation)) {
1219
+ throw workflowError("WORKFLOW_COMPLETION_INVALID", `The latest Tester dispatch #${baseline.sequence} has no passing Test Report with a passed validation-adequacy Gate.`);
1220
+ }
1221
+ }
1222
+ const codeDiff = await gateState(fs, input, "code-diff");
1223
+ if (!gatePassedForDispatch(state, flow, baseline.targetRole, "code-diff", codeDiff)) {
1224
+ throw workflowError("WORKFLOW_COMPLETION_INVALID", `The code-diff Gate has not approved the work since the latest ${baseline.targetRole} dispatch #${baseline.sequence}.`, "Run .ai/tools/request-gate-review --gate code-diff for the current changes, then complete Final Acceptance.");
1291
1225
  }
1292
1226
  const acceptance = await artifactState(fs, input, "final-acceptance.md", "final-acceptance");
1293
1227
  if (!acceptance.complete) {
@@ -1297,7 +1231,11 @@ async function validateCompleteDeliveryEvidence(fs, input, state, flow) {
1297
1231
  throw workflowError("WORKFLOW_COMPLETION_INVALID", `Final Acceptance Decision must be accepted or accepted-with-known-risks; found ${acceptance.value ?? "missing"}.`);
1298
1232
  }
1299
1233
  if (baseline.artifactHashes["final-acceptance.md"] === acceptance.hash) {
1300
- throw workflowError("WORKFLOW_COMPLETION_INVALID", "Final Acceptance was not produced after the final Architect dispatch.");
1234
+ throw workflowError("WORKFLOW_COMPLETION_INVALID", `Final Acceptance was not produced after the latest dispatch #${baseline.sequence}.`);
1235
+ }
1236
+ const pending = await getAllowedTransitions(fs, input, state, current);
1237
+ if (pending.length > 0) {
1238
+ throw workflowError("WORKFLOW_COMPLETION_INVALID", `The ${flow} flow still requires a dispatch before completion: ${pending.join(", ")}.`);
1301
1239
  }
1302
1240
  }
1303
1241
  function evidenceProducedAfterActiveDispatch(state, flow, targetRole, artifact, currentHash) {
@@ -1,4 +1,4 @@
1
- import { ARCHITECT_DEBUG_DISPOSITIONS, ARCHITECT_DEBUG_STATUSES, ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_DIAGNOSIS_DISPOSITIONS, ARCHITECTURE_EVIDENCE_STATUSES, ARCHITECTURE_PLAN_RESULTS, CODER_COMPLETION_DECISIONS, DOCS_REPORT_DECISIONS, DOCS_UPDATE_COMMIT_RULE, DOCS_SYNC_CORRECTION_OWNERS, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, PLANNING_PROGRESS_STATUSES, STRICT_NONE_VALUE, TEST_INFRASTRUCTURE_STATUSES, TEST_RESULTS, renderArtifactOptions } from "../../shared/validation/artifact-contract.js";
1
+ import { ARCHITECT_DEBUG_DISPOSITIONS, ARCHITECT_DEBUG_STATUSES, ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_DIAGNOSIS_DISPOSITIONS, ARCHITECTURE_EVIDENCE_STATUSES, ARCHITECTURE_PLAN_RESULTS, CODER_COMPLETION_DECISIONS, DOCS_REPORT_DECISIONS, DOCS_UPDATE_COMMIT_RULE, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, PLANNING_PROGRESS_STATUSES, STRICT_NONE_VALUE, TEST_INFRASTRUCTURE_STATUSES, TEST_RESULTS, renderArtifactOptions } from "../../shared/validation/artifact-contract.js";
2
2
  const CURRENT_HANDOFF_NOTICE = "<!-- VCM current handoff: replace this file with one complete, self-contained snapshot of the current result. Restate all still-relevant evidence; do not refer to a prior revision, route message, Session, or transcript as evidence. -->";
3
3
  export function renderArchitectureBriefTemplate(taskSlug) {
4
4
  return `# Architecture Brief: ${taskSlug}
@@ -483,52 +483,6 @@ TBD
483
483
  ${renderArtifactOptions(ARCHITECTURE_DIAGNOSIS_DISPOSITIONS)}
484
484
  `;
485
485
  }
486
- export function renderDocsSyncReportTemplate(taskSlug) {
487
- return `# Docs Sync Report: ${taskSlug}
488
-
489
- ${CURRENT_HANDOFF_NOTICE}
490
-
491
- ## Summary
492
-
493
- TBD
494
-
495
- ## Architecture Drift Check
496
-
497
- TBD
498
-
499
- ## Docs Updated
500
-
501
- TBD
502
-
503
- ## Docs Reviewed And Left Unchanged
504
-
505
- TBD
506
-
507
- ## Public Contract / Module Boundary Notes
508
-
509
- TBD
510
-
511
- ## Remaining Documentation Risks
512
-
513
- TBD
514
-
515
- ## Known Issues Disposition
516
-
517
- TBD
518
-
519
- ## Correction Owner
520
-
521
- ${renderArtifactOptions(DOCS_SYNC_CORRECTION_OWNERS)}
522
-
523
- ## Correction Evidence
524
-
525
- ${STRICT_NONE_VALUE}
526
-
527
- ## Decision
528
-
529
- ${renderArtifactOptions(DOCS_REPORT_DECISIONS)}
530
- `;
531
- }
532
486
  export function renderDocsUpdateReportTemplate(taskSlug, assignmentId = "docs-only") {
533
487
  return `# Docs Update Report: ${taskSlug}
534
488
 
@@ -639,7 +593,7 @@ TBD
639
593
 
640
594
  TBD
641
595
 
642
- ## Review And Docs Sync
596
+ ## Review And Documentation
643
597
 
644
598
  TBD
645
599