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