vibe-coding-master 0.7.42 → 0.7.44

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 (50) hide show
  1. package/README.md +49 -17
  2. package/dist/backend/api/artifact-routes.js +3 -0
  3. package/dist/backend/api/harness-routes.js +16 -0
  4. package/dist/backend/api/task-routes.js +32 -2
  5. package/dist/backend/api/workflow-control-routes.js +7 -26
  6. package/dist/backend/cli/install-vcm-harness.js +61 -6
  7. package/dist/backend/role-tool-policy.js +1 -1
  8. package/dist/backend/server.js +14 -5
  9. package/dist/backend/services/artifact-service.js +10 -32
  10. package/dist/backend/services/auto-memory-service.js +642 -14
  11. package/dist/backend/services/claude-hook-service.js +183 -6
  12. package/dist/backend/services/gate-review-service.js +173 -74
  13. package/dist/backend/services/harness-feedback-service.js +180 -80
  14. package/dist/backend/services/harness-service.js +65 -9
  15. package/dist/backend/services/memory-review-paths.js +13 -0
  16. package/dist/backend/services/role-stall-detector-service.js +322 -0
  17. package/dist/backend/services/round-service.js +25 -0
  18. package/dist/backend/services/runtime-coordinator-service.js +12 -1
  19. package/dist/backend/services/session-service.js +70 -3
  20. package/dist/backend/services/status-service.js +1 -0
  21. package/dist/backend/services/translation-worker-service.js +19 -4
  22. package/dist/backend/services/workflow-control-service.js +524 -204
  23. package/dist/backend/templates/handoff.js +46 -5
  24. package/dist/backend/templates/harness/architect-agent.js +12 -6
  25. package/dist/backend/templates/harness/architect-scaffold-worker-agent.js +1 -1
  26. package/dist/backend/templates/harness/check-scaffold-ledger.js +234 -10
  27. package/dist/backend/templates/harness/claude-root.js +3 -2
  28. package/dist/backend/templates/harness/coder-agent.js +12 -5
  29. package/dist/backend/templates/harness/gate-review.js +150 -57
  30. package/dist/backend/templates/harness/harness-engineer-agent.js +38 -13
  31. package/dist/backend/templates/harness/project-manager-agent.js +18 -12
  32. package/dist/backend/templates/harness/resolve-durable-doc-assignment.js +60 -0
  33. package/dist/backend/templates/harness/tester-agent.js +13 -0
  34. package/dist/backend/templates/harness/vcm-ask-user-skill.js +82 -0
  35. package/dist/backend/templates/harness/vcm-code-navigation-skill.js +7 -6
  36. package/dist/backend/templates/harness/vcm-task-state-skill.js +2 -2
  37. package/dist/backend/templates/harness/vcm-workflow-review-skill.js +7 -9
  38. package/dist/shared/types/role-stall.js +1 -0
  39. package/dist/shared/types/workflow.js +15 -0
  40. package/dist/shared/validation/artifact-check.js +3 -3
  41. package/dist/shared/validation/artifact-contract.js +1 -1
  42. package/dist/shared/validation/artifact-registry.js +17 -1
  43. package/dist-frontend/assets/{index-C_XHGNBD.css → index-B0d4Z6ny.css} +1 -1
  44. package/dist-frontend/assets/index-BvCmrFlN.js +97 -0
  45. package/dist-frontend/index.html +2 -2
  46. package/package.json +1 -1
  47. package/scripts/claude-plugins/vcm-lsp-bridge/.claude-plugin/plugin.json +21 -6
  48. package/scripts/harness-tools/vcm-artifact +1 -2
  49. package/scripts/harness-tools/vcm-bash-guard +204 -14
  50. package/dist-frontend/assets/index-Bocc2DWF.js +0 -97
@@ -2,16 +2,26 @@ 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 { WORKFLOW_FLOWS } from "../../shared/types/workflow.js";
5
+ import { WORKFLOW_EVIDENCE_ARTIFACTS, WORKFLOW_EVIDENCE_GATES, WORKFLOW_FLOWS } from "../../shared/types/workflow.js";
6
6
  import { checkMarkdownArtifact, readArtifactSectionContent } from "../../shared/validation/artifact-check.js";
7
7
  import { renderWorkflowProgressTemplate } from "../templates/handoff.js";
8
8
  const HISTORY_HEADER = "| Sequence | Flow | Target Role | Evidence | Override Authorization | Confirmed At |";
9
9
  const HISTORY_SEPARATOR = "| --- | --- | --- | --- | --- | --- |";
10
10
  const TARGET_ROLES = new Set(["architect", "coder", "tester"]);
11
11
  const FINAL_GATE_STATUSES = new Set(["disabled", "not_required", "skipped", "overridden"]);
12
+ const MISSING_EVIDENCE_HASH = "<missing>";
13
+ const DOCS_ONLY_ROLE_TRANSITIONS = [
14
+ "docs-only/architect",
15
+ "docs-only/coder",
16
+ "docs-only/tester"
17
+ ];
18
+ const DOCS_ONLY_EXIT_TRANSITIONS = [
19
+ "code-change/architect",
20
+ "validation-only/tester"
21
+ ];
12
22
  export function createWorkflowControlService(deps) {
13
23
  const now = deps.now ?? (() => new Date().toISOString());
14
- const id = deps.id ?? (() => `wfovr_${randomUUID()}`);
24
+ const id = deps.id ?? (() => `wfauth_${randomUUID()}`);
15
25
  const locks = new Map();
16
26
  async function getState(input) {
17
27
  try {
@@ -34,6 +44,7 @@ export function createWorkflowControlService(deps) {
34
44
  return withLock(statePath(input), async () => {
35
45
  const state = await getState(input);
36
46
  failOnStateWarnings(state);
47
+ failWhileAwaitingUser(state);
37
48
  if (state.pendingDispatch) {
38
49
  throw workflowError("WORKFLOW_DISPATCH_PENDING", `A ${state.pendingDispatch.targetRole} dispatch is already ${state.pendingDispatch.status}.`, "Complete or recover the existing dispatch before proposing another workflow transition.");
39
50
  }
@@ -44,49 +55,38 @@ export function createWorkflowControlService(deps) {
44
55
  if (candidate.status !== "completed") {
45
56
  throw workflowError("WORKFLOW_PROPOSAL_REQUIRED", "Workflow Progress must propose one role dispatch or mark the active flow completed.", "Set Proposed Dispatch to a target role, or submit Status: completed after the flow's completion evidence exists.");
46
57
  }
47
- await validateCompletion(deps.fs, input, candidate);
58
+ await validateCompletion(deps.fs, input, state, candidate);
48
59
  const normalized = renderWorkflowProgress(candidate);
49
60
  await writeAtomic(deps.fs, progressPath(input), normalized);
50
61
  return { path: relativeProgressPath(input), content: normalized };
51
62
  }
63
+ if (current.status === "completed" && !candidate.proposal.requestedFlow) {
64
+ throw workflowError("WORKFLOW_FLOW_REQUIRED", "Requested Flow is required when starting another flow after completion.", "Select the next flow explicitly before dispatching its first role.");
65
+ }
52
66
  const baseHistoryHash = historyHash(current.history);
53
67
  const effectiveFlow = resolveEffectiveFlow(current.flow, candidate.proposal.requestedFlow);
54
- const verdict = await evaluateTransition(deps.fs, input, current, effectiveFlow, candidate.proposal.targetRole);
68
+ const verdict = await evaluateTransition(deps.fs, input, state, current, effectiveFlow, candidate.proposal.targetRole);
55
69
  let overrideAuthorizationId;
70
+ let userAuthorization;
56
71
  if (!verdict.allowed) {
57
- const authorizationId = candidate.proposal.authorizationId;
58
- if (authorizationId === "request") {
59
- const existingPending = state.overrideRequests.find((entry) => entry.status === "pending");
60
- if (existingPending) {
61
- throw workflowError("WORKFLOW_OVERRIDE_PENDING", `Workflow override ${existingPending.id} is already waiting for the user's decision.`, "Wait for the current user decision before requesting another workflow override.");
62
- }
63
- const quote = candidate.proposal.authorizationQuote?.trim();
64
- const violatedRule = candidate.proposal.violatedRule?.trim();
65
- if (!quote || !violatedRule || violatedRule !== verdict.reason) {
66
- throw workflowError("WORKFLOW_OVERRIDE_REQUEST_INVALID", `Override request must include the proposed user authorization and the exact violated rule: ${verdict.reason}`, "Copy the rejection reason exactly into Violated Rule and record the user's proposed authorization text.");
67
- }
68
- const pending = createOverrideRequest(id(), current, candidate, effectiveFlow, baseHistoryHash, verdict.reason, quote, now());
69
- const next = {
70
- ...state,
71
- overrideRequests: [...state.overrideRequests, pending],
72
- updatedAt: now()
73
- };
74
- await saveState(input, next);
75
- throw workflowError("WORKFLOW_OVERRIDE_PENDING", `Workflow override ${pending.id} requires direct user confirmation in VCM.`, "Wait for the user's decision. If approved, resubmit with the returned Authorization ID and exact authorization text.");
72
+ const authorizationText = candidate.proposal.authorizationText?.trim();
73
+ const violatedRule = candidate.proposal.violatedRule?.trim();
74
+ if (!authorizationText && !violatedRule) {
75
+ throw workflowError("WORKFLOW_TRANSITION_DENIED", verdict.reason, verdict.allowedTransitions.length > 0
76
+ ? `Allowed next dispatches: ${verdict.allowedTransitions.join(", ")}. Recheck the flow, or ask the user directly for an exact one-time authorization.`
77
+ : verdict.blockedHint);
76
78
  }
77
- if (authorizationId && authorizationId !== "none") {
78
- const override = state.overrideRequests.find((entry) => entry.id === authorizationId);
79
- validateApprovedOverride(override, current, candidate, effectiveFlow, baseHistoryHash, verdict.reason);
80
- overrideAuthorizationId = override.id;
79
+ if (!authorizationText || violatedRule !== verdict.reason) {
80
+ throw workflowError("WORKFLOW_USER_AUTHORIZATION_INVALID", `User authorization must include the exact authorization text and exact violated rule: ${verdict.reason}`, "Ask the user directly, then copy the user's authorization verbatim into Authorization Text and the rejection reason verbatim into Violated Rule.");
81
81
  }
82
- else {
83
- throw workflowError("WORKFLOW_TRANSITION_DENIED", verdict.reason, verdict.allowedTransitions.length > 0
84
- ? `Allowed next dispatches: ${verdict.allowedTransitions.join(", ")}. Recheck the flow, or request an exact one-time user override.`
85
- : "No role dispatch is legal at this checkpoint. Complete the required Gate, user decision, or PM-only step first.");
82
+ if (state.userAuthorizations.some((entry) => entry.authorizationText === authorizationText)) {
83
+ throw workflowError("WORKFLOW_USER_AUTHORIZATION_REUSED", "This user authorization has already been recorded for another workflow transition.", "Ask the user for a new explicit authorization for this exact transition.");
86
84
  }
85
+ userAuthorization = createUserAuthorization(id(), current, candidate, effectiveFlow, baseHistoryHash, verdict.reason, authorizationText, now());
86
+ overrideAuthorizationId = userAuthorization.id;
87
87
  }
88
- else if (candidate.proposal.authorizationId && candidate.proposal.authorizationId !== "none") {
89
- throw workflowError("WORKFLOW_OVERRIDE_NOT_REQUIRED", "This workflow transition is legal and must not consume a user override.", "Set every User Override field to none.");
88
+ else if (candidate.proposal.authorizationText || candidate.proposal.violatedRule) {
89
+ throw workflowError("WORKFLOW_USER_AUTHORIZATION_NOT_REQUIRED", "This workflow transition is legal and must not consume user authorization.", "Set every User Authorization field to none.");
90
90
  }
91
91
  const timestamp = now();
92
92
  const pendingDispatch = {
@@ -104,20 +104,16 @@ export function createWorkflowControlService(deps) {
104
104
  };
105
105
  const accepted = {
106
106
  ...candidate,
107
- proposal: {
108
- ...candidate.proposal,
109
- authorizationId: overrideAuthorizationId,
110
- authorizationQuote: overrideAuthorizationId
111
- ? state.overrideRequests.find((entry) => entry.id === overrideAuthorizationId)?.authorizationText
112
- : undefined,
113
- violatedRule: overrideAuthorizationId ? verdict.reason : undefined
114
- }
107
+ proposal: candidate.proposal
115
108
  };
116
109
  const normalized = renderWorkflowProgress(accepted);
117
110
  await writeAtomic(deps.fs, progressPath(input), normalized);
118
111
  await saveState(input, {
119
112
  ...state,
120
113
  pendingDispatch,
114
+ userAuthorizations: userAuthorization
115
+ ? [...state.userAuthorizations, userAuthorization]
116
+ : state.userAuthorizations,
121
117
  updatedAt: timestamp
122
118
  });
123
119
  return { path: relativeProgressPath(input), content: normalized };
@@ -126,6 +122,7 @@ export function createWorkflowControlService(deps) {
126
122
  async function assertRouteAuthorized(input) {
127
123
  const state = await getState(input);
128
124
  failOnStateWarnings(state);
125
+ failWhileAwaitingUser(state);
129
126
  const pending = state.pendingDispatch;
130
127
  if (!pending || pending.status !== "pending") {
131
128
  throw workflowError("WORKFLOW_ROUTE_NOT_APPROVED", "Project Manager has no pending workflow approval for this route.", "Submit a valid workflow-progress.md transition before the PM route message.");
@@ -179,6 +176,7 @@ export function createWorkflowControlService(deps) {
179
176
  await withLock(statePath(input), async () => {
180
177
  const state = await getState(input);
181
178
  failOnStateWarnings(state);
179
+ failWhileAwaitingUser(state);
182
180
  const pending = state.pendingDispatch;
183
181
  if (!pending || pending.status !== "dispatching" || pending.messageId !== messageId) {
184
182
  throw workflowError("WORKFLOW_DISPATCH_CONFIRMATION_MISMATCH", `Message ${messageId} does not own the pending workflow dispatch.`, "Do not advance Workflow Progress from an unrelated UserPromptSubmit event.");
@@ -196,6 +194,8 @@ export function createWorkflowControlService(deps) {
196
194
  overrideAuthorizationId: pending.overrideAuthorizationId,
197
195
  confirmedAt: timestamp
198
196
  };
197
+ const activeDispatch = await captureEvidenceBaseline(deps.fs, input, entry.sequence, entry.flow, entry.targetRole, timestamp);
198
+ const flowRun = await advanceFlowRun(deps.fs, input, state, current, pending.effectiveFlow, Boolean(pending.overrideAuthorizationId), entry.sequence);
199
199
  const completed = {
200
200
  ...current,
201
201
  flow: pending.effectiveFlow,
@@ -203,63 +203,66 @@ export function createWorkflowControlService(deps) {
203
203
  history: [...current.history, entry],
204
204
  proposal: undefined
205
205
  };
206
- const overrideRequests = state.overrideRequests.map((entry) => entry.id === pending.overrideAuthorizationId
206
+ const userAuthorizations = state.userAuthorizations.map((entry) => entry.id === pending.overrideAuthorizationId
207
207
  ? { ...entry, status: "consumed", consumedAt: timestamp }
208
208
  : entry);
209
209
  await writeAtomic(deps.fs, progressPath(input), renderWorkflowProgress(completed));
210
210
  await saveState(input, {
211
211
  ...state,
212
212
  pendingDispatch: null,
213
- overrideRequests,
213
+ activeDispatch,
214
+ flowRun,
215
+ userAuthorizations,
214
216
  updatedAt: timestamp
215
217
  });
216
218
  });
217
219
  }
218
- async function approveOverride(input, overrideId, authorizationText) {
219
- return decideOverride(input, overrideId, "approved", authorizationText);
220
- }
221
- async function rejectOverride(input, overrideId) {
222
- return decideOverride(input, overrideId, "rejected");
223
- }
224
- async function decideOverride(input, overrideId, decision, authorizationText) {
220
+ return {
221
+ getState,
222
+ getProgress: (input) => readProgress(deps.fs, input),
223
+ requestUserInput,
224
+ resolveUserInput,
225
+ submitProgress,
226
+ assertRouteAuthorized,
227
+ claimDispatch,
228
+ releaseDispatch,
229
+ confirmDispatch
230
+ };
231
+ async function requestUserInput(input, question) {
225
232
  return withLock(statePath(input), async () => {
226
233
  const state = await getState(input);
227
- failOnStateWarnings(state);
228
- const existing = state.overrideRequests.find((entry) => entry.id === overrideId);
229
- if (!existing || existing.status !== "pending") {
230
- throw workflowError("WORKFLOW_OVERRIDE_NOT_PENDING", `Workflow override ${overrideId} is not pending.`, "Refresh the task state and act only on the current pending override.");
231
- }
232
- const normalizedAuthorization = authorizationText?.trim();
233
- if (decision === "approved" && !normalizedAuthorization) {
234
- throw workflowError("WORKFLOW_OVERRIDE_AUTHORIZATION_REQUIRED", "Direct user authorization text is required.", "Describe the exact one-time workflow exception being authorized.");
234
+ const normalizedQuestion = question.trim();
235
+ if (!normalizedQuestion) {
236
+ throw workflowError("WORKFLOW_USER_QUESTION_REQUIRED", "A non-empty user question is required.");
235
237
  }
236
238
  const timestamp = now();
237
239
  const next = {
238
240
  ...state,
239
- overrideRequests: state.overrideRequests.map((entry) => entry.id === overrideId
240
- ? {
241
- ...entry,
242
- status: decision,
243
- authorizationText: decision === "approved" ? normalizedAuthorization : undefined,
244
- decidedAt: timestamp
245
- }
246
- : entry),
241
+ awaitingUser: {
242
+ question: normalizedQuestion,
243
+ requestedAt: timestamp
244
+ },
245
+ pendingDispatch: null,
247
246
  updatedAt: timestamp
248
247
  };
249
248
  await saveState(input, next);
250
249
  return next;
251
250
  });
252
251
  }
253
- return {
254
- getState,
255
- submitProgress,
256
- assertRouteAuthorized,
257
- claimDispatch,
258
- releaseDispatch,
259
- confirmDispatch,
260
- approveOverride,
261
- rejectOverride
262
- };
252
+ async function resolveUserInput(input) {
253
+ return withLock(statePath(input), async () => {
254
+ const state = await getState(input);
255
+ if (!state.awaitingUser)
256
+ return state;
257
+ const next = {
258
+ ...state,
259
+ awaitingUser: null,
260
+ updatedAt: now()
261
+ };
262
+ await saveState(input, next);
263
+ return next;
264
+ });
265
+ }
263
266
  async function saveState(input, state) {
264
267
  await deps.fs.writeJsonAtomic(statePath(input), state);
265
268
  }
@@ -315,26 +318,24 @@ export function parseWorkflowProgress(content, expectedTaskSlug) {
315
318
  else if (requestedFlowValue !== "none" || evidence !== "none") {
316
319
  errors.push("A completed/no-dispatch proposal must use Requested Flow, Target Role, and Evidence value none.");
317
320
  }
318
- const overrideSection = readArtifactSectionContent(content, "User Override") ?? "";
319
- const authorizationId = rawField(overrideSection, "Authorization ID");
320
- const authorizationQuote = rawField(overrideSection, "Authorization Quote");
321
- const violatedRule = rawField(overrideSection, "Violated Rule");
322
- if (!authorizationId || !authorizationQuote || !violatedRule) {
323
- errors.push("User Override requires Authorization ID, Authorization Quote, and Violated Rule fields.");
321
+ const authorizationSection = readArtifactSectionContent(content, "User Authorization") ?? "";
322
+ const authorizationText = rawField(authorizationSection, "Authorization Text");
323
+ const violatedRule = rawField(authorizationSection, "Violated Rule");
324
+ if (!authorizationText || !violatedRule) {
325
+ errors.push("User Authorization requires Authorization Text and Violated Rule fields.");
324
326
  }
325
327
  else if (proposal) {
326
- const allNone = authorizationId === "none" && authorizationQuote === "none" && violatedRule === "none";
327
- const allSet = authorizationId !== "none" && authorizationQuote !== "none" && violatedRule !== "none";
328
+ const allNone = authorizationText === "none" && violatedRule === "none";
329
+ const allSet = authorizationText !== "none" && violatedRule !== "none";
328
330
  if (!allNone && !allSet)
329
- errors.push("User Override fields must all be none or all contain the exact override data.");
331
+ errors.push("User Authorization fields must both be none or both contain exact authorization evidence.");
330
332
  if (allSet) {
331
- proposal.authorizationId = authorizationId;
332
- proposal.authorizationQuote = authorizationQuote;
333
+ proposal.authorizationText = authorizationText;
333
334
  proposal.violatedRule = violatedRule;
334
335
  }
335
336
  }
336
- else if (authorizationId !== "none" || authorizationQuote !== "none" || violatedRule !== "none") {
337
- errors.push("User Override fields must be none when no role dispatch is proposed.");
337
+ else if (authorizationText !== "none" || violatedRule !== "none") {
338
+ errors.push("User Authorization fields must be none when no role dispatch is proposed.");
338
339
  }
339
340
  if (errors.length > 0)
340
341
  throw progressValidationError(errors);
@@ -372,165 +373,215 @@ Requested Flow: ${proposal?.requestedFlow ?? "none"}
372
373
  Target Role: ${proposal?.targetRole ?? "none"}
373
374
  Evidence: ${proposal?.evidence ?? "none"}
374
375
 
375
- ## User Override
376
+ ## User Authorization
376
377
 
377
- Authorization ID: ${proposal?.authorizationId ?? "none"}
378
- Authorization Quote: ${proposal?.authorizationQuote ?? "none"}
378
+ Authorization Text: ${proposal?.authorizationText ?? "none"}
379
379
  Violated Rule: ${proposal?.violatedRule ?? "none"}
380
380
  `;
381
381
  }
382
- async function evaluateTransition(fs, input, current, effectiveFlow, targetRole) {
383
- const allowedTransitions = await getAllowedTransitions(fs, input, current);
382
+ async function evaluateTransition(fs, input, state, current, effectiveFlow, targetRole) {
383
+ const allowedTransitions = await getAllowedTransitions(fs, input, state, current);
384
384
  const signature = `${effectiveFlow}/${targetRole}`;
385
- if (allowedTransitions.includes(signature))
386
- return { allowed: true, reason: "allowed", allowedTransitions };
385
+ if (allowedTransitions.includes(signature)) {
386
+ return { allowed: true, reason: "allowed", allowedTransitions, blockedHint: "" };
387
+ }
387
388
  return {
388
389
  allowed: false,
389
390
  reason: `Transition ${signature} is not legal after the confirmed Workflow Progress history.`,
390
- allowedTransitions
391
+ allowedTransitions,
392
+ blockedHint: await describeBlockedCheckpoint(fs, input, state, current)
391
393
  };
392
394
  }
393
- async function getAllowedTransitions(fs, input, current) {
395
+ async function getAllowedTransitions(fs, input, state, current) {
394
396
  if (current.status === "completed")
395
- return [];
397
+ return initialTransitions();
396
398
  if (!current.flow || current.history.length === 0) {
397
- return [
398
- "code-change/architect",
399
- "architect-debug/architect",
400
- "architecture-diagnosis/architect",
401
- "docs-only/architect",
402
- "validation-only/tester"
403
- ];
399
+ return initialTransitions();
404
400
  }
405
401
  const flow = current.flow;
402
+ const flowRun = resolveFlowRun(state.flowRun, current);
406
403
  if (flow === "docs-only") {
407
- const docs = await artifactState(fs, input, "docs-sync-report.md", "docs-sync-report");
408
- return docs.value === "synced" || docs.value === "unchanged"
409
- ? []
410
- : ["docs-only/architect", "code-change/architect", "validation-only/tester"];
404
+ const docs = await artifactState(fs, input, "docs-update-report.md", "docs-update-report");
405
+ const active = state.activeDispatch;
406
+ if (!active || active.flow !== "docs-only") {
407
+ return [...DOCS_ONLY_ROLE_TRANSITIONS, ...DOCS_ONLY_EXIT_TRANSITIONS];
408
+ }
409
+ const hasFreshResult = evidenceProducedAfterActiveDispatch(state, flow, active.targetRole, "docs-update-report.md", docs.hash);
410
+ return hasFreshResult && docs.complete
411
+ ? [...DOCS_ONLY_ROLE_TRANSITIONS, ...DOCS_ONLY_EXIT_TRANSITIONS]
412
+ : [`docs-only/${active.targetRole}`, ...DOCS_ONLY_EXIT_TRANSITIONS];
411
413
  }
412
414
  if (flow === "validation-only") {
413
415
  const test = await artifactState(fs, input, "test-report.md", "test-report");
416
+ if (!evidenceIsFresh(state, flow, "tester", "test-report.md", test.hash))
417
+ return ["validation-only/tester"];
414
418
  if (test.infrastructure === "production-change-required")
415
419
  return ["code-change/architect"];
416
420
  if (test.value === "incomplete" || test.infrastructure === "repair-required")
417
421
  return ["validation-only/tester"];
418
422
  const validationGate = await gateState(fs, input, "validation-adequacy");
419
- if (validationGate?.decision === "request_changes")
423
+ if (freshGateDecision(state, flow, "tester", "validation-adequacy", validationGate) === "request_changes") {
420
424
  return ["validation-only/tester"];
421
- if (!gatePassed(validationGate))
425
+ }
426
+ if (!gatePassedForDispatch(state, flow, "tester", "validation-adequacy", validationGate))
422
427
  return [];
423
428
  return [];
424
429
  }
425
- const segment = current.history.slice(findLastIndex(current.history, (entry) => entry.flow !== flow) + 1);
426
- if (flow === "code-change")
427
- return allowedCodeChange(fs, input, current.history);
428
- if (flow === "architect-debug")
429
- return allowedArchitectFix(fs, input, current, segment, "architect-debug");
430
- return allowedArchitectFix(fs, input, current, segment, "architecture-diagnosis");
430
+ const segment = activeFlowSegment(current.history, flow, flowRun.startedAtSequence);
431
+ if (flow === "code-change") {
432
+ return allowedCodeChange(fs, input, state, segment, flowRun.resumedFromBranch !== undefined);
433
+ }
434
+ if (flow === "architect-debug") {
435
+ return allowedArchitectFix(fs, input, state, segment, "architect-debug", flowRun.resumedFromBranch !== undefined, flowRun.activeBranch ? flowRun.rootFlow : undefined);
436
+ }
437
+ return allowedArchitectFix(fs, input, state, segment, "architecture-diagnosis", false, flowRun.activeBranch ? flowRun.rootFlow : undefined);
438
+ }
439
+ function initialTransitions() {
440
+ return [
441
+ "code-change/architect",
442
+ "architect-debug/architect",
443
+ "architecture-diagnosis/architect",
444
+ ...DOCS_ONLY_ROLE_TRANSITIONS,
445
+ "validation-only/tester"
446
+ ];
431
447
  }
432
- async function allowedCodeChange(fs, input, segment) {
448
+ async function allowedCodeChange(fs, input, state, segment, resumedFromBranch) {
433
449
  const coderIndex = findLastIndex(segment, (entry) => entry.targetRole === "coder");
434
450
  const testerIndex = findLastIndex(segment, (entry) => entry.targetRole === "tester");
451
+ if (resumedFromBranch && coderIndex < 0 && testerIndex < 0) {
452
+ return allowedPostImplementationArchitect(fs, input, state, "code-change", segment);
453
+ }
435
454
  if (coderIndex < 0) {
436
455
  const plan = await artifactState(fs, input, "architecture-plan.md", "architecture-plan");
437
- const gate = await gateState(fs, input, "architecture-plan");
438
- if (plan.value !== "complete")
456
+ if (!evidenceIsFresh(state, "code-change", "architect", "architecture-plan.md", plan.hash)
457
+ || plan.value !== "complete")
439
458
  return ["code-change/architect"];
440
- if (gate?.decision === "request_changes")
459
+ const gate = await gateState(fs, input, "architecture-plan");
460
+ if (freshGateDecision(state, "code-change", "architect", "architecture-plan", gate) === "request_changes") {
441
461
  return ["code-change/architect"];
442
- return gatePassed(gate) ? ["code-change/coder"] : [];
462
+ }
463
+ return gatePassedForDispatch(state, "code-change", "architect", "architecture-plan", gate)
464
+ ? ["code-change/coder"]
465
+ : [];
443
466
  }
444
467
  if (testerIndex < coderIndex) {
445
468
  const coder = await artifactState(fs, input, "coder-completion.md", "coder-completion");
469
+ if (!evidenceIsFresh(state, "code-change", "coder", "coder-completion.md", coder.hash)) {
470
+ return ["code-change/coder"];
471
+ }
446
472
  if (coder.value === "failed")
447
473
  return ["architect-debug/architect"];
448
474
  return coder.value === "ready_for_review" ? ["code-change/tester"] : ["code-change/coder"];
449
475
  }
450
476
  const architectsAfterTester = segment.filter((entry, index) => index > testerIndex && entry.targetRole === "architect");
451
477
  if (architectsAfterTester.length > 0) {
452
- const docs = await artifactState(fs, input, "docs-sync-report.md", "docs-sync-report");
453
- if (docs.value === "synced" || docs.value === "unchanged") {
454
- const acceptance = await artifactState(fs, input, "final-acceptance.md", "final-acceptance");
455
- if (acceptance.value === "needs-coder-follow-up")
456
- return ["code-change/coder"];
457
- if (acceptance.value === "needs-docs-sync")
458
- return ["code-change/architect"];
459
- if (acceptance.value === "needs-architect-follow-up") {
460
- if (architectsAfterTester.length === 1)
461
- return ["code-change/architect"];
462
- return allowedArchitectureFollowup(fs, input, architectsAfterTester[1]?.confirmedAt);
463
- }
464
- return [];
465
- }
466
- return ["code-change/architect"];
478
+ return allowedPostImplementationArchitect(fs, input, state, "code-change", architectsAfterTester);
467
479
  }
468
- return allowedAfterTester(fs, input, "coder", {
480
+ return allowedAfterTester(fs, input, state, "coder", {
469
481
  testerFailureFlow: "architect-debug",
470
482
  implementationFailureFlow: "architect-debug",
471
483
  successTarget: "code-change/architect"
472
484
  });
473
485
  }
474
- async function allowedArchitectureFollowup(fs, input, followupDispatchedAt) {
486
+ async function allowedArchitectureFollowup(fs, input, state, followupDispatchedAt) {
475
487
  const plan = await artifactState(fs, input, "architecture-plan.md", "architecture-plan");
476
- if (plan.value !== "complete")
488
+ if (!evidenceIsFresh(state, "code-change", "architect", "architecture-plan.md", plan.hash)
489
+ || plan.value !== "complete")
477
490
  return ["code-change/architect"];
478
491
  const gate = await gateState(fs, input, "architecture-plan");
479
492
  if (!gate || !followupDispatchedAt || gate.updatedAt <= followupDispatchedAt)
480
493
  return [];
481
- if (gate.decision === "request_changes")
494
+ if (freshGateDecision(state, "code-change", "architect", "architecture-plan", gate) === "request_changes") {
482
495
  return ["code-change/architect"];
483
- return gatePassed(gate) ? ["code-change/coder"] : [];
496
+ }
497
+ return gatePassedForDispatch(state, "code-change", "architect", "architecture-plan", gate)
498
+ ? ["code-change/coder"]
499
+ : [];
484
500
  }
485
- async function allowedArchitectFix(fs, input, current, segment, source) {
501
+ async function allowedArchitectFix(fs, input, state, segment, source, resumedFromBranch, parentFlow) {
486
502
  const architectIndex = findLastIndex(segment, (entry) => entry.targetRole === "architect");
487
503
  const testerIndex = findLastIndex(segment, (entry) => entry.targetRole === "tester");
504
+ if (resumedFromBranch && architectIndex >= 0 && testerIndex < 0) {
505
+ return allowedPostImplementationArchitect(fs, input, state, source, segment);
506
+ }
488
507
  if (architectIndex < 0)
489
508
  return [`${source}/architect`];
490
509
  if (testerIndex < 0) {
491
510
  const artifact = source === "architect-debug"
492
511
  ? await artifactState(fs, input, "architect-debug.md", "architect-debug")
493
512
  : await artifactState(fs, input, "architecture-diagnosis.md", "architecture-diagnosis");
513
+ const artifactName = source === "architect-debug" ? "architect-debug.md" : "architecture-diagnosis.md";
514
+ if (!evidenceIsFresh(state, source, "architect", artifactName, artifact.hash)) {
515
+ return [`${source}/architect`];
516
+ }
494
517
  if (source === "architect-debug" && artifact.disposition === "normal architecture plan required") {
495
518
  return ["code-change/architect"];
496
519
  }
497
520
  return artifact.complete ? [`${source}/tester`] : [`${source}/architect`];
498
521
  }
499
522
  if (architectIndex > testerIndex) {
500
- const codeDiff = await gateState(fs, input, "code-diff");
501
- if (codeDiff?.decision === "request_changes")
523
+ const artifactName = source === "architect-debug" ? "architect-debug.md" : "architecture-diagnosis.md";
524
+ const artifact = source === "architect-debug"
525
+ ? await artifactState(fs, input, artifactName, "architect-debug")
526
+ : await artifactState(fs, input, artifactName, "architecture-diagnosis");
527
+ if (evidenceIsFresh(state, source, "architect", artifactName, artifact.hash) && artifact.complete) {
502
528
  return [`${source}/tester`];
529
+ }
503
530
  const docs = await artifactState(fs, input, "docs-sync-report.md", "docs-sync-report");
504
- return docs.value === "synced" || docs.value === "unchanged" ? [] : [`${source}/architect`];
531
+ return evidenceIsFresh(state, source, "architect", "docs-sync-report.md", docs.hash)
532
+ && (docs.value === "synced" || docs.value === "unchanged")
533
+ ? []
534
+ : [`${source}/architect`];
505
535
  }
506
- const parentCodeChange = current.history.slice(0, -segment.length).some((entry) => entry.flow === "code-change");
507
- return allowedAfterTester(fs, input, source, {
536
+ return allowedAfterTester(fs, input, state, source, {
508
537
  testerFailureFlow: source === "architect-debug" ? "architecture-diagnosis" : undefined,
509
538
  implementationFailureFlow: source,
510
- successTarget: parentCodeChange ? "code-change/architect" : `${source}/architect`
539
+ successTarget: parentFlow ? `${parentFlow}/architect` : `${source}/architect`
511
540
  });
512
541
  }
513
- async function allowedAfterTester(fs, input, codeSource, options) {
542
+ async function allowedPostImplementationArchitect(fs, input, state, flow, architectEntries) {
543
+ const docs = await artifactState(fs, input, "docs-sync-report.md", "docs-sync-report");
544
+ const acceptance = await artifactState(fs, input, "final-acceptance.md", "final-acceptance");
545
+ if (flow === "code-change" && architectEntries.length > 1 && acceptance.value === "needs-architect-follow-up") {
546
+ return allowedArchitectureFollowup(fs, input, state, architectEntries[1]?.confirmedAt);
547
+ }
548
+ if (evidenceIsFresh(state, flow, "architect", "docs-sync-report.md", docs.hash)
549
+ && (docs.value === "synced" || docs.value === "unchanged")) {
550
+ if (acceptance.value === "needs-coder-follow-up" && flow === "code-change")
551
+ return ["code-change/coder"];
552
+ if (acceptance.value === "needs-docs-sync")
553
+ return [`${flow}/architect`];
554
+ if (acceptance.value === "needs-architect-follow-up")
555
+ return [`${flow}/architect`];
556
+ return [];
557
+ }
558
+ return [`${flow}/architect`];
559
+ }
560
+ async function allowedAfterTester(fs, input, state, codeSource, options) {
514
561
  const test = await artifactState(fs, input, "test-report.md", "test-report");
515
562
  const currentFlow = codeSource === "coder" ? "code-change" : codeSource;
563
+ if (!evidenceIsFresh(state, currentFlow, "tester", "test-report.md", test.hash)) {
564
+ return [`${currentFlow}/tester`];
565
+ }
516
566
  if (test.value === "incomplete" || test.infrastructure === "repair-required")
517
567
  return [`${currentFlow}/tester`];
518
568
  if (test.value === "fail" && test.infrastructure !== "repair-required") {
519
569
  return options.testerFailureFlow ? [`${options.testerFailureFlow}/architect`] : [];
520
570
  }
521
571
  const validation = await gateState(fs, input, "validation-adequacy");
522
- if (validation?.decision === "request_changes")
572
+ if (freshGateDecision(state, currentFlow, "tester", "validation-adequacy", validation) === "request_changes") {
523
573
  return [`${currentFlow}/tester`];
524
- if (!gatePassed(validation))
574
+ }
575
+ if (!gatePassedForDispatch(state, currentFlow, "tester", "validation-adequacy", validation))
525
576
  return [];
526
577
  const codeDiff = await gateState(fs, input, "code-diff");
527
- if (codeDiff?.decision === "request_changes") {
528
- const scopes = new Set(codeDiff.findings?.map((finding) => finding.scope).filter(Boolean));
578
+ if (freshGateDecision(state, currentFlow, "tester", "code-diff", codeDiff) === "request_changes") {
579
+ const scopes = new Set(codeDiff?.findings?.map((finding) => finding.scope).filter(Boolean));
529
580
  return scopes.size === 1 && scopes.has("test-only")
530
581
  ? [`${currentFlow}/tester`]
531
582
  : options.implementationFailureFlow ? [`${options.implementationFailureFlow}/architect`] : [];
532
583
  }
533
- if (!gatePassed(codeDiff))
584
+ if (!gatePassedForDispatch(state, currentFlow, "tester", "code-diff", codeDiff))
534
585
  return [];
535
586
  const gateCodeSource = codeSource === "architecture-diagnosis" ? "architect-diagnosis" : codeSource;
536
587
  const reviewedSources = codeDiff?.codeDiffSources ?? (codeDiff?.codeDiffSource ? [codeDiff.codeDiffSource] : []);
@@ -542,7 +593,7 @@ async function artifactState(fs, input, fileName, kind) {
542
593
  const relative = path.posix.join(input.handoffDir, fileName);
543
594
  const absolute = resolveRepoPath(input.taskRepoRoot, relative);
544
595
  if (!(await fs.pathExists(absolute)))
545
- return { complete: false };
596
+ return { complete: false, hash: MISSING_EVIDENCE_HASH };
546
597
  const content = await fs.readText(absolute);
547
598
  const check = checkMarkdownArtifact(kind, relative, content, { mode: "final" });
548
599
  const inline = (name) => new RegExp(`^${name}:\\s*(.+?)\\s*$`, "mi").exec(content)?.[1]?.trim().toLowerCase();
@@ -557,15 +608,22 @@ async function artifactState(fs, input, fileName, kind) {
557
608
  value = readArtifactSectionContent(content, "Final Disposition")?.trim().toLowerCase();
558
609
  if (kind === "test-report")
559
610
  value = inline("Test Result");
560
- if (kind === "docs-sync-report" || kind === "final-acceptance")
611
+ if (kind === "docs-update-report" || kind === "docs-sync-report" || kind === "final-acceptance") {
561
612
  value = readArtifactSectionContent(content, "Decision")?.trim().toLowerCase();
613
+ }
562
614
  const infrastructure = kind === "test-report"
563
615
  ? /^Status:\s*(.+?)\s*$/mi.exec(readArtifactSectionContent(content, "Test Infrastructure") ?? "")?.[1]?.trim().toLowerCase()
564
616
  : undefined;
565
617
  const disposition = kind === "architect-debug" || kind === "architecture-diagnosis"
566
618
  ? readArtifactSectionContent(content, "Final Disposition")?.trim().toLowerCase()
567
619
  : undefined;
568
- return { complete: check.status === "ok", value, infrastructure, disposition };
620
+ return {
621
+ complete: check.status === "ok",
622
+ hash: contentHash(content),
623
+ value,
624
+ infrastructure,
625
+ disposition
626
+ };
569
627
  }
570
628
  async function gateState(fs, input, gate) {
571
629
  const target = resolveRepoPath(input.taskRepoRoot, path.posix.join(".ai/vcm/gate-reviews", "index.json"));
@@ -579,46 +637,235 @@ async function gateState(fs, input, gate) {
579
637
  return undefined;
580
638
  }
581
639
  }
582
- function gatePassed(record) {
583
- return Boolean(record && (FINAL_GATE_STATUSES.has(record.status)
584
- || (record.status === "completed" && record.decision === "approve")));
640
+ async function captureEvidenceBaseline(fs, input, sequence, flow, targetRole, confirmedAt) {
641
+ const artifactEntries = await Promise.all(WORKFLOW_EVIDENCE_ARTIFACTS.map(async (fileName) => {
642
+ const target = resolveRepoPath(input.taskRepoRoot, path.posix.join(input.handoffDir, fileName));
643
+ const hash = await fs.pathExists(target) ? contentHash(await fs.readText(target)) : MISSING_EVIDENCE_HASH;
644
+ return [fileName, hash];
645
+ }));
646
+ const gateEntries = await Promise.all(WORKFLOW_EVIDENCE_GATES.map(async (gate) => [
647
+ gate,
648
+ gateFingerprint(await gateState(fs, input, gate))
649
+ ]));
650
+ return {
651
+ sequence,
652
+ flow,
653
+ targetRole,
654
+ artifactHashes: Object.fromEntries(artifactEntries),
655
+ gateFingerprints: Object.fromEntries(gateEntries),
656
+ confirmedAt
657
+ };
658
+ }
659
+ async function advanceFlowRun(fs, input, state, current, effectiveFlow, usedOverride, nextSequence) {
660
+ if (!current.flow || current.status === "completed" || (usedOverride && effectiveFlow !== current.flow)) {
661
+ return newFlowRun(effectiveFlow, nextSequence);
662
+ }
663
+ const run = resolveFlowRun(state.flowRun, current);
664
+ if (effectiveFlow === current.flow)
665
+ return run;
666
+ if (current.flow === "code-change" && effectiveFlow === "architect-debug" && run.rootFlow === "code-change") {
667
+ return { ...run, activeBranch: "architect-debug", resumedFromBranch: undefined };
668
+ }
669
+ if (current.flow === "architect-debug" && effectiveFlow === "architecture-diagnosis") {
670
+ if (run.rootFlow === "code-change" && run.activeBranch === "architect-debug") {
671
+ return { ...run, activeBranch: "architecture-diagnosis", resumedFromBranch: undefined };
672
+ }
673
+ if (run.rootFlow === "architect-debug" && !run.activeBranch) {
674
+ return { ...run, activeBranch: "architecture-diagnosis", resumedFromBranch: undefined };
675
+ }
676
+ }
677
+ if (run.activeBranch === current.flow && effectiveFlow === run.rootFlow) {
678
+ if (current.flow === "architect-debug" && effectiveFlow === "code-change") {
679
+ const debug = await artifactState(fs, input, "architect-debug.md", "architect-debug");
680
+ if (debug.disposition === "normal architecture plan required") {
681
+ return newFlowRun("code-change", nextSequence);
682
+ }
683
+ }
684
+ return {
685
+ ...run,
686
+ activeBranch: undefined,
687
+ resumedFromBranch: current.flow
688
+ };
689
+ }
690
+ return newFlowRun(effectiveFlow, nextSequence);
691
+ }
692
+ function newFlowRun(flow, startedAtSequence) {
693
+ return { rootFlow: flow, startedAtSequence };
585
694
  }
586
- async function validateCompletion(fs, input, candidate) {
695
+ function resolveFlowRun(recorded, current) {
696
+ if (recorded && current.flow
697
+ && (recorded.rootFlow === current.flow || recorded.activeBranch === current.flow)) {
698
+ return recorded;
699
+ }
700
+ if (!current.flow || current.history.length === 0) {
701
+ return newFlowRun(current.flow ?? "code-change", 1);
702
+ }
703
+ const segments = flowHistorySegments(current.history);
704
+ const last = segments.at(-1);
705
+ const previous = segments.at(-2);
706
+ const grandparent = segments.at(-3);
707
+ if (last.flow === "architect-debug" && previous?.flow === "code-change") {
708
+ return {
709
+ rootFlow: "code-change",
710
+ activeBranch: "architect-debug",
711
+ startedAtSequence: previous.startedAtSequence
712
+ };
713
+ }
714
+ if (last.flow === "architecture-diagnosis" && previous?.flow === "architect-debug") {
715
+ return {
716
+ rootFlow: grandparent?.flow === "code-change" ? "code-change" : "architect-debug",
717
+ activeBranch: "architecture-diagnosis",
718
+ startedAtSequence: grandparent?.flow === "code-change"
719
+ ? grandparent.startedAtSequence
720
+ : previous.startedAtSequence
721
+ };
722
+ }
723
+ if (last.flow === "architecture-diagnosis" && previous?.flow === "code-change") {
724
+ return {
725
+ rootFlow: "code-change",
726
+ activeBranch: "architecture-diagnosis",
727
+ startedAtSequence: previous.startedAtSequence
728
+ };
729
+ }
730
+ if (last.flow === "code-change" && previous
731
+ && (previous.flow === "architect-debug" || previous.flow === "architecture-diagnosis")
732
+ && grandparent?.flow === "code-change") {
733
+ return {
734
+ rootFlow: "code-change",
735
+ resumedFromBranch: previous.flow,
736
+ startedAtSequence: grandparent.startedAtSequence
737
+ };
738
+ }
739
+ return newFlowRun(last.flow, last.startedAtSequence);
740
+ }
741
+ function flowHistorySegments(history) {
742
+ const segments = [];
743
+ for (const entry of history) {
744
+ if (segments.at(-1)?.flow !== entry.flow) {
745
+ segments.push({ flow: entry.flow, startedAtSequence: entry.sequence });
746
+ }
747
+ }
748
+ return segments;
749
+ }
750
+ function activeFlowSegment(history, flow, startedAtSequence) {
751
+ const runStart = history.findIndex((entry) => entry.sequence >= startedAtSequence);
752
+ const lastDifferent = findLastIndex(history, (entry) => entry.flow !== flow);
753
+ const start = Math.max(runStart < 0 ? 0 : runStart, lastDifferent + 1);
754
+ return history.slice(start);
755
+ }
756
+ function evidenceIsFresh(state, flow, targetRole, artifact, currentHash) {
757
+ const baseline = matchingEvidenceBaseline(state, flow, targetRole);
758
+ return !baseline || baseline.artifactHashes[artifact] !== currentHash;
759
+ }
760
+ function freshGateDecision(state, flow, targetRole, gate, record) {
761
+ const baseline = matchingEvidenceBaseline(state, flow, targetRole);
762
+ if (baseline && baseline.gateFingerprints[gate] === gateFingerprint(record))
763
+ return undefined;
764
+ return record?.decision;
765
+ }
766
+ function gatePassedForDispatch(state, flow, targetRole, gate, record) {
767
+ if (!record)
768
+ return false;
769
+ if (FINAL_GATE_STATUSES.has(record.status))
770
+ return true;
771
+ return freshGateDecision(state, flow, targetRole, gate, record) === "approve"
772
+ && record.status === "completed";
773
+ }
774
+ function matchingEvidenceBaseline(state, flow, targetRole) {
775
+ const baseline = state.activeDispatch;
776
+ return baseline?.flow === flow && baseline.targetRole === targetRole ? baseline : undefined;
777
+ }
778
+ function gateFingerprint(record) {
779
+ if (!record)
780
+ return MISSING_EVIDENCE_HASH;
781
+ return contentHash(JSON.stringify({
782
+ requestId: record.requestId,
783
+ inputHash: record.inputHash,
784
+ status: record.status,
785
+ decision: record.decision,
786
+ codeDiffSource: record.codeDiffSource,
787
+ codeDiffSources: record.codeDiffSources,
788
+ findings: record.findings,
789
+ completedAt: record.completedAt
790
+ }));
791
+ }
792
+ async function describeBlockedCheckpoint(fs, input, state, current) {
793
+ const baseline = state.activeDispatch;
794
+ if (!baseline || !current.flow) {
795
+ return "No role dispatch is legal at this checkpoint. Complete the required current Gate or PM-only step first.";
796
+ }
797
+ if (baseline.targetRole === "tester") {
798
+ const test = await artifactState(fs, input, "test-report.md", "test-report");
799
+ if (!evidenceIsFresh(state, baseline.flow, "tester", "test-report.md", test.hash)) {
800
+ return "The latest Tester dispatch has not produced a fresh test-report.md. Wait for Tester to finish or route Tester again if its result is incomplete.";
801
+ }
802
+ if (baseline.flow === "architecture-diagnosis" && test.value === "fail") {
803
+ 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.";
804
+ }
805
+ return "The latest Tester result is waiting for its current validation-adequacy or code-diff Gate result.";
806
+ }
807
+ if (baseline.targetRole === "architect") {
808
+ return "The latest Architect dispatch has not produced the fresh workflow artifact required for its next step.";
809
+ }
810
+ if (baseline.targetRole === "coder") {
811
+ return "The latest Coder dispatch has not produced a fresh coder-completion.md.";
812
+ }
813
+ return "No role dispatch is legal at this checkpoint. Complete the required current Gate or PM-only step first.";
814
+ }
815
+ async function validateCompletion(fs, input, state, candidate) {
587
816
  if (!candidate.flow || candidate.history.length === 0) {
588
817
  throw workflowError("WORKFLOW_COMPLETION_INVALID", "An unstarted workflow cannot be completed.");
589
818
  }
819
+ const flowRun = resolveFlowRun(state.flowRun, candidate);
820
+ if (flowRun.activeBranch === candidate.flow) {
821
+ throw workflowError("WORKFLOW_COMPLETION_INVALID", `${candidate.flow} is an active branch and must return to ${flowRun.rootFlow} before completion.`);
822
+ }
590
823
  if (candidate.flow === "code-change" || candidate.flow === "architect-debug") {
591
824
  const acceptance = await artifactState(fs, input, "final-acceptance.md", "final-acceptance");
592
- if (acceptance.value !== "accepted" && acceptance.value !== "accepted-with-known-risks") {
825
+ if (!evidenceProducedAfterActiveDispatch(state, candidate.flow, "architect", "final-acceptance.md", acceptance.hash)
826
+ || (acceptance.value !== "accepted" && acceptance.value !== "accepted-with-known-risks")) {
593
827
  throw workflowError("WORKFLOW_COMPLETION_INVALID", "Final Acceptance is not accepted for this complete delivery flow.");
594
828
  }
595
829
  return;
596
830
  }
597
831
  if (candidate.flow === "architecture-diagnosis") {
598
832
  const diagnosis = await artifactState(fs, input, "architecture-diagnosis.md", "architecture-diagnosis");
599
- if (diagnosis.disposition === "analysis completed")
833
+ if (diagnosis.disposition === "analysis completed"
834
+ && evidenceProducedAfterActiveDispatch(state, candidate.flow, "architect", "architecture-diagnosis.md", diagnosis.hash))
600
835
  return;
601
836
  const acceptance = await artifactState(fs, input, "final-acceptance.md", "final-acceptance");
602
- if (acceptance.value !== "accepted" && acceptance.value !== "accepted-with-known-risks") {
837
+ if (!evidenceProducedAfterActiveDispatch(state, candidate.flow, "architect", "final-acceptance.md", acceptance.hash)
838
+ || (acceptance.value !== "accepted" && acceptance.value !== "accepted-with-known-risks")) {
603
839
  throw workflowError("WORKFLOW_COMPLETION_INVALID", "Implemented Architecture Diagnosis requires accepted Final Acceptance evidence.");
604
840
  }
605
841
  return;
606
842
  }
607
843
  if (candidate.flow === "docs-only") {
608
- const docs = await artifactState(fs, input, "docs-sync-report.md", "docs-sync-report");
609
- if (docs.value !== "synced" && docs.value !== "unchanged") {
610
- throw workflowError("WORKFLOW_COMPLETION_INVALID", "Docs-only completion requires a synced or unchanged Docs Sync Report.");
844
+ const docs = await artifactState(fs, input, "docs-update-report.md", "docs-update-report");
845
+ const activeRole = state.activeDispatch?.flow === "docs-only"
846
+ ? state.activeDispatch.targetRole
847
+ : undefined;
848
+ if (!activeRole
849
+ || !evidenceProducedAfterActiveDispatch(state, candidate.flow, activeRole, "docs-update-report.md", docs.hash)
850
+ || !docs.complete || (docs.value !== "synced" && docs.value !== "unchanged")) {
851
+ throw workflowError("WORKFLOW_COMPLETION_INVALID", "Docs-only completion requires a fresh, complete Docs Update Report from the latest assigned role with Decision: synced or Decision: unchanged.");
611
852
  }
612
853
  return;
613
854
  }
614
855
  if (candidate.flow === "validation-only") {
615
856
  const test = await artifactState(fs, input, "test-report.md", "test-report");
616
857
  const gate = await gateState(fs, input, "validation-adequacy");
617
- if ((test.value !== "pass" && test.value !== "fail") || !gatePassed(gate)) {
858
+ if (!evidenceProducedAfterActiveDispatch(state, candidate.flow, "tester", "test-report.md", test.hash)
859
+ || (test.value !== "pass" && test.value !== "fail")
860
+ || !gatePassedForDispatch(state, candidate.flow, "tester", "validation-adequacy", gate)) {
618
861
  throw workflowError("WORKFLOW_COMPLETION_INVALID", "Validation-only completion requires a terminal Test Report and a passed Validation Adequacy Gate.");
619
862
  }
620
863
  }
621
864
  }
865
+ function evidenceProducedAfterActiveDispatch(state, flow, targetRole, artifact, currentHash) {
866
+ const baseline = matchingEvidenceBaseline(state, flow, targetRole);
867
+ return Boolean(baseline && baseline.artifactHashes[artifact] !== currentHash);
868
+ }
622
869
  function validateCandidateAgainstCurrent(current, candidate) {
623
870
  const errors = [];
624
871
  if (candidate.revision !== current.revision + 1)
@@ -632,26 +879,12 @@ function validateCandidateAgainstCurrent(current, candidate) {
632
879
  if (errors.length > 0)
633
880
  throw progressValidationError(errors);
634
881
  }
635
- function validateApprovedOverride(override, current, candidate, effectiveFlow, baseHistoryHash, violation) {
636
- const proposal = candidate.proposal;
637
- if (!override || override.status !== "approved")
638
- throw workflowError("WORKFLOW_OVERRIDE_INVALID", "The supplied workflow override is not approved.");
639
- if (override.baseRevision !== current.revision
640
- || override.baseHistoryHash !== baseHistoryHash
641
- || override.effectiveFlow !== effectiveFlow
642
- || override.requestedFlow !== proposal.requestedFlow
643
- || override.targetRole !== proposal.targetRole
644
- || override.evidence !== proposal.evidence
645
- || override.violatedRule !== violation
646
- || override.authorizationText !== proposal.authorizationQuote
647
- || proposal.violatedRule !== violation) {
648
- throw workflowError("WORKFLOW_OVERRIDE_MISMATCH", "The approved override does not match this exact workflow transition.");
649
- }
650
- }
651
- function createOverrideRequest(overrideId, current, candidate, effectiveFlow, baseHistoryHash, violation, quote, timestamp) {
882
+ function createUserAuthorization(authorizationId, current, candidate, effectiveFlow, baseHistoryHash, violation, authorizationText, timestamp) {
652
883
  return {
653
- id: overrideId,
654
- status: "pending",
884
+ id: authorizationId,
885
+ status: "accepted",
886
+ role: "project-manager",
887
+ operation: "workflow-dispatch",
655
888
  baseRevision: current.revision,
656
889
  baseHistoryHash,
657
890
  requestedFlow: candidate.proposal.requestedFlow,
@@ -659,7 +892,7 @@ function createOverrideRequest(overrideId, current, candidate, effectiveFlow, ba
659
892
  targetRole: candidate.proposal.targetRole,
660
893
  evidence: candidate.proposal.evidence,
661
894
  violatedRule: violation,
662
- proposedAuthorizationQuote: quote,
895
+ authorizationText,
663
896
  createdAt: timestamp
664
897
  };
665
898
  }
@@ -725,24 +958,52 @@ function normalizeState(value, taskSlug, timestamp) {
725
958
  const pendingDispatch = value.pendingDispatch === null
726
959
  ? null
727
960
  : isPendingDispatch(value.pendingDispatch) ? value.pendingDispatch : undefined;
728
- const overrideRequests = Array.isArray(value.overrideRequests)
729
- && value.overrideRequests.every(isOverrideRequest)
730
- ? value.overrideRequests
731
- : undefined;
732
- if (pendingDispatch === undefined || overrideRequests === undefined) {
961
+ const userAuthorizations = Array.isArray(value.userAuthorizations)
962
+ && value.userAuthorizations.every(isUserAuthorization)
963
+ ? value.userAuthorizations
964
+ : value.userAuthorizations === undefined && Array.isArray(value.overrideRequests)
965
+ ? []
966
+ : undefined;
967
+ const activeDispatch = value.activeDispatch === undefined || value.activeDispatch === null
968
+ ? null
969
+ : normalizeDispatchEvidenceBaseline(value.activeDispatch);
970
+ const flowRun = value.flowRun === undefined || value.flowRun === null
971
+ ? null
972
+ : isFlowRun(value.flowRun) ? value.flowRun : undefined;
973
+ const awaitingUser = value.awaitingUser === undefined || value.awaitingUser === null
974
+ ? null
975
+ : isAwaitingUser(value.awaitingUser) ? value.awaitingUser : undefined;
976
+ if (pendingDispatch === undefined
977
+ || activeDispatch === undefined
978
+ || flowRun === undefined
979
+ || userAuthorizations === undefined
980
+ || awaitingUser === undefined) {
733
981
  return { ...emptyState(taskSlug, timestamp), warnings: ["Workflow control state has an unsupported shape."] };
734
982
  }
735
983
  return {
736
984
  version: 1,
737
985
  taskSlug,
986
+ awaitingUser,
738
987
  pendingDispatch,
739
- overrideRequests,
988
+ activeDispatch,
989
+ flowRun,
990
+ userAuthorizations,
740
991
  warnings: [],
741
992
  updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : timestamp
742
993
  };
743
994
  }
744
995
  function emptyState(taskSlug, timestamp) {
745
- return { version: 1, taskSlug, pendingDispatch: null, overrideRequests: [], warnings: [], updatedAt: timestamp };
996
+ return {
997
+ version: 1,
998
+ taskSlug,
999
+ awaitingUser: null,
1000
+ pendingDispatch: null,
1001
+ activeDispatch: null,
1002
+ flowRun: null,
1003
+ userAuthorizations: [],
1004
+ warnings: [],
1005
+ updatedAt: timestamp
1006
+ };
746
1007
  }
747
1008
  function resolveEffectiveFlow(current, requested) {
748
1009
  if (requested)
@@ -764,7 +1025,10 @@ function progressPath(input) {
764
1025
  return resolveRepoPath(input.taskRepoRoot, relativeProgressPath(input));
765
1026
  }
766
1027
  function historyHash(history) {
767
- return createHash("sha256").update(JSON.stringify(history)).digest("hex");
1028
+ return contentHash(JSON.stringify(history));
1029
+ }
1030
+ function contentHash(content) {
1031
+ return createHash("sha256").update(content).digest("hex");
768
1032
  }
769
1033
  function field(content, name) {
770
1034
  return rawField(content, name)?.toLowerCase();
@@ -792,11 +1056,21 @@ function failOnStateWarnings(state) {
792
1056
  if (state.warnings.length > 0)
793
1057
  throw workflowError("WORKFLOW_STATE_INVALID", state.warnings.join(" "));
794
1058
  }
1059
+ function failWhileAwaitingUser(state) {
1060
+ if (!state.awaitingUser)
1061
+ return;
1062
+ throw workflowError("WORKFLOW_AWAITING_USER", "Project Manager is waiting for the user's answer and cannot advance the workflow.", "Wait for a new direct user message. The previous workflow approval was canceled; request a fresh approval after the answer arrives.");
1063
+ }
795
1064
  function progressValidationError(errors) {
796
1065
  return workflowError("WORKFLOW_PROGRESS_INVALID", `Workflow Progress validation failed:\n${errors.map((error) => `- ${error}`).join("\n")}`);
797
1066
  }
798
1067
  function workflowError(code, message, hint) {
799
- return new VcmError({ code, message, hint, statusCode: code.includes("PENDING") ? 409 : 422 });
1068
+ return new VcmError({
1069
+ code,
1070
+ message,
1071
+ hint,
1072
+ statusCode: code.includes("PENDING") || code.includes("AWAITING_USER") ? 409 : 422
1073
+ });
800
1074
  }
801
1075
  async function writeAtomic(fs, target, content) {
802
1076
  if (fs.writeTextAtomic)
@@ -807,6 +1081,12 @@ async function writeAtomic(fs, target, content) {
807
1081
  function isRecord(value) {
808
1082
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
809
1083
  }
1084
+ function isAwaitingUser(value) {
1085
+ return isRecord(value)
1086
+ && typeof value.question === "string"
1087
+ && value.question.trim().length > 0
1088
+ && typeof value.requestedAt === "string";
1089
+ }
810
1090
  function isPendingDispatch(value) {
811
1091
  if (!isRecord(value))
812
1092
  return false;
@@ -824,11 +1104,13 @@ function isPendingDispatch(value) {
824
1104
  && typeof value.createdAt === "string"
825
1105
  && typeof value.updatedAt === "string";
826
1106
  }
827
- function isOverrideRequest(value) {
1107
+ function isUserAuthorization(value) {
828
1108
  if (!isRecord(value))
829
1109
  return false;
830
1110
  return typeof value.id === "string"
831
- && ["pending", "approved", "rejected", "consumed"].includes(String(value.status))
1111
+ && (value.status === "accepted" || value.status === "consumed")
1112
+ && value.role === "project-manager"
1113
+ && value.operation === "workflow-dispatch"
832
1114
  && Number.isInteger(value.baseRevision)
833
1115
  && typeof value.baseHistoryHash === "string"
834
1116
  && (value.requestedFlow === undefined || Boolean(asFlow(typeof value.requestedFlow === "string" ? value.requestedFlow : undefined)))
@@ -836,12 +1118,50 @@ function isOverrideRequest(value) {
836
1118
  && Boolean(asTargetRole(typeof value.targetRole === "string" ? value.targetRole : undefined))
837
1119
  && typeof value.evidence === "string"
838
1120
  && typeof value.violatedRule === "string"
839
- && typeof value.proposedAuthorizationQuote === "string"
840
- && (value.authorizationText === undefined || typeof value.authorizationText === "string")
1121
+ && typeof value.authorizationText === "string"
841
1122
  && typeof value.createdAt === "string"
842
- && (value.decidedAt === undefined || typeof value.decidedAt === "string")
843
1123
  && (value.consumedAt === undefined || typeof value.consumedAt === "string");
844
1124
  }
1125
+ function normalizeDispatchEvidenceBaseline(value) {
1126
+ if (!isRecord(value) || !isRecord(value.artifactHashes) || !isRecord(value.gateFingerprints))
1127
+ return undefined;
1128
+ const artifactHashes = value.artifactHashes;
1129
+ const gateFingerprints = value.gateFingerprints;
1130
+ if (!(Number.isInteger(value.sequence)
1131
+ && Boolean(asFlow(typeof value.flow === "string" ? value.flow : undefined))
1132
+ && Boolean(asTargetRole(typeof value.targetRole === "string" ? value.targetRole : undefined))
1133
+ && WORKFLOW_EVIDENCE_GATES.every((gate) => typeof gateFingerprints[gate] === "string")
1134
+ && typeof value.confirmedAt === "string"))
1135
+ return undefined;
1136
+ return {
1137
+ sequence: Number(value.sequence),
1138
+ flow: value.flow,
1139
+ targetRole: value.targetRole,
1140
+ artifactHashes: Object.fromEntries(WORKFLOW_EVIDENCE_ARTIFACTS.map((artifact) => [
1141
+ artifact,
1142
+ typeof artifactHashes[artifact] === "string" ? artifactHashes[artifact] : MISSING_EVIDENCE_HASH
1143
+ ])),
1144
+ gateFingerprints: Object.fromEntries(WORKFLOW_EVIDENCE_GATES.map((gate) => [
1145
+ gate,
1146
+ gateFingerprints[gate]
1147
+ ])),
1148
+ confirmedAt: value.confirmedAt
1149
+ };
1150
+ }
1151
+ function isFlowRun(value) {
1152
+ if (!isRecord(value))
1153
+ return false;
1154
+ const rootFlow = asFlow(typeof value.rootFlow === "string" ? value.rootFlow : undefined);
1155
+ const activeBranch = value.activeBranch;
1156
+ const resumedFromBranch = value.resumedFromBranch;
1157
+ return Boolean(rootFlow)
1158
+ && (activeBranch === undefined || activeBranch === "architect-debug" || activeBranch === "architecture-diagnosis")
1159
+ && (resumedFromBranch === undefined
1160
+ || resumedFromBranch === "architect-debug"
1161
+ || resumedFromBranch === "architecture-diagnosis")
1162
+ && Number.isInteger(value.startedAtSequence)
1163
+ && Number(value.startedAtSequence) >= 1;
1164
+ }
845
1165
  function findLastIndex(values, predicate) {
846
1166
  for (let index = values.length - 1; index >= 0; index -= 1) {
847
1167
  if (predicate(values[index]))