vibe-coding-master 0.7.50 → 0.8.0
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/dist/backend/adapters/filesystem.js +6 -0
- package/dist/backend/api/task-routes.js +20 -1
- package/dist/backend/server.js +4 -2
- package/dist/backend/services/auto-memory-service.js +79 -28
- package/dist/backend/services/claude-hook-service.js +5 -3
- package/dist/backend/services/gate-review-service.js +21 -2
- package/dist/backend/services/harness-feedback-service.js +53 -15
- package/dist/backend/services/message-service.js +10 -0
- package/dist/backend/services/runtime-recovery-service.js +9 -0
- package/dist/backend/services/translation-service.js +74 -3
- package/dist/backend/services/workflow-control-service.js +436 -75
- package/dist/backend/templates/handoff.js +14 -2
- package/dist/backend/templates/harness/architect-agent.js +6 -5
- package/dist/backend/templates/harness/architect-evidence-worker-agent.js +2 -0
- package/dist/backend/templates/harness/architect-validation-worker-agent.js +1 -1
- package/dist/backend/templates/harness/claude-root.js +3 -3
- package/dist/backend/templates/harness/coder-agent.js +1 -0
- package/dist/backend/templates/harness/harness-engineer-agent.js +14 -11
- package/dist/backend/templates/harness/project-manager-agent.js +5 -1
- package/dist/backend/templates/harness/tester-agent.js +6 -0
- package/dist/backend/templates/harness/vcm-workflow-review-skill.js +17 -1
- package/dist/shared/validation/artifact-check.js +36 -3
- package/dist/shared/validation/artifact-contract.js +7 -0
- package/dist/shared/validation/artifact-registry.js +4 -1
- package/dist-frontend/assets/{index-DLsIPTvK.js → index-Dh7uVCmk.js} +1 -1
- package/dist-frontend/index.html +1 -1
- package/package.json +1 -1
- package/scripts/harness-tools/vcm-bash-guard +245 -21
|
@@ -4,11 +4,14 @@ import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
|
4
4
|
import { VcmError } from "../errors.js";
|
|
5
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
|
+
import { ARCHITECT_DEBUG_NORMAL_PLAN_DISPOSITION } from "../../shared/validation/artifact-contract.js";
|
|
7
8
|
import { renderWorkflowProgressTemplate } from "../templates/handoff.js";
|
|
8
|
-
const HISTORY_HEADER = "| Sequence | Flow | Target Role | Evidence | Override Authorization | Confirmed At |";
|
|
9
|
-
const HISTORY_SEPARATOR = "| --- | --- | --- | --- | --- | --- |";
|
|
9
|
+
const HISTORY_HEADER = "| Sequence | Flow | Target Role | Evidence | Override Authorization | Follow-Up Approval | Confirmed At |";
|
|
10
|
+
const HISTORY_SEPARATOR = "| --- | --- | --- | --- | --- | --- | --- |";
|
|
11
|
+
const LEGACY_HISTORY_HEADER = "| Sequence | Flow | Target Role | Evidence | Override Authorization | Confirmed At |";
|
|
12
|
+
const LEGACY_HISTORY_SEPARATOR = "| --- | --- | --- | --- | --- | --- |";
|
|
10
13
|
const TARGET_ROLES = new Set(["architect", "coder", "tester"]);
|
|
11
|
-
const
|
|
14
|
+
const FRESH_EXCEPTION_GATE_STATUSES = new Set(["not_required", "skipped", "overridden"]);
|
|
12
15
|
const MISSING_EVIDENCE_HASH = "<missing>";
|
|
13
16
|
const DOCS_ONLY_ROLE_TRANSITIONS = [
|
|
14
17
|
"docs-only/architect",
|
|
@@ -25,18 +28,50 @@ export function createWorkflowControlService(deps) {
|
|
|
25
28
|
const locks = new Map();
|
|
26
29
|
async function getState(input) {
|
|
27
30
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
await recoverCommitJournal(input);
|
|
32
|
+
const hasProgress = await deps.fs.pathExists(progressPath(input));
|
|
33
|
+
const hasState = await deps.fs.pathExists(statePath(input));
|
|
34
|
+
if (!hasProgress && !hasState) {
|
|
35
|
+
await writeAtomic(deps.fs, progressPath(input), renderWorkflowProgressTemplate(input.taskSlug));
|
|
36
|
+
return emptyState(input.taskSlug, now());
|
|
37
|
+
}
|
|
38
|
+
if (!hasProgress) {
|
|
39
|
+
const state = hasState
|
|
40
|
+
? normalizeState(await deps.fs.readJson(statePath(input)), input.taskSlug, now())
|
|
41
|
+
: emptyState(input.taskSlug, now());
|
|
42
|
+
return {
|
|
43
|
+
...state,
|
|
44
|
+
warnings: [...state.warnings, "Workflow Progress is missing while workflow runtime state still exists. Restore workflow-progress.md before continuing."]
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const progress = parseWorkflowProgress(await deps.fs.readText(progressPath(input)), input.taskSlug);
|
|
48
|
+
if (!hasState) {
|
|
49
|
+
if (progress.revision === 0 && progress.status === "not-started" && progress.history.length === 0 && !progress.proposal) {
|
|
50
|
+
return emptyState(input.taskSlug, now());
|
|
51
|
+
}
|
|
52
|
+
if (!progress.proposal && progress.history.length > 0) {
|
|
53
|
+
const recovered = await reconstructStateFromProgress(input, progress);
|
|
54
|
+
await saveState(input, recovered);
|
|
55
|
+
return recovered;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
...emptyState(input.taskSlug, now()),
|
|
59
|
+
warnings: ["Workflow runtime state is missing for an active Workflow Progress record. Restore workflow-control.json before continuing."]
|
|
60
|
+
};
|
|
32
61
|
}
|
|
33
62
|
const state = normalizeState(await deps.fs.readJson(statePath(input)), input.taskSlug, now());
|
|
34
|
-
return
|
|
63
|
+
return {
|
|
64
|
+
...state,
|
|
65
|
+
warnings: [...state.warnings, ...stateProgressConsistencyWarnings(state, progress)]
|
|
66
|
+
};
|
|
35
67
|
}
|
|
36
68
|
catch (error) {
|
|
69
|
+
const message = errorMessage(error);
|
|
37
70
|
return {
|
|
38
71
|
...emptyState(input.taskSlug, now()),
|
|
39
|
-
warnings: [
|
|
72
|
+
warnings: [message.startsWith("Workflow Progress validation failed:")
|
|
73
|
+
? `Workflow Progress is invalid and was preserved unchanged: ${message}`
|
|
74
|
+
: `Workflow control state could not be read: ${message}`]
|
|
40
75
|
};
|
|
41
76
|
}
|
|
42
77
|
}
|
|
@@ -66,9 +101,23 @@ export function createWorkflowControlService(deps) {
|
|
|
66
101
|
const baseHistoryHash = historyHash(current.history);
|
|
67
102
|
const effectiveFlow = resolveEffectiveFlow(current.flow, candidate.proposal.requestedFlow);
|
|
68
103
|
const verdict = await evaluateTransition(deps.fs, input, state, current, effectiveFlow, candidate.proposal.targetRole);
|
|
104
|
+
const followUpApprovalText = candidate.proposal.followUpApprovalText?.trim();
|
|
105
|
+
const followUpAllowed = Boolean(followUpApprovalText) && await canRouteUserApprovedFollowUp(deps.fs, input, state, current, effectiveFlow, candidate.proposal.targetRole, verdict.allowedTransitions);
|
|
69
106
|
let overrideAuthorizationId;
|
|
70
107
|
let userAuthorization;
|
|
71
|
-
|
|
108
|
+
let followUpApprovalId;
|
|
109
|
+
let userApprovedFollowUp;
|
|
110
|
+
if (followUpApprovalText) {
|
|
111
|
+
if (verdict.allowed) {
|
|
112
|
+
throw workflowError("WORKFLOW_FOLLOW_UP_APPROVAL_NOT_REQUIRED", "This workflow transition is already legal and must not consume a post-validation follow-up approval.", "Set User-Approved Follow-Up Approval Text to none.");
|
|
113
|
+
}
|
|
114
|
+
if (!followUpAllowed) {
|
|
115
|
+
throw workflowError("WORKFLOW_FOLLOW_UP_APPROVAL_INVALID", "User-approved follow-up work is legal only for a Tester dispatch after a fresh passing Test Report and successful validation-adequacy and code-diff Gates.", "Use the normal allowed transition, or ask the user only when optional Tester-owned work is proposed after validation is fully green.");
|
|
116
|
+
}
|
|
117
|
+
userApprovedFollowUp = createUserApprovedFollowUp(id(), current, candidate, effectiveFlow, baseHistoryHash, followUpApprovalText, now());
|
|
118
|
+
followUpApprovalId = userApprovedFollowUp.id;
|
|
119
|
+
}
|
|
120
|
+
else if (!verdict.allowed) {
|
|
72
121
|
const authorizationText = candidate.proposal.authorizationText?.trim();
|
|
73
122
|
const violatedRule = candidate.proposal.violatedRule?.trim();
|
|
74
123
|
if (!authorizationText && !violatedRule) {
|
|
@@ -79,9 +128,6 @@ export function createWorkflowControlService(deps) {
|
|
|
79
128
|
if (!authorizationText || violatedRule !== verdict.reason) {
|
|
80
129
|
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
130
|
}
|
|
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.");
|
|
84
|
-
}
|
|
85
131
|
userAuthorization = createUserAuthorization(id(), current, candidate, effectiveFlow, baseHistoryHash, verdict.reason, authorizationText, now());
|
|
86
132
|
overrideAuthorizationId = userAuthorization.id;
|
|
87
133
|
}
|
|
@@ -98,6 +144,7 @@ export function createWorkflowControlService(deps) {
|
|
|
98
144
|
evidence: candidate.proposal.evidence,
|
|
99
145
|
expectedRoutePath: expectedRoutePath(input.handoffDir, candidate.proposal.targetRole),
|
|
100
146
|
overrideAuthorizationId,
|
|
147
|
+
followUpApprovalId,
|
|
101
148
|
status: "pending",
|
|
102
149
|
createdAt: timestamp,
|
|
103
150
|
updatedAt: timestamp
|
|
@@ -107,13 +154,15 @@ export function createWorkflowControlService(deps) {
|
|
|
107
154
|
proposal: candidate.proposal
|
|
108
155
|
};
|
|
109
156
|
const normalized = renderWorkflowProgress(accepted);
|
|
110
|
-
await
|
|
111
|
-
await saveState(input, {
|
|
157
|
+
await commitStateAndProgress(input, normalized, {
|
|
112
158
|
...state,
|
|
113
159
|
pendingDispatch,
|
|
114
160
|
userAuthorizations: userAuthorization
|
|
115
161
|
? [...state.userAuthorizations, userAuthorization]
|
|
116
162
|
: state.userAuthorizations,
|
|
163
|
+
userApprovedFollowUps: userApprovedFollowUp
|
|
164
|
+
? [...state.userApprovedFollowUps, userApprovedFollowUp]
|
|
165
|
+
: state.userApprovedFollowUps,
|
|
117
166
|
updatedAt: timestamp
|
|
118
167
|
});
|
|
119
168
|
return { path: relativeProgressPath(input), content: normalized };
|
|
@@ -172,6 +221,25 @@ export function createWorkflowControlService(deps) {
|
|
|
172
221
|
});
|
|
173
222
|
});
|
|
174
223
|
}
|
|
224
|
+
async function cancelPendingDispatch(input) {
|
|
225
|
+
await withLock(statePath(input), async () => {
|
|
226
|
+
const state = await getState(input);
|
|
227
|
+
const pending = state.pendingDispatch;
|
|
228
|
+
if (!pending)
|
|
229
|
+
return;
|
|
230
|
+
const progress = await readProgress(deps.fs, input);
|
|
231
|
+
await commitStateAndProgress(input, renderWorkflowProgress({
|
|
232
|
+
...progress,
|
|
233
|
+
proposal: undefined
|
|
234
|
+
}), {
|
|
235
|
+
...state,
|
|
236
|
+
pendingDispatch: null,
|
|
237
|
+
userAuthorizations: state.userAuthorizations.filter((entry) => entry.id !== pending.overrideAuthorizationId),
|
|
238
|
+
userApprovedFollowUps: state.userApprovedFollowUps.filter((entry) => entry.id !== pending.followUpApprovalId),
|
|
239
|
+
updatedAt: now()
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
}
|
|
175
243
|
async function confirmDispatch(input, messageId) {
|
|
176
244
|
await withLock(statePath(input), async () => {
|
|
177
245
|
const state = await getState(input);
|
|
@@ -192,10 +260,11 @@ export function createWorkflowControlService(deps) {
|
|
|
192
260
|
targetRole: pending.targetRole,
|
|
193
261
|
evidence: pending.evidence,
|
|
194
262
|
overrideAuthorizationId: pending.overrideAuthorizationId,
|
|
263
|
+
followUpApprovalId: pending.followUpApprovalId,
|
|
195
264
|
confirmedAt: timestamp
|
|
196
265
|
};
|
|
197
266
|
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,
|
|
267
|
+
const flowRun = await advanceFlowRun(deps.fs, input, state, current, pending.effectiveFlow, entry.sequence);
|
|
199
268
|
const completed = {
|
|
200
269
|
...current,
|
|
201
270
|
flow: pending.effectiveFlow,
|
|
@@ -206,36 +275,69 @@ export function createWorkflowControlService(deps) {
|
|
|
206
275
|
const userAuthorizations = state.userAuthorizations.map((entry) => entry.id === pending.overrideAuthorizationId
|
|
207
276
|
? { ...entry, status: "consumed", consumedAt: timestamp }
|
|
208
277
|
: entry);
|
|
209
|
-
|
|
210
|
-
|
|
278
|
+
const userApprovedFollowUps = state.userApprovedFollowUps.map((entry) => entry.id === pending.followUpApprovalId
|
|
279
|
+
? { ...entry, status: "consumed", consumedAt: timestamp }
|
|
280
|
+
: entry);
|
|
281
|
+
await commitStateAndProgress(input, renderWorkflowProgress(completed), {
|
|
211
282
|
...state,
|
|
212
283
|
pendingDispatch: null,
|
|
213
284
|
activeDispatch,
|
|
214
285
|
flowRun,
|
|
215
286
|
userAuthorizations,
|
|
287
|
+
userApprovedFollowUps,
|
|
216
288
|
updatedAt: timestamp
|
|
217
289
|
});
|
|
218
290
|
});
|
|
219
291
|
}
|
|
292
|
+
async function recoverTask(input) {
|
|
293
|
+
return withLock(statePath(input), async () => {
|
|
294
|
+
await recoverCommitJournal(input);
|
|
295
|
+
const state = await getState(input);
|
|
296
|
+
if (state.warnings.length > 0 || state.pendingDispatch?.status !== "dispatching")
|
|
297
|
+
return false;
|
|
298
|
+
await saveState(input, {
|
|
299
|
+
...state,
|
|
300
|
+
pendingDispatch: {
|
|
301
|
+
...state.pendingDispatch,
|
|
302
|
+
status: "pending",
|
|
303
|
+
routeContentHash: undefined,
|
|
304
|
+
messageId: undefined,
|
|
305
|
+
updatedAt: now()
|
|
306
|
+
},
|
|
307
|
+
updatedAt: now()
|
|
308
|
+
});
|
|
309
|
+
return true;
|
|
310
|
+
});
|
|
311
|
+
}
|
|
220
312
|
return {
|
|
221
313
|
getState,
|
|
222
|
-
getProgress: (input) =>
|
|
314
|
+
getProgress: async (input) => {
|
|
315
|
+
await getState(input);
|
|
316
|
+
return readProgress(deps.fs, input);
|
|
317
|
+
},
|
|
223
318
|
requestUserInput,
|
|
224
319
|
resolveUserInput,
|
|
225
320
|
submitProgress,
|
|
226
321
|
assertRouteAuthorized,
|
|
227
322
|
claimDispatch,
|
|
228
323
|
releaseDispatch,
|
|
229
|
-
|
|
324
|
+
cancelPendingDispatch,
|
|
325
|
+
confirmDispatch,
|
|
326
|
+
recoverTask
|
|
230
327
|
};
|
|
231
328
|
async function requestUserInput(input, question) {
|
|
232
329
|
return withLock(statePath(input), async () => {
|
|
233
330
|
const state = await getState(input);
|
|
331
|
+
failOnStateWarnings(state);
|
|
332
|
+
if (state.pendingDispatch?.status === "dispatching") {
|
|
333
|
+
throw workflowError("WORKFLOW_DISPATCH_PENDING", "A role dispatch is already being submitted and cannot be canceled by a user question.", "Wait for the target UserPromptSubmit confirmation or recover the dispatch before asking the user.");
|
|
334
|
+
}
|
|
234
335
|
const normalizedQuestion = question.trim();
|
|
235
336
|
if (!normalizedQuestion) {
|
|
236
337
|
throw workflowError("WORKFLOW_USER_QUESTION_REQUIRED", "A non-empty user question is required.");
|
|
237
338
|
}
|
|
238
339
|
const timestamp = now();
|
|
340
|
+
const pending = state.pendingDispatch;
|
|
239
341
|
const next = {
|
|
240
342
|
...state,
|
|
241
343
|
awaitingUser: {
|
|
@@ -243,15 +345,27 @@ export function createWorkflowControlService(deps) {
|
|
|
243
345
|
requestedAt: timestamp
|
|
244
346
|
},
|
|
245
347
|
pendingDispatch: null,
|
|
348
|
+
userAuthorizations: state.userAuthorizations.filter((entry) => entry.id !== pending?.overrideAuthorizationId),
|
|
349
|
+
userApprovedFollowUps: state.userApprovedFollowUps.filter((entry) => entry.id !== pending?.followUpApprovalId),
|
|
246
350
|
updatedAt: timestamp
|
|
247
351
|
};
|
|
248
|
-
|
|
352
|
+
if (pending) {
|
|
353
|
+
const progress = await readProgress(deps.fs, input);
|
|
354
|
+
await commitStateAndProgress(input, renderWorkflowProgress({
|
|
355
|
+
...progress,
|
|
356
|
+
proposal: undefined
|
|
357
|
+
}), next);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
await saveState(input, next);
|
|
361
|
+
}
|
|
249
362
|
return next;
|
|
250
363
|
});
|
|
251
364
|
}
|
|
252
365
|
async function resolveUserInput(input) {
|
|
253
366
|
return withLock(statePath(input), async () => {
|
|
254
367
|
const state = await getState(input);
|
|
368
|
+
failOnStateWarnings(state);
|
|
255
369
|
if (!state.awaitingUser)
|
|
256
370
|
return state;
|
|
257
371
|
const next = {
|
|
@@ -266,6 +380,58 @@ export function createWorkflowControlService(deps) {
|
|
|
266
380
|
async function saveState(input, state) {
|
|
267
381
|
await deps.fs.writeJsonAtomic(statePath(input), state);
|
|
268
382
|
}
|
|
383
|
+
async function commitStateAndProgress(input, progressContent, state) {
|
|
384
|
+
const journal = {
|
|
385
|
+
version: 1,
|
|
386
|
+
taskSlug: input.taskSlug,
|
|
387
|
+
status: "pending",
|
|
388
|
+
progressContent,
|
|
389
|
+
state,
|
|
390
|
+
createdAt: now()
|
|
391
|
+
};
|
|
392
|
+
await deps.fs.writeJsonAtomic(transactionPath(input), journal);
|
|
393
|
+
await writeAtomic(deps.fs, progressPath(input), progressContent);
|
|
394
|
+
await saveState(input, state);
|
|
395
|
+
await deps.fs.writeJsonAtomic(transactionPath(input), { ...journal, status: "committed" });
|
|
396
|
+
await removeTransactionJournal(transactionPath(input));
|
|
397
|
+
}
|
|
398
|
+
async function recoverCommitJournal(input) {
|
|
399
|
+
const target = transactionPath(input);
|
|
400
|
+
if (!(await deps.fs.pathExists(target)))
|
|
401
|
+
return;
|
|
402
|
+
const journal = await deps.fs.readJson(target);
|
|
403
|
+
if (journal.version !== 1 || journal.taskSlug !== input.taskSlug) {
|
|
404
|
+
throw new Error("Workflow commit journal does not match the active task.");
|
|
405
|
+
}
|
|
406
|
+
if (journal.status === "pending") {
|
|
407
|
+
parseWorkflowProgress(journal.progressContent, input.taskSlug);
|
|
408
|
+
const state = normalizeState(journal.state, input.taskSlug, now());
|
|
409
|
+
failOnStateWarnings(state);
|
|
410
|
+
await writeAtomic(deps.fs, progressPath(input), journal.progressContent);
|
|
411
|
+
await saveState(input, state);
|
|
412
|
+
await deps.fs.writeJsonAtomic(target, { ...journal, status: "committed" });
|
|
413
|
+
}
|
|
414
|
+
await removeTransactionJournal(target);
|
|
415
|
+
}
|
|
416
|
+
async function removeTransactionJournal(target) {
|
|
417
|
+
try {
|
|
418
|
+
await deps.fs.removePath?.(target, { force: true });
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
// A committed journal is safe to replay or remove on the next state read.
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
async function reconstructStateFromProgress(input, progress) {
|
|
425
|
+
const timestamp = now();
|
|
426
|
+
const last = progress.history.at(-1);
|
|
427
|
+
const activeDispatch = await captureEvidenceBaseline(deps.fs, input, last.sequence, last.flow, last.targetRole, last.confirmedAt ?? timestamp);
|
|
428
|
+
return {
|
|
429
|
+
...emptyState(input.taskSlug, timestamp),
|
|
430
|
+
activeDispatch,
|
|
431
|
+
flowRun: resolveFlowRun(null, progress),
|
|
432
|
+
updatedAt: timestamp
|
|
433
|
+
};
|
|
434
|
+
}
|
|
269
435
|
async function withLock(key, operation) {
|
|
270
436
|
const previous = locks.get(key) ?? Promise.resolve();
|
|
271
437
|
const next = previous.catch(() => undefined).then(operation);
|
|
@@ -337,6 +503,24 @@ export function parseWorkflowProgress(content, expectedTaskSlug) {
|
|
|
337
503
|
else if (authorizationText !== "none" || violatedRule !== "none") {
|
|
338
504
|
errors.push("User Authorization fields must be none when no role dispatch is proposed.");
|
|
339
505
|
}
|
|
506
|
+
const followUpSection = readArtifactSectionContent(content, "User-Approved Follow-Up");
|
|
507
|
+
const followUpApprovalText = followUpSection === undefined
|
|
508
|
+
? "none"
|
|
509
|
+
: rawField(followUpSection, "Approval Text");
|
|
510
|
+
if (!followUpApprovalText) {
|
|
511
|
+
errors.push("User-Approved Follow-Up requires an Approval Text field.");
|
|
512
|
+
}
|
|
513
|
+
else if (proposal) {
|
|
514
|
+
if (followUpApprovalText !== "none") {
|
|
515
|
+
if (proposal.authorizationText || proposal.violatedRule) {
|
|
516
|
+
errors.push("User-Approved Follow-Up and User Authorization cannot be used in the same proposal.");
|
|
517
|
+
}
|
|
518
|
+
proposal.followUpApprovalText = followUpApprovalText;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
else if (followUpApprovalText !== "none") {
|
|
522
|
+
errors.push("User-Approved Follow-Up Approval Text must be none when no role dispatch is proposed.");
|
|
523
|
+
}
|
|
340
524
|
if (errors.length > 0)
|
|
341
525
|
throw progressValidationError(errors);
|
|
342
526
|
return {
|
|
@@ -354,7 +538,7 @@ export function renderWorkflowProgress(progress) {
|
|
|
354
538
|
: [
|
|
355
539
|
HISTORY_HEADER,
|
|
356
540
|
HISTORY_SEPARATOR,
|
|
357
|
-
...progress.history.map((entry) => `| ${entry.sequence} | ${entry.flow} | ${entry.targetRole} | ${escapeCell(entry.evidence)} | ${entry.overrideAuthorizationId ?? "none"} | ${entry.confirmedAt ?? "none"} |`)
|
|
541
|
+
...progress.history.map((entry) => `| ${entry.sequence} | ${entry.flow} | ${entry.targetRole} | ${escapeCell(entry.evidence)} | ${entry.overrideAuthorizationId ?? "none"} | ${entry.followUpApprovalId ?? "none"} | ${entry.confirmedAt ?? "none"} |`)
|
|
358
542
|
].join("\n");
|
|
359
543
|
const proposal = progress.proposal;
|
|
360
544
|
return `# Workflow Progress: ${progress.taskSlug}
|
|
@@ -377,6 +561,10 @@ Evidence: ${proposal?.evidence ?? "none"}
|
|
|
377
561
|
|
|
378
562
|
Authorization Text: ${proposal?.authorizationText ?? "none"}
|
|
379
563
|
Violated Rule: ${proposal?.violatedRule ?? "none"}
|
|
564
|
+
|
|
565
|
+
## User-Approved Follow-Up
|
|
566
|
+
|
|
567
|
+
Approval Text: ${proposal?.followUpApprovalText ?? "none"}
|
|
380
568
|
`;
|
|
381
569
|
}
|
|
382
570
|
async function evaluateTransition(fs, input, state, current, effectiveFlow, targetRole) {
|
|
@@ -392,6 +580,32 @@ async function evaluateTransition(fs, input, state, current, effectiveFlow, targ
|
|
|
392
580
|
blockedHint: await describeBlockedCheckpoint(fs, input, state, current)
|
|
393
581
|
};
|
|
394
582
|
}
|
|
583
|
+
async function canRouteUserApprovedFollowUp(fs, input, state, current, effectiveFlow, targetRole, allowedTransitions) {
|
|
584
|
+
if (targetRole !== "tester"
|
|
585
|
+
|| current.status !== "active"
|
|
586
|
+
|| current.flow !== effectiveFlow
|
|
587
|
+
|| (effectiveFlow !== "code-change"
|
|
588
|
+
&& effectiveFlow !== "architect-debug"
|
|
589
|
+
&& effectiveFlow !== "architecture-diagnosis"))
|
|
590
|
+
return false;
|
|
591
|
+
const active = state.activeDispatch;
|
|
592
|
+
if (!active || active.flow !== effectiveFlow || active.targetRole !== "tester")
|
|
593
|
+
return false;
|
|
594
|
+
const test = await artifactState(fs, input, "test-report.md", "test-report");
|
|
595
|
+
if (test.value !== "pass" || !evidenceIsFresh(state, effectiveFlow, "tester", "test-report.md", test.hash)) {
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
const validation = await gateState(fs, input, "validation-adequacy");
|
|
599
|
+
const codeDiff = await gateState(fs, input, "code-diff");
|
|
600
|
+
if (!gatePassedForDispatch(state, effectiveFlow, "tester", "validation-adequacy", validation)
|
|
601
|
+
|| !gatePassedForDispatch(state, effectiveFlow, "tester", "code-diff", codeDiff))
|
|
602
|
+
return false;
|
|
603
|
+
const flowRun = resolveFlowRun(state.flowRun, current);
|
|
604
|
+
const expectedExit = flowRun.activeBranch === effectiveFlow
|
|
605
|
+
? `${flowRun.rootFlow}/architect`
|
|
606
|
+
: `${effectiveFlow}/architect`;
|
|
607
|
+
return allowedTransitions.length === 1 && allowedTransitions[0] === expectedExit;
|
|
608
|
+
}
|
|
395
609
|
async function getAllowedTransitions(fs, input, state, current) {
|
|
396
610
|
if (current.status === "completed")
|
|
397
611
|
return initialTransitions();
|
|
@@ -427,9 +641,12 @@ async function getAllowedTransitions(fs, input, state, current) {
|
|
|
427
641
|
return [];
|
|
428
642
|
return [];
|
|
429
643
|
}
|
|
644
|
+
const coderDocsCorrection = await allowedActiveCoderDocsCorrection(fs, input, state, flow);
|
|
645
|
+
if (coderDocsCorrection)
|
|
646
|
+
return coderDocsCorrection;
|
|
430
647
|
const segment = activeFlowSegment(current.history, flow, flowRun.startedAtSequence);
|
|
431
648
|
if (flow === "code-change") {
|
|
432
|
-
return allowedCodeChange(fs, input, state, segment, flowRun.resumedFromBranch
|
|
649
|
+
return allowedCodeChange(fs, input, state, segment, flowRun.resumedFromBranch);
|
|
433
650
|
}
|
|
434
651
|
if (flow === "architect-debug") {
|
|
435
652
|
return allowedArchitectFix(fs, input, state, segment, "architect-debug", flowRun.resumedFromBranch !== undefined, flowRun.activeBranch ? flowRun.rootFlow : undefined);
|
|
@@ -448,8 +665,15 @@ function initialTransitions() {
|
|
|
448
665
|
async function allowedCodeChange(fs, input, state, segment, resumedFromBranch) {
|
|
449
666
|
const coderIndex = findLastIndex(segment, (entry) => entry.targetRole === "coder");
|
|
450
667
|
const testerIndex = findLastIndex(segment, (entry) => entry.targetRole === "tester");
|
|
451
|
-
if (resumedFromBranch && coderIndex < 0
|
|
452
|
-
|
|
668
|
+
if (resumedFromBranch && coderIndex < 0) {
|
|
669
|
+
if (testerIndex < 0) {
|
|
670
|
+
return allowedPostImplementationArchitect(fs, input, state, "code-change", segment);
|
|
671
|
+
}
|
|
672
|
+
return allowedAfterTester(fs, input, state, "code-change", resumedFromBranch, {
|
|
673
|
+
testerFailureFlow: "architect-debug",
|
|
674
|
+
implementationFailureFlow: "architect-debug",
|
|
675
|
+
successTarget: "code-change/architect"
|
|
676
|
+
});
|
|
453
677
|
}
|
|
454
678
|
if (coderIndex < 0) {
|
|
455
679
|
const plan = await artifactState(fs, input, "architecture-plan.md", "architecture-plan");
|
|
@@ -477,7 +701,7 @@ async function allowedCodeChange(fs, input, state, segment, resumedFromBranch) {
|
|
|
477
701
|
if (architectsAfterTester.length > 0) {
|
|
478
702
|
return allowedPostImplementationArchitect(fs, input, state, "code-change", architectsAfterTester);
|
|
479
703
|
}
|
|
480
|
-
return allowedAfterTester(fs, input, state, "coder", {
|
|
704
|
+
return allowedAfterTester(fs, input, state, "code-change", "coder", {
|
|
481
705
|
testerFailureFlow: "architect-debug",
|
|
482
706
|
implementationFailureFlow: "architect-debug",
|
|
483
707
|
successTarget: "code-change/architect"
|
|
@@ -514,9 +738,15 @@ async function allowedArchitectFix(fs, input, state, segment, source, resumedFro
|
|
|
514
738
|
if (!evidenceIsFresh(state, source, "architect", artifactName, artifact.hash)) {
|
|
515
739
|
return [`${source}/architect`];
|
|
516
740
|
}
|
|
517
|
-
if (source === "architect-debug" && artifact.disposition ===
|
|
741
|
+
if (source === "architect-debug" && artifact.disposition === ARCHITECT_DEBUG_NORMAL_PLAN_DISPOSITION) {
|
|
518
742
|
return ["code-change/architect"];
|
|
519
743
|
}
|
|
744
|
+
if (source === "architecture-diagnosis" && artifact.disposition === "analysis completed") {
|
|
745
|
+
return parentFlow ? [`${parentFlow}/architect`] : [];
|
|
746
|
+
}
|
|
747
|
+
if (artifact.disposition === "user clarification required") {
|
|
748
|
+
return [`${source}/architect`];
|
|
749
|
+
}
|
|
520
750
|
return artifact.complete ? [`${source}/tester`] : [`${source}/architect`];
|
|
521
751
|
}
|
|
522
752
|
if (architectIndex > testerIndex) {
|
|
@@ -524,6 +754,10 @@ async function allowedArchitectFix(fs, input, state, segment, source, resumedFro
|
|
|
524
754
|
const artifact = source === "architect-debug"
|
|
525
755
|
? await artifactState(fs, input, artifactName, "architect-debug")
|
|
526
756
|
: await artifactState(fs, input, artifactName, "architecture-diagnosis");
|
|
757
|
+
if (evidenceIsFresh(state, source, "architect", artifactName, artifact.hash)
|
|
758
|
+
&& artifact.disposition === "user clarification required") {
|
|
759
|
+
return [`${source}/architect`];
|
|
760
|
+
}
|
|
527
761
|
if (evidenceIsFresh(state, source, "architect", artifactName, artifact.hash) && artifact.complete) {
|
|
528
762
|
return [`${source}/tester`];
|
|
529
763
|
}
|
|
@@ -533,7 +767,7 @@ async function allowedArchitectFix(fs, input, state, segment, source, resumedFro
|
|
|
533
767
|
? []
|
|
534
768
|
: [`${source}/architect`];
|
|
535
769
|
}
|
|
536
|
-
return allowedAfterTester(fs, input, state, source, {
|
|
770
|
+
return allowedAfterTester(fs, input, state, source, source, {
|
|
537
771
|
testerFailureFlow: source === "architect-debug" ? "architecture-diagnosis" : undefined,
|
|
538
772
|
implementationFailureFlow: source,
|
|
539
773
|
successTarget: parentFlow ? `${parentFlow}/architect` : `${source}/architect`
|
|
@@ -545,21 +779,46 @@ async function allowedPostImplementationArchitect(fs, input, state, flow, archit
|
|
|
545
779
|
if (flow === "code-change" && architectEntries.length > 1 && acceptance.value === "needs-architect-follow-up") {
|
|
546
780
|
return allowedArchitectureFollowup(fs, input, state, architectEntries[1]?.confirmedAt);
|
|
547
781
|
}
|
|
548
|
-
if (evidenceIsFresh(state, flow, "architect", "docs-sync-report.md", docs.hash)
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
782
|
+
if (evidenceIsFresh(state, flow, "architect", "docs-sync-report.md", docs.hash) && docs.complete) {
|
|
783
|
+
if (docs.value === "synced" || docs.value === "unchanged") {
|
|
784
|
+
if (acceptance.value === "needs-coder-follow-up" && flow === "code-change")
|
|
785
|
+
return ["code-change/coder"];
|
|
786
|
+
if (acceptance.value === "needs-docs-sync")
|
|
787
|
+
return [`${flow}/architect`];
|
|
788
|
+
if (acceptance.value === "needs-architect-follow-up")
|
|
789
|
+
return [`${flow}/architect`];
|
|
790
|
+
return [];
|
|
791
|
+
}
|
|
792
|
+
if (docs.value === "blocked" && docs.correctionOwner && docs.correctionOwner !== "none") {
|
|
793
|
+
return [`${flow}/${docs.correctionOwner}`];
|
|
794
|
+
}
|
|
557
795
|
}
|
|
558
796
|
return [`${flow}/architect`];
|
|
559
797
|
}
|
|
560
|
-
async function
|
|
798
|
+
async function allowedActiveCoderDocsCorrection(fs, input, state, flow) {
|
|
799
|
+
const active = state.activeDispatch;
|
|
800
|
+
if (!active || active.flow !== flow || active.targetRole !== "coder")
|
|
801
|
+
return undefined;
|
|
802
|
+
const docs = await artifactState(fs, input, "docs-sync-report.md", "docs-sync-report");
|
|
803
|
+
if (!docs.complete
|
|
804
|
+
|| docs.value !== "blocked"
|
|
805
|
+
|| docs.correctionOwner !== "coder"
|
|
806
|
+
|| active.artifactHashes["docs-sync-report.md"] !== docs.hash)
|
|
807
|
+
return undefined;
|
|
808
|
+
const coder = await artifactState(fs, input, "coder-completion.md", "coder-completion");
|
|
809
|
+
if (!evidenceIsFresh(state, flow, "coder", "coder-completion.md", coder.hash)
|
|
810
|
+
|| coder.value === "incomplete")
|
|
811
|
+
return [`${flow}/coder`];
|
|
812
|
+
if (coder.value === "ready_for_review")
|
|
813
|
+
return [`${flow}/tester`];
|
|
814
|
+
if (flow === "code-change")
|
|
815
|
+
return ["architect-debug/architect"];
|
|
816
|
+
if (flow === "architect-debug")
|
|
817
|
+
return ["architecture-diagnosis/architect"];
|
|
818
|
+
return [];
|
|
819
|
+
}
|
|
820
|
+
async function allowedAfterTester(fs, input, state, currentFlow, codeSource, options) {
|
|
561
821
|
const test = await artifactState(fs, input, "test-report.md", "test-report");
|
|
562
|
-
const currentFlow = codeSource === "coder" ? "code-change" : codeSource;
|
|
563
822
|
if (!evidenceIsFresh(state, currentFlow, "tester", "test-report.md", test.hash)) {
|
|
564
823
|
return [`${currentFlow}/tester`];
|
|
565
824
|
}
|
|
@@ -617,12 +876,18 @@ async function artifactState(fs, input, fileName, kind) {
|
|
|
617
876
|
const disposition = kind === "architect-debug" || kind === "architecture-diagnosis"
|
|
618
877
|
? readArtifactSectionContent(content, "Final Disposition")?.trim().toLowerCase()
|
|
619
878
|
: undefined;
|
|
879
|
+
const correctionOwner = kind === "docs-sync-report"
|
|
880
|
+
? readArtifactSectionContent(content, "Correction Owner")?.trim().toLowerCase()
|
|
881
|
+
: undefined;
|
|
620
882
|
return {
|
|
621
883
|
complete: check.status === "ok",
|
|
622
|
-
hash:
|
|
884
|
+
hash: await artifactFingerprint(fs, absolute, content),
|
|
623
885
|
value,
|
|
624
886
|
infrastructure,
|
|
625
|
-
disposition
|
|
887
|
+
disposition,
|
|
888
|
+
correctionOwner: correctionOwner === "none" || asTargetRole(correctionOwner)
|
|
889
|
+
? correctionOwner
|
|
890
|
+
: undefined
|
|
626
891
|
};
|
|
627
892
|
}
|
|
628
893
|
async function gateState(fs, input, gate) {
|
|
@@ -640,7 +905,9 @@ async function gateState(fs, input, gate) {
|
|
|
640
905
|
async function captureEvidenceBaseline(fs, input, sequence, flow, targetRole, confirmedAt) {
|
|
641
906
|
const artifactEntries = await Promise.all(WORKFLOW_EVIDENCE_ARTIFACTS.map(async (fileName) => {
|
|
642
907
|
const target = resolveRepoPath(input.taskRepoRoot, path.posix.join(input.handoffDir, fileName));
|
|
643
|
-
const hash = await fs.pathExists(target)
|
|
908
|
+
const hash = await fs.pathExists(target)
|
|
909
|
+
? await artifactFingerprint(fs, target, await fs.readText(target))
|
|
910
|
+
: MISSING_EVIDENCE_HASH;
|
|
644
911
|
return [fileName, hash];
|
|
645
912
|
}));
|
|
646
913
|
const gateEntries = await Promise.all(WORKFLOW_EVIDENCE_GATES.map(async (gate) => [
|
|
@@ -656,8 +923,8 @@ async function captureEvidenceBaseline(fs, input, sequence, flow, targetRole, co
|
|
|
656
923
|
confirmedAt
|
|
657
924
|
};
|
|
658
925
|
}
|
|
659
|
-
async function advanceFlowRun(fs, input, state, current, effectiveFlow,
|
|
660
|
-
if (!current.flow || current.status === "completed"
|
|
926
|
+
async function advanceFlowRun(fs, input, state, current, effectiveFlow, nextSequence) {
|
|
927
|
+
if (!current.flow || current.status === "completed") {
|
|
661
928
|
return newFlowRun(effectiveFlow, nextSequence);
|
|
662
929
|
}
|
|
663
930
|
const run = resolveFlowRun(state.flowRun, current);
|
|
@@ -677,7 +944,7 @@ async function advanceFlowRun(fs, input, state, current, effectiveFlow, usedOver
|
|
|
677
944
|
if (run.activeBranch === current.flow && effectiveFlow === run.rootFlow) {
|
|
678
945
|
if (current.flow === "architect-debug" && effectiveFlow === "code-change") {
|
|
679
946
|
const debug = await artifactState(fs, input, "architect-debug.md", "architect-debug");
|
|
680
|
-
if (debug.disposition ===
|
|
947
|
+
if (debug.disposition === ARCHITECT_DEBUG_NORMAL_PLAN_DISPOSITION) {
|
|
681
948
|
return newFlowRun("code-change", nextSequence);
|
|
682
949
|
}
|
|
683
950
|
}
|
|
@@ -766,11 +1033,18 @@ function freshGateDecision(state, flow, targetRole, gate, record) {
|
|
|
766
1033
|
function gatePassedForDispatch(state, flow, targetRole, gate, record) {
|
|
767
1034
|
if (!record)
|
|
768
1035
|
return false;
|
|
769
|
-
if (
|
|
1036
|
+
if (record.status === "disabled")
|
|
770
1037
|
return true;
|
|
1038
|
+
if (FRESH_EXCEPTION_GATE_STATUSES.has(record.status)) {
|
|
1039
|
+
return gateRecordIsFresh(state, flow, targetRole, gate, record);
|
|
1040
|
+
}
|
|
771
1041
|
return freshGateDecision(state, flow, targetRole, gate, record) === "approve"
|
|
772
1042
|
&& record.status === "completed";
|
|
773
1043
|
}
|
|
1044
|
+
function gateRecordIsFresh(state, flow, targetRole, gate, record) {
|
|
1045
|
+
const baseline = matchingEvidenceBaseline(state, flow, targetRole);
|
|
1046
|
+
return !baseline || baseline.gateFingerprints[gate] !== gateFingerprint(record);
|
|
1047
|
+
}
|
|
774
1048
|
function matchingEvidenceBaseline(state, flow, targetRole) {
|
|
775
1049
|
const baseline = state.activeDispatch;
|
|
776
1050
|
return baseline?.flow === flow && baseline.targetRole === targetRole ? baseline : undefined;
|
|
@@ -786,7 +1060,8 @@ function gateFingerprint(record) {
|
|
|
786
1060
|
codeDiffSource: record.codeDiffSource,
|
|
787
1061
|
codeDiffSources: record.codeDiffSources,
|
|
788
1062
|
findings: record.findings,
|
|
789
|
-
completedAt: record.completedAt
|
|
1063
|
+
completedAt: record.completedAt,
|
|
1064
|
+
updatedAt: record.updatedAt
|
|
790
1065
|
}));
|
|
791
1066
|
}
|
|
792
1067
|
async function describeBlockedCheckpoint(fs, input, state, current) {
|
|
@@ -821,11 +1096,7 @@ async function validateCompletion(fs, input, state, candidate) {
|
|
|
821
1096
|
throw workflowError("WORKFLOW_COMPLETION_INVALID", `${candidate.flow} is an active branch and must return to ${flowRun.rootFlow} before completion.`);
|
|
822
1097
|
}
|
|
823
1098
|
if (candidate.flow === "code-change" || candidate.flow === "architect-debug") {
|
|
824
|
-
|
|
825
|
-
if (!evidenceProducedAfterActiveDispatch(state, candidate.flow, "architect", "final-acceptance.md", acceptance.hash)
|
|
826
|
-
|| (acceptance.value !== "accepted" && acceptance.value !== "accepted-with-known-risks")) {
|
|
827
|
-
throw workflowError("WORKFLOW_COMPLETION_INVALID", "Final Acceptance is not accepted for this complete delivery flow.");
|
|
828
|
-
}
|
|
1099
|
+
await validateCompleteDeliveryEvidence(fs, input, state, candidate.flow);
|
|
829
1100
|
return;
|
|
830
1101
|
}
|
|
831
1102
|
if (candidate.flow === "architecture-diagnosis") {
|
|
@@ -833,11 +1104,7 @@ async function validateCompletion(fs, input, state, candidate) {
|
|
|
833
1104
|
if (diagnosis.disposition === "analysis completed"
|
|
834
1105
|
&& evidenceProducedAfterActiveDispatch(state, candidate.flow, "architect", "architecture-diagnosis.md", diagnosis.hash))
|
|
835
1106
|
return;
|
|
836
|
-
|
|
837
|
-
if (!evidenceProducedAfterActiveDispatch(state, candidate.flow, "architect", "final-acceptance.md", acceptance.hash)
|
|
838
|
-
|| (acceptance.value !== "accepted" && acceptance.value !== "accepted-with-known-risks")) {
|
|
839
|
-
throw workflowError("WORKFLOW_COMPLETION_INVALID", "Implemented Architecture Diagnosis requires accepted Final Acceptance evidence.");
|
|
840
|
-
}
|
|
1107
|
+
await validateCompleteDeliveryEvidence(fs, input, state, candidate.flow);
|
|
841
1108
|
return;
|
|
842
1109
|
}
|
|
843
1110
|
if (candidate.flow === "docs-only") {
|
|
@@ -862,6 +1129,35 @@ async function validateCompletion(fs, input, state, candidate) {
|
|
|
862
1129
|
}
|
|
863
1130
|
}
|
|
864
1131
|
}
|
|
1132
|
+
async function validateCompleteDeliveryEvidence(fs, input, state, flow) {
|
|
1133
|
+
const baseline = state.activeDispatch;
|
|
1134
|
+
if (!baseline || baseline.flow !== flow) {
|
|
1135
|
+
throw workflowError("WORKFLOW_COMPLETION_INVALID", `The latest confirmed dispatch does not belong to the active ${flow} flow.`);
|
|
1136
|
+
}
|
|
1137
|
+
if (baseline.targetRole !== "architect") {
|
|
1138
|
+
throw workflowError("WORKFLOW_COMPLETION_INVALID", `The latest ${flow} dispatch is to ${baseline.targetRole}. Complete every required downstream validation and Gate step, then route Architect for docs sync before completing the flow.`);
|
|
1139
|
+
}
|
|
1140
|
+
const docs = await artifactState(fs, input, "docs-sync-report.md", "docs-sync-report");
|
|
1141
|
+
if (!docs.complete) {
|
|
1142
|
+
throw workflowError("WORKFLOW_COMPLETION_INVALID", "Docs Sync Report is missing or malformed for this complete delivery flow.");
|
|
1143
|
+
}
|
|
1144
|
+
if (baseline.artifactHashes["docs-sync-report.md"] === docs.hash) {
|
|
1145
|
+
throw workflowError("WORKFLOW_COMPLETION_INVALID", "Docs Sync Report was not produced after the final Architect dispatch.");
|
|
1146
|
+
}
|
|
1147
|
+
if (docs.value !== "synced" && docs.value !== "unchanged") {
|
|
1148
|
+
throw workflowError("WORKFLOW_COMPLETION_INVALID", `Docs Sync Report Decision must be synced or unchanged before completion; found ${docs.value ?? "missing"}.`);
|
|
1149
|
+
}
|
|
1150
|
+
const acceptance = await artifactState(fs, input, "final-acceptance.md", "final-acceptance");
|
|
1151
|
+
if (!acceptance.complete) {
|
|
1152
|
+
throw workflowError("WORKFLOW_COMPLETION_INVALID", "Final Acceptance is missing or malformed for this complete delivery flow.");
|
|
1153
|
+
}
|
|
1154
|
+
if (acceptance.value !== "accepted" && acceptance.value !== "accepted-with-known-risks") {
|
|
1155
|
+
throw workflowError("WORKFLOW_COMPLETION_INVALID", `Final Acceptance Decision must be accepted or accepted-with-known-risks; found ${acceptance.value ?? "missing"}.`);
|
|
1156
|
+
}
|
|
1157
|
+
if (baseline.artifactHashes["final-acceptance.md"] === acceptance.hash) {
|
|
1158
|
+
throw workflowError("WORKFLOW_COMPLETION_INVALID", "Final Acceptance was not produced after the final Architect dispatch.");
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
865
1161
|
function evidenceProducedAfterActiveDispatch(state, flow, targetRole, artifact, currentHash) {
|
|
866
1162
|
const baseline = matchingEvidenceBaseline(state, flow, targetRole);
|
|
867
1163
|
return Boolean(baseline && baseline.artifactHashes[artifact] !== currentHash);
|
|
@@ -879,6 +1175,31 @@ function validateCandidateAgainstCurrent(current, candidate) {
|
|
|
879
1175
|
if (errors.length > 0)
|
|
880
1176
|
throw progressValidationError(errors);
|
|
881
1177
|
}
|
|
1178
|
+
function stateProgressConsistencyWarnings(state, progress) {
|
|
1179
|
+
const warnings = [];
|
|
1180
|
+
const pending = state.pendingDispatch;
|
|
1181
|
+
if (!pending && progress.proposal) {
|
|
1182
|
+
warnings.push("Workflow Progress contains a dispatch proposal without a runtime approval.");
|
|
1183
|
+
}
|
|
1184
|
+
if (pending && (pending.revision !== progress.revision
|
|
1185
|
+
|| pending.baseHistoryHash !== historyHash(progress.history)
|
|
1186
|
+
|| progress.proposal?.requestedFlow !== pending.requestedFlow
|
|
1187
|
+
|| progress.proposal?.targetRole !== pending.targetRole
|
|
1188
|
+
|| progress.proposal?.evidence !== pending.evidence)) {
|
|
1189
|
+
warnings.push("Workflow runtime approval does not match workflow-progress.md.");
|
|
1190
|
+
}
|
|
1191
|
+
const last = progress.history.at(-1);
|
|
1192
|
+
if (last && (!state.activeDispatch
|
|
1193
|
+
|| state.activeDispatch.sequence !== last.sequence
|
|
1194
|
+
|| state.activeDispatch.flow !== last.flow
|
|
1195
|
+
|| state.activeDispatch.targetRole !== last.targetRole)) {
|
|
1196
|
+
warnings.push("Workflow active dispatch does not match the latest Workflow Progress history entry.");
|
|
1197
|
+
}
|
|
1198
|
+
if (!last && state.activeDispatch) {
|
|
1199
|
+
warnings.push("Workflow active dispatch exists without a Workflow Progress history entry.");
|
|
1200
|
+
}
|
|
1201
|
+
return warnings;
|
|
1202
|
+
}
|
|
882
1203
|
function createUserAuthorization(authorizationId, current, candidate, effectiveFlow, baseHistoryHash, violation, authorizationText, timestamp) {
|
|
883
1204
|
return {
|
|
884
1205
|
id: authorizationId,
|
|
@@ -896,31 +1217,33 @@ function createUserAuthorization(authorizationId, current, candidate, effectiveF
|
|
|
896
1217
|
createdAt: timestamp
|
|
897
1218
|
};
|
|
898
1219
|
}
|
|
1220
|
+
function createUserApprovedFollowUp(approvalId, current, candidate, effectiveFlow, baseHistoryHash, approvalText, timestamp) {
|
|
1221
|
+
return {
|
|
1222
|
+
id: approvalId,
|
|
1223
|
+
status: "accepted",
|
|
1224
|
+
role: "project-manager",
|
|
1225
|
+
operation: "post-validation-follow-up",
|
|
1226
|
+
baseRevision: current.revision,
|
|
1227
|
+
baseHistoryHash,
|
|
1228
|
+
effectiveFlow: effectiveFlow,
|
|
1229
|
+
targetRole: "tester",
|
|
1230
|
+
evidence: candidate.proposal.evidence,
|
|
1231
|
+
approvalText,
|
|
1232
|
+
createdAt: timestamp
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
899
1235
|
async function readProgress(fs, input) {
|
|
900
1236
|
const target = progressPath(input);
|
|
901
1237
|
if (!(await fs.pathExists(target)))
|
|
902
1238
|
return parseWorkflowProgress(renderWorkflowProgressTemplate(input.taskSlug), input.taskSlug);
|
|
903
1239
|
return parseWorkflowProgress(await fs.readText(target), input.taskSlug);
|
|
904
1240
|
}
|
|
905
|
-
async function ensureProgressFile(fs, input) {
|
|
906
|
-
const target = progressPath(input);
|
|
907
|
-
if (!(await fs.pathExists(target))) {
|
|
908
|
-
await writeAtomic(fs, target, renderWorkflowProgressTemplate(input.taskSlug));
|
|
909
|
-
return undefined;
|
|
910
|
-
}
|
|
911
|
-
try {
|
|
912
|
-
parseWorkflowProgress(await fs.readText(target), input.taskSlug);
|
|
913
|
-
return undefined;
|
|
914
|
-
}
|
|
915
|
-
catch (error) {
|
|
916
|
-
return `Workflow Progress is invalid and was preserved unchanged: ${errorMessage(error)}`;
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
1241
|
function parseHistory(value, errors) {
|
|
920
1242
|
if (!value || value.trim() === "none")
|
|
921
1243
|
return [];
|
|
922
1244
|
const lines = value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
923
|
-
|
|
1245
|
+
const legacy = lines[0] === LEGACY_HISTORY_HEADER && lines[1] === LEGACY_HISTORY_SEPARATOR;
|
|
1246
|
+
if (!legacy && (lines[0] !== HISTORY_HEADER || lines[1] !== HISTORY_SEPARATOR)) {
|
|
924
1247
|
errors.push("Dispatch History must use the exact VCM table header.");
|
|
925
1248
|
return [];
|
|
926
1249
|
}
|
|
@@ -929,8 +1252,9 @@ function parseHistory(value, errors) {
|
|
|
929
1252
|
const cells = line.startsWith("|") && line.endsWith("|")
|
|
930
1253
|
? line.slice(1, -1).split("|").map((cell) => cell.trim())
|
|
931
1254
|
: [];
|
|
932
|
-
|
|
933
|
-
|
|
1255
|
+
const expectedColumns = legacy ? 6 : 7;
|
|
1256
|
+
if (cells.length !== expectedColumns) {
|
|
1257
|
+
errors.push(`Every Dispatch History row must contain exactly ${expectedColumns} columns.`);
|
|
934
1258
|
continue;
|
|
935
1259
|
}
|
|
936
1260
|
const sequence = Number(cells[0]);
|
|
@@ -946,7 +1270,8 @@ function parseHistory(value, errors) {
|
|
|
946
1270
|
targetRole,
|
|
947
1271
|
evidence: cells[3],
|
|
948
1272
|
overrideAuthorizationId: cells[4] === "none" ? undefined : cells[4],
|
|
949
|
-
|
|
1273
|
+
followUpApprovalId: legacy || cells[5] === "none" ? undefined : cells[5],
|
|
1274
|
+
confirmedAt: cells[legacy ? 5 : 6] === "none" ? undefined : cells[legacy ? 5 : 6]
|
|
950
1275
|
});
|
|
951
1276
|
}
|
|
952
1277
|
return entries;
|
|
@@ -964,6 +1289,11 @@ function normalizeState(value, taskSlug, timestamp) {
|
|
|
964
1289
|
: value.userAuthorizations === undefined && Array.isArray(value.overrideRequests)
|
|
965
1290
|
? []
|
|
966
1291
|
: undefined;
|
|
1292
|
+
const userApprovedFollowUps = value.userApprovedFollowUps === undefined
|
|
1293
|
+
? []
|
|
1294
|
+
: Array.isArray(value.userApprovedFollowUps) && value.userApprovedFollowUps.every(isUserApprovedFollowUp)
|
|
1295
|
+
? value.userApprovedFollowUps
|
|
1296
|
+
: undefined;
|
|
967
1297
|
const activeDispatch = value.activeDispatch === undefined || value.activeDispatch === null
|
|
968
1298
|
? null
|
|
969
1299
|
: normalizeDispatchEvidenceBaseline(value.activeDispatch);
|
|
@@ -977,6 +1307,7 @@ function normalizeState(value, taskSlug, timestamp) {
|
|
|
977
1307
|
|| activeDispatch === undefined
|
|
978
1308
|
|| flowRun === undefined
|
|
979
1309
|
|| userAuthorizations === undefined
|
|
1310
|
+
|| userApprovedFollowUps === undefined
|
|
980
1311
|
|| awaitingUser === undefined) {
|
|
981
1312
|
return { ...emptyState(taskSlug, timestamp), warnings: ["Workflow control state has an unsupported shape."] };
|
|
982
1313
|
}
|
|
@@ -988,6 +1319,7 @@ function normalizeState(value, taskSlug, timestamp) {
|
|
|
988
1319
|
activeDispatch,
|
|
989
1320
|
flowRun,
|
|
990
1321
|
userAuthorizations,
|
|
1322
|
+
userApprovedFollowUps,
|
|
991
1323
|
warnings: [],
|
|
992
1324
|
updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : timestamp
|
|
993
1325
|
};
|
|
@@ -1001,6 +1333,7 @@ function emptyState(taskSlug, timestamp) {
|
|
|
1001
1333
|
activeDispatch: null,
|
|
1002
1334
|
flowRun: null,
|
|
1003
1335
|
userAuthorizations: [],
|
|
1336
|
+
userApprovedFollowUps: [],
|
|
1004
1337
|
warnings: [],
|
|
1005
1338
|
updatedAt: timestamp
|
|
1006
1339
|
};
|
|
@@ -1018,6 +1351,9 @@ function expectedRoutePath(handoffDir, role) {
|
|
|
1018
1351
|
function statePath(input) {
|
|
1019
1352
|
return path.join(input.taskRepoRoot, input.stateRoot, "workflow-control.json");
|
|
1020
1353
|
}
|
|
1354
|
+
function transactionPath(input) {
|
|
1355
|
+
return path.join(input.taskRepoRoot, input.stateRoot, "workflow-control-transaction.json");
|
|
1356
|
+
}
|
|
1021
1357
|
function relativeProgressPath(input) {
|
|
1022
1358
|
return path.posix.join(input.handoffDir, "workflow-progress.md");
|
|
1023
1359
|
}
|
|
@@ -1030,6 +1366,12 @@ function historyHash(history) {
|
|
|
1030
1366
|
function contentHash(content) {
|
|
1031
1367
|
return createHash("sha256").update(content).digest("hex");
|
|
1032
1368
|
}
|
|
1369
|
+
async function artifactFingerprint(fs, target, content) {
|
|
1370
|
+
const contentDigest = contentHash(content);
|
|
1371
|
+
if (!fs.fileVersion)
|
|
1372
|
+
return contentDigest;
|
|
1373
|
+
return contentHash(`${contentDigest}:${await fs.fileVersion(target)}`);
|
|
1374
|
+
}
|
|
1033
1375
|
function field(content, name) {
|
|
1034
1376
|
return rawField(content, name)?.toLowerCase();
|
|
1035
1377
|
}
|
|
@@ -1098,6 +1440,7 @@ function isPendingDispatch(value) {
|
|
|
1098
1440
|
&& typeof value.evidence === "string"
|
|
1099
1441
|
&& typeof value.expectedRoutePath === "string"
|
|
1100
1442
|
&& (value.overrideAuthorizationId === undefined || typeof value.overrideAuthorizationId === "string")
|
|
1443
|
+
&& (value.followUpApprovalId === undefined || typeof value.followUpApprovalId === "string")
|
|
1101
1444
|
&& (value.status === "pending" || value.status === "dispatching")
|
|
1102
1445
|
&& (value.routeContentHash === undefined || typeof value.routeContentHash === "string")
|
|
1103
1446
|
&& (value.messageId === undefined || typeof value.messageId === "string")
|
|
@@ -1122,6 +1465,24 @@ function isUserAuthorization(value) {
|
|
|
1122
1465
|
&& typeof value.createdAt === "string"
|
|
1123
1466
|
&& (value.consumedAt === undefined || typeof value.consumedAt === "string");
|
|
1124
1467
|
}
|
|
1468
|
+
function isUserApprovedFollowUp(value) {
|
|
1469
|
+
if (!isRecord(value))
|
|
1470
|
+
return false;
|
|
1471
|
+
return typeof value.id === "string"
|
|
1472
|
+
&& (value.status === "accepted" || value.status === "consumed")
|
|
1473
|
+
&& value.role === "project-manager"
|
|
1474
|
+
&& value.operation === "post-validation-follow-up"
|
|
1475
|
+
&& Number.isInteger(value.baseRevision)
|
|
1476
|
+
&& typeof value.baseHistoryHash === "string"
|
|
1477
|
+
&& (value.effectiveFlow === "code-change"
|
|
1478
|
+
|| value.effectiveFlow === "architect-debug"
|
|
1479
|
+
|| value.effectiveFlow === "architecture-diagnosis")
|
|
1480
|
+
&& value.targetRole === "tester"
|
|
1481
|
+
&& typeof value.evidence === "string"
|
|
1482
|
+
&& typeof value.approvalText === "string"
|
|
1483
|
+
&& typeof value.createdAt === "string"
|
|
1484
|
+
&& (value.consumedAt === undefined || typeof value.consumedAt === "string");
|
|
1485
|
+
}
|
|
1125
1486
|
function normalizeDispatchEvidenceBaseline(value) {
|
|
1126
1487
|
if (!isRecord(value) || !isRecord(value.artifactHashes) || !isRecord(value.gateFingerprints))
|
|
1127
1488
|
return undefined;
|