vibe-coding-master 0.9.2 → 0.9.4
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 +3 -2
- package/dist/backend/adapters/command-runner.js +1 -0
- package/dist/backend/adapters/filesystem.js +5 -2
- package/dist/backend/api/artifact-routes.js +0 -3
- package/dist/backend/api/task-routes.js +0 -1
- package/dist/backend/cli/install-vcm-harness.js +6 -0
- package/dist/backend/server.js +2 -0
- package/dist/backend/services/artifact-service.js +1 -16
- package/dist/backend/services/auto-memory-service.js +101 -16
- package/dist/backend/services/claude-hook-service.js +31 -12
- package/dist/backend/services/docs-sync-check.js +32 -0
- package/dist/backend/services/gate-review-service.js +10 -2
- package/dist/backend/services/harness-feedback-service.js +21 -3
- package/dist/backend/services/lsp-plugin.js +6 -2
- package/dist/backend/services/managed-artifact-validation.js +2 -1
- package/dist/backend/services/runtime-coordinator-service.js +1 -1
- package/dist/backend/services/session-service.js +1 -1
- package/dist/backend/services/status-service.js +0 -1
- package/dist/backend/services/translation-worker-service.js +44 -6
- package/dist/backend/services/workflow-control-service.js +103 -165
- package/dist/backend/templates/handoff.js +2 -48
- package/dist/backend/templates/harness/architect-agent.js +42 -23
- package/dist/backend/templates/harness/claude-root.js +9 -9
- package/dist/backend/templates/harness/coder-agent.js +0 -1
- package/dist/backend/templates/harness/gate-review.js +55 -12
- package/dist/backend/templates/harness/project-manager-agent.js +33 -27
- package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +13 -14
- package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +2 -2
- package/dist/backend/templates/harness/vcm-workflow-review-skill.js +2 -2
- package/dist/shared/types/gate-review.js +4 -0
- package/dist/shared/types/workflow.js +0 -1
- package/dist/shared/validation/artifact-check.js +1 -27
- package/dist/shared/validation/artifact-contract.js +0 -2
- package/dist/shared/validation/artifact-registry.js +1 -18
- package/package.json +1 -1
- package/scripts/claude-plugins/vcm-lsp-bridge/vcm-lsp-proxy.mjs +5 -1
- package/scripts/harness-tools/check-docs-sync +246 -0
- package/scripts/harness-tools/check-durable-docs +24 -0
- package/scripts/harness-tools/run-long-check +3 -3
- package/scripts/verify-package.mjs +1 -0
|
@@ -22,6 +22,7 @@ const MEMORY_TOTAL_LIMIT_BYTES = 80 * 1024;
|
|
|
22
22
|
const MEMORY_INITIALIZED_MIN_FILES = 2;
|
|
23
23
|
const MEMORY_FILE_NAMES = ["glossary.md", "style-guide.md", "project-context.md", "decisions.md"];
|
|
24
24
|
const TRANSLATOR_ROLE = "translator";
|
|
25
|
+
const QUEUE_HOOK_EVENTS = new Set(["UserPromptSubmit", "Stop", "StopFailure"]);
|
|
25
26
|
const FILE_BROWSER_DEFAULT_LIMIT = 200;
|
|
26
27
|
const FILE_BROWSER_MAX_LIMIT = 500;
|
|
27
28
|
const FILE_BROWSER_SEARCH_MAX_DEPTH = 6;
|
|
@@ -99,6 +100,26 @@ const BINARY_LIKE_EXTENSIONS = new Set([
|
|
|
99
100
|
export function createTranslationWorkerService(deps) {
|
|
100
101
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
101
102
|
const createId = deps.id ?? (() => randomUUID());
|
|
103
|
+
const queueLocks = new Map();
|
|
104
|
+
// Every queue and index access happens in this process. Serializing them per
|
|
105
|
+
// repository keeps concurrent load-modify-save cycles from dropping each
|
|
106
|
+
// other's updates and keeps readers out of an in-flight rename.
|
|
107
|
+
function withQueueLock(repoRoot, operation) {
|
|
108
|
+
const key = path.resolve(repoRoot);
|
|
109
|
+
const previous = queueLocks.get(key) ?? Promise.resolve();
|
|
110
|
+
const result = previous.then(operation);
|
|
111
|
+
const settled = result.then(() => undefined, () => undefined);
|
|
112
|
+
queueLocks.set(key, settled);
|
|
113
|
+
void settled.then(() => {
|
|
114
|
+
if (queueLocks.get(key) === settled) {
|
|
115
|
+
queueLocks.delete(key);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
function scheduleDispatch(repoRoot) {
|
|
121
|
+
void withQueueLock(repoRoot, () => dispatchNext(repoRoot)).catch(() => undefined);
|
|
122
|
+
}
|
|
102
123
|
async function ensureLayout(repoRoot) {
|
|
103
124
|
await Promise.all([
|
|
104
125
|
deps.fs.ensureDir(resolveRepoPath(repoRoot, MEMORY_DIR)),
|
|
@@ -917,7 +938,7 @@ export function createTranslationWorkerService(deps) {
|
|
|
917
938
|
queue.updatedAt = now();
|
|
918
939
|
await saveQueue(repoRoot, queue);
|
|
919
940
|
}
|
|
920
|
-
|
|
941
|
+
const service = {
|
|
921
942
|
async cleanupStartupRuntime(repoRoot) {
|
|
922
943
|
await removeRepoPath(repoRoot, TRANSLATIONS_RUNTIME_DIR, true);
|
|
923
944
|
await cleanupStartupFileIndex(repoRoot);
|
|
@@ -1006,7 +1027,7 @@ export function createTranslationWorkerService(deps) {
|
|
|
1006
1027
|
const activeExisting = (await loadRuntimeFileJobs(repoRoot)).find((job) => fileTranslationReplacementKey(job) === replacementKey &&
|
|
1007
1028
|
isActiveFileTranslationJobStatus(job.status));
|
|
1008
1029
|
if (activeExisting) {
|
|
1009
|
-
|
|
1030
|
+
scheduleDispatch(repoRoot);
|
|
1010
1031
|
return activeExisting;
|
|
1011
1032
|
}
|
|
1012
1033
|
const timestamp = now();
|
|
@@ -1106,7 +1127,7 @@ export function createTranslationWorkerService(deps) {
|
|
|
1106
1127
|
lastUpdatedAt: timestamp
|
|
1107
1128
|
});
|
|
1108
1129
|
await deps.fs.writeText(resolveRepoPath(repoRoot, job.reportPath), `# Translation Report\n\nStatus: queued\nJob: ${jobId}\n`);
|
|
1109
|
-
|
|
1130
|
+
scheduleDispatch(repoRoot);
|
|
1110
1131
|
return job;
|
|
1111
1132
|
},
|
|
1112
1133
|
async createBootstrapRun(repoRoot, input) {
|
|
@@ -1165,7 +1186,7 @@ export function createTranslationWorkerService(deps) {
|
|
|
1165
1186
|
index.runs = [run, ...index.runs];
|
|
1166
1187
|
index.updatedAt = timestamp;
|
|
1167
1188
|
await saveBootstrapIndex(repoRoot, index);
|
|
1168
|
-
|
|
1189
|
+
scheduleDispatch(repoRoot);
|
|
1169
1190
|
return run;
|
|
1170
1191
|
},
|
|
1171
1192
|
async createMemoryUpdate(repoRoot, input) {
|
|
@@ -1223,7 +1244,7 @@ export function createTranslationWorkerService(deps) {
|
|
|
1223
1244
|
"Delete or merge stale, duplicate, temporary, and task-local content."
|
|
1224
1245
|
]
|
|
1225
1246
|
});
|
|
1226
|
-
|
|
1247
|
+
scheduleDispatch(repoRoot);
|
|
1227
1248
|
return queueItem;
|
|
1228
1249
|
},
|
|
1229
1250
|
async readFileJobOutput(repoRoot, jobId) {
|
|
@@ -1297,7 +1318,7 @@ export function createTranslationWorkerService(deps) {
|
|
|
1297
1318
|
});
|
|
1298
1319
|
job.queueItemId = queueItem.id;
|
|
1299
1320
|
if (!input.deferDispatch) {
|
|
1300
|
-
|
|
1321
|
+
scheduleDispatch(repoRoot);
|
|
1301
1322
|
}
|
|
1302
1323
|
return job;
|
|
1303
1324
|
},
|
|
@@ -1410,6 +1431,23 @@ export function createTranslationWorkerService(deps) {
|
|
|
1410
1431
|
});
|
|
1411
1432
|
}
|
|
1412
1433
|
};
|
|
1434
|
+
return {
|
|
1435
|
+
...service,
|
|
1436
|
+
cleanupStartupRuntime: (repoRoot) => withQueueLock(repoRoot, () => service.cleanupStartupRuntime(repoRoot)),
|
|
1437
|
+
getState: (repoRoot, options) => withQueueLock(repoRoot, () => service.getState(repoRoot, options)),
|
|
1438
|
+
createFileJob: (repoRoot, input) => withQueueLock(repoRoot, () => service.createFileJob(repoRoot, input)),
|
|
1439
|
+
readFileJobOutput: (repoRoot, jobId) => withQueueLock(repoRoot, () => service.readFileJobOutput(repoRoot, jobId)),
|
|
1440
|
+
createBootstrapRun: (repoRoot, input) => withQueueLock(repoRoot, () => service.createBootstrapRun(repoRoot, input)),
|
|
1441
|
+
createMemoryUpdate: (repoRoot, input) => withQueueLock(repoRoot, () => service.createMemoryUpdate(repoRoot, input)),
|
|
1442
|
+
createConversationJob: (repoRoot, input) => withQueueLock(repoRoot, () => service.createConversationJob(repoRoot, input)),
|
|
1443
|
+
validateConversationResult: (repoRoot, input) => withQueueLock(repoRoot, () => service.validateConversationResult(repoRoot, input)),
|
|
1444
|
+
promoteFileJob: (repoRoot, jobId, targetPath) => withQueueLock(repoRoot, () => service.promoteFileJob(repoRoot, jobId, targetPath)),
|
|
1445
|
+
// Only the events that touch the queue wait for the lock, so a SessionStart
|
|
1446
|
+
// hook fired while a dispatch is starting the Translator never waits on it.
|
|
1447
|
+
handleTranslatorHook: (repoRoot, eventName, taskSlug) => QUEUE_HOOK_EVENTS.has(eventName)
|
|
1448
|
+
? withQueueLock(repoRoot, () => service.handleTranslatorHook(repoRoot, eventName, taskSlug))
|
|
1449
|
+
: service.handleTranslatorHook(repoRoot, eventName, taskSlug)
|
|
1450
|
+
};
|
|
1413
1451
|
}
|
|
1414
1452
|
function requireTranslationTaskSlug(value) {
|
|
1415
1453
|
const taskSlug = value?.trim();
|
|
@@ -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
|
|
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", "
|
|
735
|
-
|| !gatePassedForDispatch(state, effectiveFlow, "tester", "code-diff", codeDiff))
|
|
705
|
+
if (!gatePassedForDispatch(state, effectiveFlow, "tester", "code-diff", codeDiff))
|
|
736
706
|
return false;
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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 (
|
|
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
|
-
|
|
854
|
-
|
|
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
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
const
|
|
930
|
-
if (
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
if (
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
if (
|
|
944
|
-
return ["
|
|
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
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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
|
|
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 === "
|
|
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
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
if (candidate.flow === "
|
|
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
|
-
|
|
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
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
const
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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",
|
|
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,
|
|
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
|
|
596
|
+
## Review And Documentation
|
|
643
597
|
|
|
644
598
|
TBD
|
|
645
599
|
|