vibe-coding-master 0.7.42 → 0.7.43

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 (33) hide show
  1. package/README.md +10 -5
  2. package/dist/backend/api/task-routes.js +31 -2
  3. package/dist/backend/api/workflow-control-routes.js +0 -31
  4. package/dist/backend/cli/install-vcm-harness.js +40 -6
  5. package/dist/backend/role-tool-policy.js +1 -1
  6. package/dist/backend/server.js +12 -5
  7. package/dist/backend/services/artifact-service.js +3 -2
  8. package/dist/backend/services/auto-memory-service.js +2 -2
  9. package/dist/backend/services/claude-hook-service.js +103 -4
  10. package/dist/backend/services/harness-feedback-service.js +105 -3
  11. package/dist/backend/services/harness-service.js +38 -6
  12. package/dist/backend/services/memory-review-paths.js +13 -0
  13. package/dist/backend/services/role-stall-detector-service.js +322 -0
  14. package/dist/backend/services/round-service.js +25 -0
  15. package/dist/backend/services/runtime-coordinator-service.js +10 -0
  16. package/dist/backend/services/session-service.js +70 -3
  17. package/dist/backend/services/workflow-control-service.js +439 -203
  18. package/dist/backend/templates/handoff.js +2 -3
  19. package/dist/backend/templates/harness/architect-agent.js +2 -2
  20. package/dist/backend/templates/harness/coder-agent.js +4 -5
  21. package/dist/backend/templates/harness/gate-review.js +7 -9
  22. package/dist/backend/templates/harness/harness-engineer-agent.js +18 -8
  23. package/dist/backend/templates/harness/project-manager-agent.js +5 -5
  24. package/dist/backend/templates/harness/vcm-code-navigation-skill.js +6 -7
  25. package/dist/backend/templates/harness/vcm-workflow-review-skill.js +7 -9
  26. package/dist/shared/types/role-stall.js +1 -0
  27. package/dist/shared/types/workflow.js +14 -0
  28. package/dist/shared/validation/artifact-registry.js +1 -1
  29. package/dist-frontend/assets/{index-C_XHGNBD.css → index-B0d4Z6ny.css} +1 -1
  30. package/dist-frontend/assets/{index-Bocc2DWF.js → index-VW9tYPP5.js} +38 -38
  31. package/dist-frontend/index.html +2 -2
  32. package/package.json +1 -1
  33. package/scripts/harness-tools/vcm-bash-guard +203 -13
@@ -2,16 +2,17 @@ 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>";
12
13
  export function createWorkflowControlService(deps) {
13
14
  const now = deps.now ?? (() => new Date().toISOString());
14
- const id = deps.id ?? (() => `wfovr_${randomUUID()}`);
15
+ const id = deps.id ?? (() => `wfauth_${randomUUID()}`);
15
16
  const locks = new Map();
16
17
  async function getState(input) {
17
18
  try {
@@ -44,49 +45,38 @@ export function createWorkflowControlService(deps) {
44
45
  if (candidate.status !== "completed") {
45
46
  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
47
  }
47
- await validateCompletion(deps.fs, input, candidate);
48
+ await validateCompletion(deps.fs, input, state, candidate);
48
49
  const normalized = renderWorkflowProgress(candidate);
49
50
  await writeAtomic(deps.fs, progressPath(input), normalized);
50
51
  return { path: relativeProgressPath(input), content: normalized };
51
52
  }
53
+ if (current.status === "completed" && !candidate.proposal.requestedFlow) {
54
+ 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.");
55
+ }
52
56
  const baseHistoryHash = historyHash(current.history);
53
57
  const effectiveFlow = resolveEffectiveFlow(current.flow, candidate.proposal.requestedFlow);
54
- const verdict = await evaluateTransition(deps.fs, input, current, effectiveFlow, candidate.proposal.targetRole);
58
+ const verdict = await evaluateTransition(deps.fs, input, state, current, effectiveFlow, candidate.proposal.targetRole);
55
59
  let overrideAuthorizationId;
60
+ let userAuthorization;
56
61
  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.");
62
+ const authorizationText = candidate.proposal.authorizationText?.trim();
63
+ const violatedRule = candidate.proposal.violatedRule?.trim();
64
+ if (!authorizationText && !violatedRule) {
65
+ throw workflowError("WORKFLOW_TRANSITION_DENIED", verdict.reason, verdict.allowedTransitions.length > 0
66
+ ? `Allowed next dispatches: ${verdict.allowedTransitions.join(", ")}. Recheck the flow, or ask the user directly for an exact one-time authorization.`
67
+ : verdict.blockedHint);
76
68
  }
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;
69
+ if (!authorizationText || violatedRule !== verdict.reason) {
70
+ 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
71
  }
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.");
72
+ if (state.userAuthorizations.some((entry) => entry.authorizationText === authorizationText)) {
73
+ 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
74
  }
75
+ userAuthorization = createUserAuthorization(id(), current, candidate, effectiveFlow, baseHistoryHash, verdict.reason, authorizationText, now());
76
+ overrideAuthorizationId = userAuthorization.id;
87
77
  }
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.");
78
+ else if (candidate.proposal.authorizationText || candidate.proposal.violatedRule) {
79
+ 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
80
  }
91
81
  const timestamp = now();
92
82
  const pendingDispatch = {
@@ -104,20 +94,16 @@ export function createWorkflowControlService(deps) {
104
94
  };
105
95
  const accepted = {
106
96
  ...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
- }
97
+ proposal: candidate.proposal
115
98
  };
116
99
  const normalized = renderWorkflowProgress(accepted);
117
100
  await writeAtomic(deps.fs, progressPath(input), normalized);
118
101
  await saveState(input, {
119
102
  ...state,
120
103
  pendingDispatch,
104
+ userAuthorizations: userAuthorization
105
+ ? [...state.userAuthorizations, userAuthorization]
106
+ : state.userAuthorizations,
121
107
  updatedAt: timestamp
122
108
  });
123
109
  return { path: relativeProgressPath(input), content: normalized };
@@ -196,6 +182,8 @@ export function createWorkflowControlService(deps) {
196
182
  overrideAuthorizationId: pending.overrideAuthorizationId,
197
183
  confirmedAt: timestamp
198
184
  };
185
+ const activeDispatch = await captureEvidenceBaseline(deps.fs, input, entry.sequence, entry.flow, entry.targetRole, timestamp);
186
+ const flowRun = await advanceFlowRun(deps.fs, input, state, current, pending.effectiveFlow, Boolean(pending.overrideAuthorizationId), entry.sequence);
199
187
  const completed = {
200
188
  ...current,
201
189
  flow: pending.effectiveFlow,
@@ -203,62 +191,27 @@ export function createWorkflowControlService(deps) {
203
191
  history: [...current.history, entry],
204
192
  proposal: undefined
205
193
  };
206
- const overrideRequests = state.overrideRequests.map((entry) => entry.id === pending.overrideAuthorizationId
194
+ const userAuthorizations = state.userAuthorizations.map((entry) => entry.id === pending.overrideAuthorizationId
207
195
  ? { ...entry, status: "consumed", consumedAt: timestamp }
208
196
  : entry);
209
197
  await writeAtomic(deps.fs, progressPath(input), renderWorkflowProgress(completed));
210
198
  await saveState(input, {
211
199
  ...state,
212
200
  pendingDispatch: null,
213
- overrideRequests,
201
+ activeDispatch,
202
+ flowRun,
203
+ userAuthorizations,
214
204
  updatedAt: timestamp
215
205
  });
216
206
  });
217
207
  }
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) {
225
- return withLock(statePath(input), async () => {
226
- 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.");
235
- }
236
- const timestamp = now();
237
- const next = {
238
- ...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),
247
- updatedAt: timestamp
248
- };
249
- await saveState(input, next);
250
- return next;
251
- });
252
- }
253
208
  return {
254
209
  getState,
255
210
  submitProgress,
256
211
  assertRouteAuthorized,
257
212
  claimDispatch,
258
213
  releaseDispatch,
259
- confirmDispatch,
260
- approveOverride,
261
- rejectOverride
214
+ confirmDispatch
262
215
  };
263
216
  async function saveState(input, state) {
264
217
  await deps.fs.writeJsonAtomic(statePath(input), state);
@@ -315,26 +268,24 @@ export function parseWorkflowProgress(content, expectedTaskSlug) {
315
268
  else if (requestedFlowValue !== "none" || evidence !== "none") {
316
269
  errors.push("A completed/no-dispatch proposal must use Requested Flow, Target Role, and Evidence value none.");
317
270
  }
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.");
271
+ const authorizationSection = readArtifactSectionContent(content, "User Authorization") ?? "";
272
+ const authorizationText = rawField(authorizationSection, "Authorization Text");
273
+ const violatedRule = rawField(authorizationSection, "Violated Rule");
274
+ if (!authorizationText || !violatedRule) {
275
+ errors.push("User Authorization requires Authorization Text and Violated Rule fields.");
324
276
  }
325
277
  else if (proposal) {
326
- const allNone = authorizationId === "none" && authorizationQuote === "none" && violatedRule === "none";
327
- const allSet = authorizationId !== "none" && authorizationQuote !== "none" && violatedRule !== "none";
278
+ const allNone = authorizationText === "none" && violatedRule === "none";
279
+ const allSet = authorizationText !== "none" && violatedRule !== "none";
328
280
  if (!allNone && !allSet)
329
- errors.push("User Override fields must all be none or all contain the exact override data.");
281
+ errors.push("User Authorization fields must both be none or both contain exact authorization evidence.");
330
282
  if (allSet) {
331
- proposal.authorizationId = authorizationId;
332
- proposal.authorizationQuote = authorizationQuote;
283
+ proposal.authorizationText = authorizationText;
333
284
  proposal.violatedRule = violatedRule;
334
285
  }
335
286
  }
336
- else if (authorizationId !== "none" || authorizationQuote !== "none" || violatedRule !== "none") {
337
- errors.push("User Override fields must be none when no role dispatch is proposed.");
287
+ else if (authorizationText !== "none" || violatedRule !== "none") {
288
+ errors.push("User Authorization fields must be none when no role dispatch is proposed.");
338
289
  }
339
290
  if (errors.length > 0)
340
291
  throw progressValidationError(errors);
@@ -372,165 +323,211 @@ Requested Flow: ${proposal?.requestedFlow ?? "none"}
372
323
  Target Role: ${proposal?.targetRole ?? "none"}
373
324
  Evidence: ${proposal?.evidence ?? "none"}
374
325
 
375
- ## User Override
326
+ ## User Authorization
376
327
 
377
- Authorization ID: ${proposal?.authorizationId ?? "none"}
378
- Authorization Quote: ${proposal?.authorizationQuote ?? "none"}
328
+ Authorization Text: ${proposal?.authorizationText ?? "none"}
379
329
  Violated Rule: ${proposal?.violatedRule ?? "none"}
380
330
  `;
381
331
  }
382
- async function evaluateTransition(fs, input, current, effectiveFlow, targetRole) {
383
- const allowedTransitions = await getAllowedTransitions(fs, input, current);
332
+ async function evaluateTransition(fs, input, state, current, effectiveFlow, targetRole) {
333
+ const allowedTransitions = await getAllowedTransitions(fs, input, state, current);
384
334
  const signature = `${effectiveFlow}/${targetRole}`;
385
- if (allowedTransitions.includes(signature))
386
- return { allowed: true, reason: "allowed", allowedTransitions };
335
+ if (allowedTransitions.includes(signature)) {
336
+ return { allowed: true, reason: "allowed", allowedTransitions, blockedHint: "" };
337
+ }
387
338
  return {
388
339
  allowed: false,
389
340
  reason: `Transition ${signature} is not legal after the confirmed Workflow Progress history.`,
390
- allowedTransitions
341
+ allowedTransitions,
342
+ blockedHint: await describeBlockedCheckpoint(fs, input, state, current)
391
343
  };
392
344
  }
393
- async function getAllowedTransitions(fs, input, current) {
345
+ async function getAllowedTransitions(fs, input, state, current) {
394
346
  if (current.status === "completed")
395
- return [];
347
+ return initialTransitions();
396
348
  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
- ];
349
+ return initialTransitions();
404
350
  }
405
351
  const flow = current.flow;
352
+ const flowRun = resolveFlowRun(state.flowRun, current);
406
353
  if (flow === "docs-only") {
407
354
  const docs = await artifactState(fs, input, "docs-sync-report.md", "docs-sync-report");
408
- return docs.value === "synced" || docs.value === "unchanged"
355
+ return evidenceIsFresh(state, flow, "architect", "docs-sync-report.md", docs.hash)
356
+ && docs.complete && (docs.value === "synced" || docs.value === "unchanged")
409
357
  ? []
410
358
  : ["docs-only/architect", "code-change/architect", "validation-only/tester"];
411
359
  }
412
360
  if (flow === "validation-only") {
413
361
  const test = await artifactState(fs, input, "test-report.md", "test-report");
362
+ if (!evidenceIsFresh(state, flow, "tester", "test-report.md", test.hash))
363
+ return ["validation-only/tester"];
414
364
  if (test.infrastructure === "production-change-required")
415
365
  return ["code-change/architect"];
416
366
  if (test.value === "incomplete" || test.infrastructure === "repair-required")
417
367
  return ["validation-only/tester"];
418
368
  const validationGate = await gateState(fs, input, "validation-adequacy");
419
- if (validationGate?.decision === "request_changes")
369
+ if (freshGateDecision(state, flow, "tester", "validation-adequacy", validationGate) === "request_changes") {
420
370
  return ["validation-only/tester"];
421
- if (!gatePassed(validationGate))
371
+ }
372
+ if (!gatePassedForDispatch(state, flow, "tester", "validation-adequacy", validationGate))
422
373
  return [];
423
374
  return [];
424
375
  }
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");
376
+ const segment = activeFlowSegment(current.history, flow, flowRun.startedAtSequence);
377
+ if (flow === "code-change") {
378
+ return allowedCodeChange(fs, input, state, segment, flowRun.resumedFromBranch !== undefined);
379
+ }
380
+ if (flow === "architect-debug") {
381
+ return allowedArchitectFix(fs, input, state, segment, "architect-debug", flowRun.resumedFromBranch !== undefined, flowRun.activeBranch ? flowRun.rootFlow : undefined);
382
+ }
383
+ return allowedArchitectFix(fs, input, state, segment, "architecture-diagnosis", false, flowRun.activeBranch ? flowRun.rootFlow : undefined);
431
384
  }
432
- async function allowedCodeChange(fs, input, segment) {
385
+ function initialTransitions() {
386
+ return [
387
+ "code-change/architect",
388
+ "architect-debug/architect",
389
+ "architecture-diagnosis/architect",
390
+ "docs-only/architect",
391
+ "validation-only/tester"
392
+ ];
393
+ }
394
+ async function allowedCodeChange(fs, input, state, segment, resumedFromBranch) {
433
395
  const coderIndex = findLastIndex(segment, (entry) => entry.targetRole === "coder");
434
396
  const testerIndex = findLastIndex(segment, (entry) => entry.targetRole === "tester");
397
+ if (resumedFromBranch && coderIndex < 0 && testerIndex < 0) {
398
+ return allowedPostImplementationArchitect(fs, input, state, "code-change", segment);
399
+ }
435
400
  if (coderIndex < 0) {
436
401
  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")
402
+ if (!evidenceIsFresh(state, "code-change", "architect", "architecture-plan.md", plan.hash)
403
+ || plan.value !== "complete")
439
404
  return ["code-change/architect"];
440
- if (gate?.decision === "request_changes")
405
+ const gate = await gateState(fs, input, "architecture-plan");
406
+ if (freshGateDecision(state, "code-change", "architect", "architecture-plan", gate) === "request_changes") {
441
407
  return ["code-change/architect"];
442
- return gatePassed(gate) ? ["code-change/coder"] : [];
408
+ }
409
+ return gatePassedForDispatch(state, "code-change", "architect", "architecture-plan", gate)
410
+ ? ["code-change/coder"]
411
+ : [];
443
412
  }
444
413
  if (testerIndex < coderIndex) {
445
414
  const coder = await artifactState(fs, input, "coder-completion.md", "coder-completion");
415
+ if (!evidenceIsFresh(state, "code-change", "coder", "coder-completion.md", coder.hash)) {
416
+ return ["code-change/coder"];
417
+ }
446
418
  if (coder.value === "failed")
447
419
  return ["architect-debug/architect"];
448
420
  return coder.value === "ready_for_review" ? ["code-change/tester"] : ["code-change/coder"];
449
421
  }
450
422
  const architectsAfterTester = segment.filter((entry, index) => index > testerIndex && entry.targetRole === "architect");
451
423
  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"];
424
+ return allowedPostImplementationArchitect(fs, input, state, "code-change", architectsAfterTester);
467
425
  }
468
- return allowedAfterTester(fs, input, "coder", {
426
+ return allowedAfterTester(fs, input, state, "coder", {
469
427
  testerFailureFlow: "architect-debug",
470
428
  implementationFailureFlow: "architect-debug",
471
429
  successTarget: "code-change/architect"
472
430
  });
473
431
  }
474
- async function allowedArchitectureFollowup(fs, input, followupDispatchedAt) {
432
+ async function allowedArchitectureFollowup(fs, input, state, followupDispatchedAt) {
475
433
  const plan = await artifactState(fs, input, "architecture-plan.md", "architecture-plan");
476
- if (plan.value !== "complete")
434
+ if (!evidenceIsFresh(state, "code-change", "architect", "architecture-plan.md", plan.hash)
435
+ || plan.value !== "complete")
477
436
  return ["code-change/architect"];
478
437
  const gate = await gateState(fs, input, "architecture-plan");
479
438
  if (!gate || !followupDispatchedAt || gate.updatedAt <= followupDispatchedAt)
480
439
  return [];
481
- if (gate.decision === "request_changes")
440
+ if (freshGateDecision(state, "code-change", "architect", "architecture-plan", gate) === "request_changes") {
482
441
  return ["code-change/architect"];
483
- return gatePassed(gate) ? ["code-change/coder"] : [];
442
+ }
443
+ return gatePassedForDispatch(state, "code-change", "architect", "architecture-plan", gate)
444
+ ? ["code-change/coder"]
445
+ : [];
484
446
  }
485
- async function allowedArchitectFix(fs, input, current, segment, source) {
447
+ async function allowedArchitectFix(fs, input, state, segment, source, resumedFromBranch, parentFlow) {
486
448
  const architectIndex = findLastIndex(segment, (entry) => entry.targetRole === "architect");
487
449
  const testerIndex = findLastIndex(segment, (entry) => entry.targetRole === "tester");
450
+ if (resumedFromBranch && architectIndex >= 0 && testerIndex < 0) {
451
+ return allowedPostImplementationArchitect(fs, input, state, source, segment);
452
+ }
488
453
  if (architectIndex < 0)
489
454
  return [`${source}/architect`];
490
455
  if (testerIndex < 0) {
491
456
  const artifact = source === "architect-debug"
492
457
  ? await artifactState(fs, input, "architect-debug.md", "architect-debug")
493
458
  : await artifactState(fs, input, "architecture-diagnosis.md", "architecture-diagnosis");
459
+ const artifactName = source === "architect-debug" ? "architect-debug.md" : "architecture-diagnosis.md";
460
+ if (!evidenceIsFresh(state, source, "architect", artifactName, artifact.hash)) {
461
+ return [`${source}/architect`];
462
+ }
494
463
  if (source === "architect-debug" && artifact.disposition === "normal architecture plan required") {
495
464
  return ["code-change/architect"];
496
465
  }
497
466
  return artifact.complete ? [`${source}/tester`] : [`${source}/architect`];
498
467
  }
499
468
  if (architectIndex > testerIndex) {
500
- const codeDiff = await gateState(fs, input, "code-diff");
501
- if (codeDiff?.decision === "request_changes")
469
+ const artifactName = source === "architect-debug" ? "architect-debug.md" : "architecture-diagnosis.md";
470
+ const artifact = source === "architect-debug"
471
+ ? await artifactState(fs, input, artifactName, "architect-debug")
472
+ : await artifactState(fs, input, artifactName, "architecture-diagnosis");
473
+ if (evidenceIsFresh(state, source, "architect", artifactName, artifact.hash) && artifact.complete) {
502
474
  return [`${source}/tester`];
475
+ }
503
476
  const docs = await artifactState(fs, input, "docs-sync-report.md", "docs-sync-report");
504
- return docs.value === "synced" || docs.value === "unchanged" ? [] : [`${source}/architect`];
477
+ return evidenceIsFresh(state, source, "architect", "docs-sync-report.md", docs.hash)
478
+ && (docs.value === "synced" || docs.value === "unchanged")
479
+ ? []
480
+ : [`${source}/architect`];
505
481
  }
506
- const parentCodeChange = current.history.slice(0, -segment.length).some((entry) => entry.flow === "code-change");
507
- return allowedAfterTester(fs, input, source, {
482
+ return allowedAfterTester(fs, input, state, source, {
508
483
  testerFailureFlow: source === "architect-debug" ? "architecture-diagnosis" : undefined,
509
484
  implementationFailureFlow: source,
510
- successTarget: parentCodeChange ? "code-change/architect" : `${source}/architect`
485
+ successTarget: parentFlow ? `${parentFlow}/architect` : `${source}/architect`
511
486
  });
512
487
  }
513
- async function allowedAfterTester(fs, input, codeSource, options) {
488
+ async function allowedPostImplementationArchitect(fs, input, state, flow, architectEntries) {
489
+ const docs = await artifactState(fs, input, "docs-sync-report.md", "docs-sync-report");
490
+ const acceptance = await artifactState(fs, input, "final-acceptance.md", "final-acceptance");
491
+ if (flow === "code-change" && architectEntries.length > 1 && acceptance.value === "needs-architect-follow-up") {
492
+ return allowedArchitectureFollowup(fs, input, state, architectEntries[1]?.confirmedAt);
493
+ }
494
+ if (evidenceIsFresh(state, flow, "architect", "docs-sync-report.md", docs.hash)
495
+ && (docs.value === "synced" || docs.value === "unchanged")) {
496
+ if (acceptance.value === "needs-coder-follow-up" && flow === "code-change")
497
+ return ["code-change/coder"];
498
+ if (acceptance.value === "needs-docs-sync")
499
+ return [`${flow}/architect`];
500
+ if (acceptance.value === "needs-architect-follow-up")
501
+ return [`${flow}/architect`];
502
+ return [];
503
+ }
504
+ return [`${flow}/architect`];
505
+ }
506
+ async function allowedAfterTester(fs, input, state, codeSource, options) {
514
507
  const test = await artifactState(fs, input, "test-report.md", "test-report");
515
508
  const currentFlow = codeSource === "coder" ? "code-change" : codeSource;
509
+ if (!evidenceIsFresh(state, currentFlow, "tester", "test-report.md", test.hash)) {
510
+ return [`${currentFlow}/tester`];
511
+ }
516
512
  if (test.value === "incomplete" || test.infrastructure === "repair-required")
517
513
  return [`${currentFlow}/tester`];
518
514
  if (test.value === "fail" && test.infrastructure !== "repair-required") {
519
515
  return options.testerFailureFlow ? [`${options.testerFailureFlow}/architect`] : [];
520
516
  }
521
517
  const validation = await gateState(fs, input, "validation-adequacy");
522
- if (validation?.decision === "request_changes")
518
+ if (freshGateDecision(state, currentFlow, "tester", "validation-adequacy", validation) === "request_changes") {
523
519
  return [`${currentFlow}/tester`];
524
- if (!gatePassed(validation))
520
+ }
521
+ if (!gatePassedForDispatch(state, currentFlow, "tester", "validation-adequacy", validation))
525
522
  return [];
526
523
  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));
524
+ if (freshGateDecision(state, currentFlow, "tester", "code-diff", codeDiff) === "request_changes") {
525
+ const scopes = new Set(codeDiff?.findings?.map((finding) => finding.scope).filter(Boolean));
529
526
  return scopes.size === 1 && scopes.has("test-only")
530
527
  ? [`${currentFlow}/tester`]
531
528
  : options.implementationFailureFlow ? [`${options.implementationFailureFlow}/architect`] : [];
532
529
  }
533
- if (!gatePassed(codeDiff))
530
+ if (!gatePassedForDispatch(state, currentFlow, "tester", "code-diff", codeDiff))
534
531
  return [];
535
532
  const gateCodeSource = codeSource === "architecture-diagnosis" ? "architect-diagnosis" : codeSource;
536
533
  const reviewedSources = codeDiff?.codeDiffSources ?? (codeDiff?.codeDiffSource ? [codeDiff.codeDiffSource] : []);
@@ -542,7 +539,7 @@ async function artifactState(fs, input, fileName, kind) {
542
539
  const relative = path.posix.join(input.handoffDir, fileName);
543
540
  const absolute = resolveRepoPath(input.taskRepoRoot, relative);
544
541
  if (!(await fs.pathExists(absolute)))
545
- return { complete: false };
542
+ return { complete: false, hash: MISSING_EVIDENCE_HASH };
546
543
  const content = await fs.readText(absolute);
547
544
  const check = checkMarkdownArtifact(kind, relative, content, { mode: "final" });
548
545
  const inline = (name) => new RegExp(`^${name}:\\s*(.+?)\\s*$`, "mi").exec(content)?.[1]?.trim().toLowerCase();
@@ -565,7 +562,13 @@ async function artifactState(fs, input, fileName, kind) {
565
562
  const disposition = kind === "architect-debug" || kind === "architecture-diagnosis"
566
563
  ? readArtifactSectionContent(content, "Final Disposition")?.trim().toLowerCase()
567
564
  : undefined;
568
- return { complete: check.status === "ok", value, infrastructure, disposition };
565
+ return {
566
+ complete: check.status === "ok",
567
+ hash: contentHash(content),
568
+ value,
569
+ infrastructure,
570
+ disposition
571
+ };
569
572
  }
570
573
  async function gateState(fs, input, gate) {
571
574
  const target = resolveRepoPath(input.taskRepoRoot, path.posix.join(".ai/vcm/gate-reviews", "index.json"));
@@ -579,46 +582,231 @@ async function gateState(fs, input, gate) {
579
582
  return undefined;
580
583
  }
581
584
  }
582
- function gatePassed(record) {
583
- return Boolean(record && (FINAL_GATE_STATUSES.has(record.status)
584
- || (record.status === "completed" && record.decision === "approve")));
585
+ async function captureEvidenceBaseline(fs, input, sequence, flow, targetRole, confirmedAt) {
586
+ const artifactEntries = await Promise.all(WORKFLOW_EVIDENCE_ARTIFACTS.map(async (fileName) => {
587
+ const target = resolveRepoPath(input.taskRepoRoot, path.posix.join(input.handoffDir, fileName));
588
+ const hash = await fs.pathExists(target) ? contentHash(await fs.readText(target)) : MISSING_EVIDENCE_HASH;
589
+ return [fileName, hash];
590
+ }));
591
+ const gateEntries = await Promise.all(WORKFLOW_EVIDENCE_GATES.map(async (gate) => [
592
+ gate,
593
+ gateFingerprint(await gateState(fs, input, gate))
594
+ ]));
595
+ return {
596
+ sequence,
597
+ flow,
598
+ targetRole,
599
+ artifactHashes: Object.fromEntries(artifactEntries),
600
+ gateFingerprints: Object.fromEntries(gateEntries),
601
+ confirmedAt
602
+ };
585
603
  }
586
- async function validateCompletion(fs, input, candidate) {
604
+ async function advanceFlowRun(fs, input, state, current, effectiveFlow, usedOverride, nextSequence) {
605
+ if (!current.flow || current.status === "completed" || (usedOverride && effectiveFlow !== current.flow)) {
606
+ return newFlowRun(effectiveFlow, nextSequence);
607
+ }
608
+ const run = resolveFlowRun(state.flowRun, current);
609
+ if (effectiveFlow === current.flow)
610
+ return run;
611
+ if (current.flow === "code-change" && effectiveFlow === "architect-debug" && run.rootFlow === "code-change") {
612
+ return { ...run, activeBranch: "architect-debug", resumedFromBranch: undefined };
613
+ }
614
+ if (current.flow === "architect-debug" && effectiveFlow === "architecture-diagnosis") {
615
+ if (run.rootFlow === "code-change" && run.activeBranch === "architect-debug") {
616
+ return { ...run, activeBranch: "architecture-diagnosis", resumedFromBranch: undefined };
617
+ }
618
+ if (run.rootFlow === "architect-debug" && !run.activeBranch) {
619
+ return { ...run, activeBranch: "architecture-diagnosis", resumedFromBranch: undefined };
620
+ }
621
+ }
622
+ if (run.activeBranch === current.flow && effectiveFlow === run.rootFlow) {
623
+ if (current.flow === "architect-debug" && effectiveFlow === "code-change") {
624
+ const debug = await artifactState(fs, input, "architect-debug.md", "architect-debug");
625
+ if (debug.disposition === "normal architecture plan required") {
626
+ return newFlowRun("code-change", nextSequence);
627
+ }
628
+ }
629
+ return {
630
+ ...run,
631
+ activeBranch: undefined,
632
+ resumedFromBranch: current.flow
633
+ };
634
+ }
635
+ return newFlowRun(effectiveFlow, nextSequence);
636
+ }
637
+ function newFlowRun(flow, startedAtSequence) {
638
+ return { rootFlow: flow, startedAtSequence };
639
+ }
640
+ function resolveFlowRun(recorded, current) {
641
+ if (recorded && current.flow
642
+ && (recorded.rootFlow === current.flow || recorded.activeBranch === current.flow)) {
643
+ return recorded;
644
+ }
645
+ if (!current.flow || current.history.length === 0) {
646
+ return newFlowRun(current.flow ?? "code-change", 1);
647
+ }
648
+ const segments = flowHistorySegments(current.history);
649
+ const last = segments.at(-1);
650
+ const previous = segments.at(-2);
651
+ const grandparent = segments.at(-3);
652
+ if (last.flow === "architect-debug" && previous?.flow === "code-change") {
653
+ return {
654
+ rootFlow: "code-change",
655
+ activeBranch: "architect-debug",
656
+ startedAtSequence: previous.startedAtSequence
657
+ };
658
+ }
659
+ if (last.flow === "architecture-diagnosis" && previous?.flow === "architect-debug") {
660
+ return {
661
+ rootFlow: grandparent?.flow === "code-change" ? "code-change" : "architect-debug",
662
+ activeBranch: "architecture-diagnosis",
663
+ startedAtSequence: grandparent?.flow === "code-change"
664
+ ? grandparent.startedAtSequence
665
+ : previous.startedAtSequence
666
+ };
667
+ }
668
+ if (last.flow === "architecture-diagnosis" && previous?.flow === "code-change") {
669
+ return {
670
+ rootFlow: "code-change",
671
+ activeBranch: "architecture-diagnosis",
672
+ startedAtSequence: previous.startedAtSequence
673
+ };
674
+ }
675
+ if (last.flow === "code-change" && previous
676
+ && (previous.flow === "architect-debug" || previous.flow === "architecture-diagnosis")
677
+ && grandparent?.flow === "code-change") {
678
+ return {
679
+ rootFlow: "code-change",
680
+ resumedFromBranch: previous.flow,
681
+ startedAtSequence: grandparent.startedAtSequence
682
+ };
683
+ }
684
+ return newFlowRun(last.flow, last.startedAtSequence);
685
+ }
686
+ function flowHistorySegments(history) {
687
+ const segments = [];
688
+ for (const entry of history) {
689
+ if (segments.at(-1)?.flow !== entry.flow) {
690
+ segments.push({ flow: entry.flow, startedAtSequence: entry.sequence });
691
+ }
692
+ }
693
+ return segments;
694
+ }
695
+ function activeFlowSegment(history, flow, startedAtSequence) {
696
+ const runStart = history.findIndex((entry) => entry.sequence >= startedAtSequence);
697
+ const lastDifferent = findLastIndex(history, (entry) => entry.flow !== flow);
698
+ const start = Math.max(runStart < 0 ? 0 : runStart, lastDifferent + 1);
699
+ return history.slice(start);
700
+ }
701
+ function evidenceIsFresh(state, flow, targetRole, artifact, currentHash) {
702
+ const baseline = matchingEvidenceBaseline(state, flow, targetRole);
703
+ return !baseline || baseline.artifactHashes[artifact] !== currentHash;
704
+ }
705
+ function freshGateDecision(state, flow, targetRole, gate, record) {
706
+ const baseline = matchingEvidenceBaseline(state, flow, targetRole);
707
+ if (baseline && baseline.gateFingerprints[gate] === gateFingerprint(record))
708
+ return undefined;
709
+ return record?.decision;
710
+ }
711
+ function gatePassedForDispatch(state, flow, targetRole, gate, record) {
712
+ if (!record)
713
+ return false;
714
+ if (FINAL_GATE_STATUSES.has(record.status))
715
+ return true;
716
+ return freshGateDecision(state, flow, targetRole, gate, record) === "approve"
717
+ && record.status === "completed";
718
+ }
719
+ function matchingEvidenceBaseline(state, flow, targetRole) {
720
+ const baseline = state.activeDispatch;
721
+ return baseline?.flow === flow && baseline.targetRole === targetRole ? baseline : undefined;
722
+ }
723
+ function gateFingerprint(record) {
724
+ if (!record)
725
+ return MISSING_EVIDENCE_HASH;
726
+ return contentHash(JSON.stringify({
727
+ requestId: record.requestId,
728
+ inputHash: record.inputHash,
729
+ status: record.status,
730
+ decision: record.decision,
731
+ codeDiffSource: record.codeDiffSource,
732
+ codeDiffSources: record.codeDiffSources,
733
+ findings: record.findings,
734
+ completedAt: record.completedAt
735
+ }));
736
+ }
737
+ async function describeBlockedCheckpoint(fs, input, state, current) {
738
+ const baseline = state.activeDispatch;
739
+ if (!baseline || !current.flow) {
740
+ return "No role dispatch is legal at this checkpoint. Complete the required current Gate or PM-only step first.";
741
+ }
742
+ if (baseline.targetRole === "tester") {
743
+ const test = await artifactState(fs, input, "test-report.md", "test-report");
744
+ if (!evidenceIsFresh(state, baseline.flow, "tester", "test-report.md", test.hash)) {
745
+ 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.";
746
+ }
747
+ if (baseline.flow === "architecture-diagnosis" && test.value === "fail") {
748
+ 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.";
749
+ }
750
+ return "The latest Tester result is waiting for its current validation-adequacy or code-diff Gate result.";
751
+ }
752
+ if (baseline.targetRole === "architect") {
753
+ return "The latest Architect dispatch has not produced the fresh workflow artifact required for its next step.";
754
+ }
755
+ if (baseline.targetRole === "coder") {
756
+ return "The latest Coder dispatch has not produced a fresh coder-completion.md.";
757
+ }
758
+ return "No role dispatch is legal at this checkpoint. Complete the required current Gate or PM-only step first.";
759
+ }
760
+ async function validateCompletion(fs, input, state, candidate) {
587
761
  if (!candidate.flow || candidate.history.length === 0) {
588
762
  throw workflowError("WORKFLOW_COMPLETION_INVALID", "An unstarted workflow cannot be completed.");
589
763
  }
764
+ const flowRun = resolveFlowRun(state.flowRun, candidate);
765
+ if (flowRun.activeBranch === candidate.flow) {
766
+ throw workflowError("WORKFLOW_COMPLETION_INVALID", `${candidate.flow} is an active branch and must return to ${flowRun.rootFlow} before completion.`);
767
+ }
590
768
  if (candidate.flow === "code-change" || candidate.flow === "architect-debug") {
591
769
  const acceptance = await artifactState(fs, input, "final-acceptance.md", "final-acceptance");
592
- if (acceptance.value !== "accepted" && acceptance.value !== "accepted-with-known-risks") {
770
+ if (!evidenceProducedAfterActiveDispatch(state, candidate.flow, "architect", "final-acceptance.md", acceptance.hash)
771
+ || (acceptance.value !== "accepted" && acceptance.value !== "accepted-with-known-risks")) {
593
772
  throw workflowError("WORKFLOW_COMPLETION_INVALID", "Final Acceptance is not accepted for this complete delivery flow.");
594
773
  }
595
774
  return;
596
775
  }
597
776
  if (candidate.flow === "architecture-diagnosis") {
598
777
  const diagnosis = await artifactState(fs, input, "architecture-diagnosis.md", "architecture-diagnosis");
599
- if (diagnosis.disposition === "analysis completed")
778
+ if (diagnosis.disposition === "analysis completed"
779
+ && evidenceProducedAfterActiveDispatch(state, candidate.flow, "architect", "architecture-diagnosis.md", diagnosis.hash))
600
780
  return;
601
781
  const acceptance = await artifactState(fs, input, "final-acceptance.md", "final-acceptance");
602
- if (acceptance.value !== "accepted" && acceptance.value !== "accepted-with-known-risks") {
782
+ if (!evidenceProducedAfterActiveDispatch(state, candidate.flow, "architect", "final-acceptance.md", acceptance.hash)
783
+ || (acceptance.value !== "accepted" && acceptance.value !== "accepted-with-known-risks")) {
603
784
  throw workflowError("WORKFLOW_COMPLETION_INVALID", "Implemented Architecture Diagnosis requires accepted Final Acceptance evidence.");
604
785
  }
605
786
  return;
606
787
  }
607
788
  if (candidate.flow === "docs-only") {
608
789
  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.");
790
+ if (!evidenceProducedAfterActiveDispatch(state, candidate.flow, "architect", "docs-sync-report.md", docs.hash)
791
+ || !docs.complete || (docs.value !== "synced" && docs.value !== "unchanged")) {
792
+ throw workflowError("WORKFLOW_COMPLETION_INVALID", "Docs-only completion requires a complete Docs Sync Report with Decision: synced or Decision: unchanged.");
611
793
  }
612
794
  return;
613
795
  }
614
796
  if (candidate.flow === "validation-only") {
615
797
  const test = await artifactState(fs, input, "test-report.md", "test-report");
616
798
  const gate = await gateState(fs, input, "validation-adequacy");
617
- if ((test.value !== "pass" && test.value !== "fail") || !gatePassed(gate)) {
799
+ if (!evidenceProducedAfterActiveDispatch(state, candidate.flow, "tester", "test-report.md", test.hash)
800
+ || (test.value !== "pass" && test.value !== "fail")
801
+ || !gatePassedForDispatch(state, candidate.flow, "tester", "validation-adequacy", gate)) {
618
802
  throw workflowError("WORKFLOW_COMPLETION_INVALID", "Validation-only completion requires a terminal Test Report and a passed Validation Adequacy Gate.");
619
803
  }
620
804
  }
621
805
  }
806
+ function evidenceProducedAfterActiveDispatch(state, flow, targetRole, artifact, currentHash) {
807
+ const baseline = matchingEvidenceBaseline(state, flow, targetRole);
808
+ return Boolean(baseline && baseline.artifactHashes[artifact] !== currentHash);
809
+ }
622
810
  function validateCandidateAgainstCurrent(current, candidate) {
623
811
  const errors = [];
624
812
  if (candidate.revision !== current.revision + 1)
@@ -632,26 +820,12 @@ function validateCandidateAgainstCurrent(current, candidate) {
632
820
  if (errors.length > 0)
633
821
  throw progressValidationError(errors);
634
822
  }
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) {
823
+ function createUserAuthorization(authorizationId, current, candidate, effectiveFlow, baseHistoryHash, violation, authorizationText, timestamp) {
652
824
  return {
653
- id: overrideId,
654
- status: "pending",
825
+ id: authorizationId,
826
+ status: "accepted",
827
+ role: "project-manager",
828
+ operation: "workflow-dispatch",
655
829
  baseRevision: current.revision,
656
830
  baseHistoryHash,
657
831
  requestedFlow: candidate.proposal.requestedFlow,
@@ -659,7 +833,7 @@ function createOverrideRequest(overrideId, current, candidate, effectiveFlow, ba
659
833
  targetRole: candidate.proposal.targetRole,
660
834
  evidence: candidate.proposal.evidence,
661
835
  violatedRule: violation,
662
- proposedAuthorizationQuote: quote,
836
+ authorizationText,
663
837
  createdAt: timestamp
664
838
  };
665
839
  }
@@ -725,24 +899,43 @@ function normalizeState(value, taskSlug, timestamp) {
725
899
  const pendingDispatch = value.pendingDispatch === null
726
900
  ? null
727
901
  : 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) {
902
+ const userAuthorizations = Array.isArray(value.userAuthorizations)
903
+ && value.userAuthorizations.every(isUserAuthorization)
904
+ ? value.userAuthorizations
905
+ : value.userAuthorizations === undefined && Array.isArray(value.overrideRequests)
906
+ ? []
907
+ : undefined;
908
+ const activeDispatch = value.activeDispatch === undefined || value.activeDispatch === null
909
+ ? null
910
+ : normalizeDispatchEvidenceBaseline(value.activeDispatch);
911
+ const flowRun = value.flowRun === undefined || value.flowRun === null
912
+ ? null
913
+ : isFlowRun(value.flowRun) ? value.flowRun : undefined;
914
+ if (pendingDispatch === undefined || activeDispatch === undefined || flowRun === undefined || userAuthorizations === undefined) {
733
915
  return { ...emptyState(taskSlug, timestamp), warnings: ["Workflow control state has an unsupported shape."] };
734
916
  }
735
917
  return {
736
918
  version: 1,
737
919
  taskSlug,
738
920
  pendingDispatch,
739
- overrideRequests,
921
+ activeDispatch,
922
+ flowRun,
923
+ userAuthorizations,
740
924
  warnings: [],
741
925
  updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : timestamp
742
926
  };
743
927
  }
744
928
  function emptyState(taskSlug, timestamp) {
745
- return { version: 1, taskSlug, pendingDispatch: null, overrideRequests: [], warnings: [], updatedAt: timestamp };
929
+ return {
930
+ version: 1,
931
+ taskSlug,
932
+ pendingDispatch: null,
933
+ activeDispatch: null,
934
+ flowRun: null,
935
+ userAuthorizations: [],
936
+ warnings: [],
937
+ updatedAt: timestamp
938
+ };
746
939
  }
747
940
  function resolveEffectiveFlow(current, requested) {
748
941
  if (requested)
@@ -764,7 +957,10 @@ function progressPath(input) {
764
957
  return resolveRepoPath(input.taskRepoRoot, relativeProgressPath(input));
765
958
  }
766
959
  function historyHash(history) {
767
- return createHash("sha256").update(JSON.stringify(history)).digest("hex");
960
+ return contentHash(JSON.stringify(history));
961
+ }
962
+ function contentHash(content) {
963
+ return createHash("sha256").update(content).digest("hex");
768
964
  }
769
965
  function field(content, name) {
770
966
  return rawField(content, name)?.toLowerCase();
@@ -824,11 +1020,13 @@ function isPendingDispatch(value) {
824
1020
  && typeof value.createdAt === "string"
825
1021
  && typeof value.updatedAt === "string";
826
1022
  }
827
- function isOverrideRequest(value) {
1023
+ function isUserAuthorization(value) {
828
1024
  if (!isRecord(value))
829
1025
  return false;
830
1026
  return typeof value.id === "string"
831
- && ["pending", "approved", "rejected", "consumed"].includes(String(value.status))
1027
+ && (value.status === "accepted" || value.status === "consumed")
1028
+ && value.role === "project-manager"
1029
+ && value.operation === "workflow-dispatch"
832
1030
  && Number.isInteger(value.baseRevision)
833
1031
  && typeof value.baseHistoryHash === "string"
834
1032
  && (value.requestedFlow === undefined || Boolean(asFlow(typeof value.requestedFlow === "string" ? value.requestedFlow : undefined)))
@@ -836,12 +1034,50 @@ function isOverrideRequest(value) {
836
1034
  && Boolean(asTargetRole(typeof value.targetRole === "string" ? value.targetRole : undefined))
837
1035
  && typeof value.evidence === "string"
838
1036
  && typeof value.violatedRule === "string"
839
- && typeof value.proposedAuthorizationQuote === "string"
840
- && (value.authorizationText === undefined || typeof value.authorizationText === "string")
1037
+ && typeof value.authorizationText === "string"
841
1038
  && typeof value.createdAt === "string"
842
- && (value.decidedAt === undefined || typeof value.decidedAt === "string")
843
1039
  && (value.consumedAt === undefined || typeof value.consumedAt === "string");
844
1040
  }
1041
+ function normalizeDispatchEvidenceBaseline(value) {
1042
+ if (!isRecord(value) || !isRecord(value.artifactHashes) || !isRecord(value.gateFingerprints))
1043
+ return undefined;
1044
+ const artifactHashes = value.artifactHashes;
1045
+ const gateFingerprints = value.gateFingerprints;
1046
+ if (!(Number.isInteger(value.sequence)
1047
+ && Boolean(asFlow(typeof value.flow === "string" ? value.flow : undefined))
1048
+ && Boolean(asTargetRole(typeof value.targetRole === "string" ? value.targetRole : undefined))
1049
+ && WORKFLOW_EVIDENCE_GATES.every((gate) => typeof gateFingerprints[gate] === "string")
1050
+ && typeof value.confirmedAt === "string"))
1051
+ return undefined;
1052
+ return {
1053
+ sequence: Number(value.sequence),
1054
+ flow: value.flow,
1055
+ targetRole: value.targetRole,
1056
+ artifactHashes: Object.fromEntries(WORKFLOW_EVIDENCE_ARTIFACTS.map((artifact) => [
1057
+ artifact,
1058
+ typeof artifactHashes[artifact] === "string" ? artifactHashes[artifact] : MISSING_EVIDENCE_HASH
1059
+ ])),
1060
+ gateFingerprints: Object.fromEntries(WORKFLOW_EVIDENCE_GATES.map((gate) => [
1061
+ gate,
1062
+ gateFingerprints[gate]
1063
+ ])),
1064
+ confirmedAt: value.confirmedAt
1065
+ };
1066
+ }
1067
+ function isFlowRun(value) {
1068
+ if (!isRecord(value))
1069
+ return false;
1070
+ const rootFlow = asFlow(typeof value.rootFlow === "string" ? value.rootFlow : undefined);
1071
+ const activeBranch = value.activeBranch;
1072
+ const resumedFromBranch = value.resumedFromBranch;
1073
+ return Boolean(rootFlow)
1074
+ && (activeBranch === undefined || activeBranch === "architect-debug" || activeBranch === "architecture-diagnosis")
1075
+ && (resumedFromBranch === undefined
1076
+ || resumedFromBranch === "architect-debug"
1077
+ || resumedFromBranch === "architecture-diagnosis")
1078
+ && Number.isInteger(value.startedAtSequence)
1079
+ && Number(value.startedAtSequence) >= 1;
1080
+ }
845
1081
  function findLastIndex(values, predicate) {
846
1082
  for (let index = values.length - 1; index >= 0; index -= 1) {
847
1083
  if (predicate(values[index]))