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.
- package/README.md +10 -5
- package/dist/backend/api/task-routes.js +31 -2
- package/dist/backend/api/workflow-control-routes.js +0 -31
- package/dist/backend/cli/install-vcm-harness.js +40 -6
- package/dist/backend/role-tool-policy.js +1 -1
- package/dist/backend/server.js +12 -5
- package/dist/backend/services/artifact-service.js +3 -2
- package/dist/backend/services/auto-memory-service.js +2 -2
- package/dist/backend/services/claude-hook-service.js +103 -4
- package/dist/backend/services/harness-feedback-service.js +105 -3
- package/dist/backend/services/harness-service.js +38 -6
- package/dist/backend/services/memory-review-paths.js +13 -0
- package/dist/backend/services/role-stall-detector-service.js +322 -0
- package/dist/backend/services/round-service.js +25 -0
- package/dist/backend/services/runtime-coordinator-service.js +10 -0
- package/dist/backend/services/session-service.js +70 -3
- package/dist/backend/services/workflow-control-service.js +439 -203
- package/dist/backend/templates/handoff.js +2 -3
- package/dist/backend/templates/harness/architect-agent.js +2 -2
- package/dist/backend/templates/harness/coder-agent.js +4 -5
- package/dist/backend/templates/harness/gate-review.js +7 -9
- package/dist/backend/templates/harness/harness-engineer-agent.js +18 -8
- package/dist/backend/templates/harness/project-manager-agent.js +5 -5
- package/dist/backend/templates/harness/vcm-code-navigation-skill.js +6 -7
- package/dist/backend/templates/harness/vcm-workflow-review-skill.js +7 -9
- package/dist/shared/types/role-stall.js +1 -0
- package/dist/shared/types/workflow.js +14 -0
- package/dist/shared/validation/artifact-registry.js +1 -1
- package/dist-frontend/assets/{index-C_XHGNBD.css → index-B0d4Z6ny.css} +1 -1
- package/dist-frontend/assets/{index-Bocc2DWF.js → index-VW9tYPP5.js} +38 -38
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- 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 ?? (() => `
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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 (
|
|
78
|
-
|
|
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
|
-
|
|
83
|
-
throw workflowError("
|
|
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.
|
|
89
|
-
throw workflowError("
|
|
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
|
|
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
|
-
|
|
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
|
|
319
|
-
const
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
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 =
|
|
327
|
-
const allSet =
|
|
278
|
+
const allNone = authorizationText === "none" && violatedRule === "none";
|
|
279
|
+
const allSet = authorizationText !== "none" && violatedRule !== "none";
|
|
328
280
|
if (!allNone && !allSet)
|
|
329
|
-
errors.push("User
|
|
281
|
+
errors.push("User Authorization fields must both be none or both contain exact authorization evidence.");
|
|
330
282
|
if (allSet) {
|
|
331
|
-
proposal.
|
|
332
|
-
proposal.authorizationQuote = authorizationQuote;
|
|
283
|
+
proposal.authorizationText = authorizationText;
|
|
333
284
|
proposal.violatedRule = violatedRule;
|
|
334
285
|
}
|
|
335
286
|
}
|
|
336
|
-
else if (
|
|
337
|
-
errors.push("User
|
|
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
|
|
326
|
+
## User Authorization
|
|
376
327
|
|
|
377
|
-
Authorization
|
|
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
|
|
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
|
|
369
|
+
if (freshGateDecision(state, flow, "tester", "validation-adequacy", validationGate) === "request_changes") {
|
|
420
370
|
return ["validation-only/tester"];
|
|
421
|
-
|
|
371
|
+
}
|
|
372
|
+
if (!gatePassedForDispatch(state, flow, "tester", "validation-adequacy", validationGate))
|
|
422
373
|
return [];
|
|
423
374
|
return [];
|
|
424
375
|
}
|
|
425
|
-
const segment =
|
|
426
|
-
if (flow === "code-change")
|
|
427
|
-
return allowedCodeChange(fs, input,
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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
|
-
|
|
438
|
-
|
|
402
|
+
if (!evidenceIsFresh(state, "code-change", "architect", "architecture-plan.md", plan.hash)
|
|
403
|
+
|| plan.value !== "complete")
|
|
439
404
|
return ["code-change/architect"];
|
|
440
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
440
|
+
if (freshGateDecision(state, "code-change", "architect", "architecture-plan", gate) === "request_changes") {
|
|
482
441
|
return ["code-change/architect"];
|
|
483
|
-
|
|
442
|
+
}
|
|
443
|
+
return gatePassedForDispatch(state, "code-change", "architect", "architecture-plan", gate)
|
|
444
|
+
? ["code-change/coder"]
|
|
445
|
+
: [];
|
|
484
446
|
}
|
|
485
|
-
async function allowedArchitectFix(fs, input,
|
|
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
|
|
501
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
485
|
+
successTarget: parentFlow ? `${parentFlow}/architect` : `${source}/architect`
|
|
511
486
|
});
|
|
512
487
|
}
|
|
513
|
-
async function
|
|
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
|
|
518
|
+
if (freshGateDecision(state, currentFlow, "tester", "validation-adequacy", validation) === "request_changes") {
|
|
523
519
|
return [`${currentFlow}/tester`];
|
|
524
|
-
|
|
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
|
|
528
|
-
const scopes = new Set(codeDiff
|
|
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 (!
|
|
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 {
|
|
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
|
|
583
|
-
|
|
584
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
610
|
-
|
|
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 ((
|
|
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
|
|
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:
|
|
654
|
-
status: "
|
|
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
|
-
|
|
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
|
|
729
|
-
&& value.
|
|
730
|
-
? value.
|
|
731
|
-
: undefined
|
|
732
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
1023
|
+
function isUserAuthorization(value) {
|
|
828
1024
|
if (!isRecord(value))
|
|
829
1025
|
return false;
|
|
830
1026
|
return typeof value.id === "string"
|
|
831
|
-
&&
|
|
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.
|
|
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]))
|