@xenonbyte/da-vinci-workflow 0.2.7 → 0.2.9

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.
@@ -1,66 +1,116 @@
1
- const fs = require("fs");
2
1
  const path = require("path");
3
2
  const { auditProject } = require("./audit");
4
- const {
5
- parseCheckpointStatusMap,
6
- normalizeCheckpointLabel,
7
- parseDisciplineMarkers,
8
- DISCIPLINE_MARKER_NAMES
9
- } = require("./audit-parsers");
10
3
  const { pathExists, readTextIfExists, dedupeMessages } = require("./utils");
11
4
  const {
12
- parseTasksArtifact,
13
5
  listImmediateDirs,
14
6
  hasAnyFile,
15
7
  pickLatestChange,
16
8
  detectSpecFiles
17
9
  } = require("./planning-parsers");
10
+ const { STATUS, getStageById } = require("./workflow-contract");
11
+ const { readCheckpointStatuses } = require("./workflow-stage");
12
+ const {
13
+ buildPersistedWorkflowBaseView,
14
+ buildDerivedWorkflowBaseView
15
+ } = require("./workflow-base-view");
18
16
  const {
19
- STATUS,
20
- CHECKPOINT_LABELS,
21
- HANDOFF_GATES,
22
- getStageById,
23
- mergeStatuses
24
- } = require("./workflow-contract");
17
+ deriveTaskGroupMetadata,
18
+ deriveTaskGroupRuntimeState,
19
+ buildTaskGroupMetadataPayload,
20
+ resolvePersistedTaskGroupSeed,
21
+ selectFocusedTaskGroup
22
+ } = require("./workflow-task-groups");
25
23
  const {
26
24
  selectPersistedStateForChange,
27
25
  persistDerivedWorkflowResult,
28
- resolveTaskGroupMetadataPath,
29
26
  writeTaskGroupMetadata,
30
- readTaskGroupMetadata,
31
- digestForPath,
32
27
  sanitizePersistedNotes
33
28
  } = require("./workflow-persisted-state");
34
29
  const {
35
30
  readExecutionSignals,
36
- summarizeSignalsBySurface,
37
- listSignalsBySurfacePrefix
31
+ summarizeSignalsBySurface
38
32
  } = require("./execution-signals");
39
- const { evaluatePlanningSignalFreshness } = require("./planning-signal-freshness");
40
- const { deriveExecutionProfile } = require("./execution-profile");
41
33
  const { collectVerificationFreshness } = require("./verify");
42
- const { runWorktreePreflight } = require("./worktree-preflight");
34
+ const {
35
+ collectPlanningSignalFreshnessState,
36
+ inspectDisciplineState,
37
+ applyWorkflowOverlays
38
+ } = require("./workflow-overlay");
39
+ const {
40
+ formatPathRef,
41
+ emitWorkflowDecisionTraces,
42
+ shouldTraceWorkflowDecisions
43
+ } = require("./workflow-decision-trace");
43
44
 
44
45
  const MAX_REPORTED_MESSAGES = 3;
45
- // Task-group metadata is versioned independently from workflow route snapshots.
46
- const TASK_GROUP_METADATA_VERSION = 2;
47
- const BLOCKING_GATE_PRIORITY = Object.freeze([
48
- "clarify",
49
- "scenarioQuality",
50
- "analyze",
51
- "taskCheckpoint",
52
- "stalePlanningSignal",
53
- "principleInheritance",
54
- "lint-tasks",
55
- "lint-spec",
56
- "scope-check"
57
- ]);
58
- const PLANNING_SIGNAL_PROMOTION_FALLBACKS = Object.freeze({
59
- "lint-spec": "breakdown",
60
- "scope-check": "tasks",
61
- "lint-tasks": "tasks"
46
+ const PERSISTED_STATE_TRACE_KEYS = Object.freeze({
47
+ missing: "fallback_missing",
48
+ "parse-error": "fallback_parse_error",
49
+ "version-mismatch": "fallback_version_mismatch",
50
+ "change-missing": "fallback_change_missing",
51
+ "fingerprint-mismatch": "fallback_fingerprint_mismatch"
62
52
  });
63
53
 
54
+ /**
55
+ * Append a workflow decision-trace record when tracing is enabled for the current run.
56
+ *
57
+ * @param {Array<object> | null} records
58
+ * @param {object} record
59
+ * @returns {void}
60
+ */
61
+ function recordWorkflowDecision(records, record) {
62
+ if (!Array.isArray(records) || !record || typeof record !== "object") {
63
+ return;
64
+ }
65
+ records.push(record);
66
+ }
67
+
68
+ /**
69
+ * Flush decision-trace records and attach non-fatal trace diagnostics to the workflow result.
70
+ *
71
+ * @param {object} result
72
+ * @param {{ env?: object, surface?: string, projectRoot?: string, records?: Array<object> | null }} [options]
73
+ * @returns {object}
74
+ */
75
+ function finalizeResultWithWorkflowDecisionTracing(result, options = {}) {
76
+ const traceResult = emitWorkflowDecisionTraces({
77
+ env: options.env,
78
+ surface: options.surface,
79
+ projectRoot: result && result.projectRoot ? result.projectRoot : options.projectRoot,
80
+ changeId: result ? result.changeId : null,
81
+ stage: result ? result.stage : "",
82
+ records: options.records
83
+ });
84
+ if (
85
+ result &&
86
+ typeof result === "object" &&
87
+ traceResult &&
88
+ traceResult.enabled &&
89
+ (traceResult.rejectedCount > 0 || traceResult.error)
90
+ ) {
91
+ result.traceDiagnostics = {
92
+ enabled: true,
93
+ written: traceResult.written,
94
+ rejectedCount: traceResult.rejectedCount,
95
+ rejections: Array.isArray(traceResult.rejections) ? traceResult.rejections : [],
96
+ tracePath: traceResult.tracePath,
97
+ error:
98
+ traceResult.error && traceResult.error.message
99
+ ? traceResult.error.message
100
+ : traceResult.error
101
+ ? String(traceResult.error)
102
+ : null
103
+ };
104
+ }
105
+ return result;
106
+ }
107
+
108
+ /**
109
+ * Reduce full audit payloads to the capped summary stored on workflow results.
110
+ *
111
+ * @param {object | null} result
112
+ * @returns {{ status: string, failures: string[], warnings: string[], notes: string[] } | null}
113
+ */
64
114
  function summarizeAudit(result) {
65
115
  if (!result) {
66
116
  return null;
@@ -74,439 +124,14 @@ function summarizeAudit(result) {
74
124
  };
75
125
  }
76
126
 
77
- function dedupeFindings(findings) {
78
- findings.blockers = dedupeMessages(findings.blockers);
79
- findings.warnings = dedupeMessages(findings.warnings);
80
- findings.notes = dedupeMessages(findings.notes);
81
- }
82
-
83
- function resolveDisciplineGateMode() {
84
- const strictEnv = String(process.env.DA_VINCI_DISCIPLINE_REQUIRE_APPROVAL || "").trim();
85
- const strict = strictEnv === "1" || /^true$/i.test(strictEnv);
86
- return {
87
- strict
88
- };
89
- }
90
-
91
- function isStrictPromotionEnabled() {
92
- const raw = String(process.env.DA_VINCI_DISCIPLINE_STRICT_PROMOTION || "").trim();
93
- return raw === "1" || /^true$/i.test(raw);
94
- }
95
-
96
- function fallbackStageIfBeyond(currentStageId, targetStageId) {
97
- const current = getStageById(currentStageId);
98
- const target = getStageById(targetStageId);
99
- if (!current || !target) {
100
- return currentStageId;
101
- }
102
- if (current.order > target.order) {
103
- return target.id;
104
- }
105
- return current.id;
106
- }
107
-
108
- function ensureGateTracking(findings) {
109
- if (!Array.isArray(findings.blockingGates)) {
110
- findings.blockingGates = [];
111
- }
112
- }
113
-
114
- function collectGateEvidenceRefs(gate) {
115
- return Array.isArray(gate && gate.evidence) ? gate.evidence.slice(0, MAX_REPORTED_MESSAGES) : [];
116
- }
117
-
118
- function addBlockingGateRecord(findings, gateId, surface, gate, reason) {
119
- ensureGateTracking(findings);
120
- findings.blockingGates.push({
121
- id: gateId,
122
- surface,
123
- reason: String(reason || "").trim(),
124
- evidence: collectGateEvidenceRefs(gate)
125
- });
126
- }
127
-
128
- function collectPlanningSignalFreshnessState(projectRoot, changeId, signalSummary) {
129
- const effectiveSignalSummary =
130
- signalSummary && typeof signalSummary === "object"
131
- ? { ...signalSummary }
132
- : {};
133
- const stalePlanningSignals = {};
134
-
135
- if (!changeId) {
136
- return {
137
- effectiveSignalSummary,
138
- stalePlanningSignals,
139
- needsRerunSurfaces: []
140
- };
141
- }
142
-
143
- for (const surface of Object.keys(PLANNING_SIGNAL_PROMOTION_FALLBACKS)) {
144
- const signal = effectiveSignalSummary[surface];
145
- if (!signal) {
146
- continue;
147
- }
148
- const freshness = evaluatePlanningSignalFreshness(projectRoot, {
149
- changeId,
150
- surface,
151
- signal
152
- });
153
- if (!freshness.applicable || freshness.fresh) {
154
- continue;
155
- }
156
- stalePlanningSignals[surface] = {
157
- surface,
158
- reasons: Array.isArray(freshness.reasons) ? freshness.reasons.slice() : [],
159
- signalStatus: normalizeSignalStatus(signal.status),
160
- signalTimestamp: signal && signal.timestamp ? String(signal.timestamp) : null,
161
- signalTimestampMs: freshness.signalTimestampMs,
162
- latestArtifactMtimeMs: freshness.latestArtifactMtimeMs,
163
- latestArtifactTimestamp:
164
- freshness.latestArtifactMtimeMs > 0 ? new Date(freshness.latestArtifactMtimeMs).toISOString() : null,
165
- staleByMs: freshness.staleByMs
166
- };
167
- delete effectiveSignalSummary[surface];
168
- }
169
-
170
- return {
171
- effectiveSignalSummary,
172
- stalePlanningSignals,
173
- needsRerunSurfaces: Object.keys(stalePlanningSignals).sort()
174
- };
175
- }
176
-
177
- function applyPlanningSignalFreshnessFindings(stageId, findings, planningSignalFreshness) {
178
- let nextStageId = stageId;
179
- const strictPromotion = isStrictPromotionEnabled();
180
- const stalePlanningSignals =
181
- planningSignalFreshness && planningSignalFreshness.stalePlanningSignals
182
- ? planningSignalFreshness.stalePlanningSignals
183
- : {};
184
-
185
- for (const surface of Object.keys(stalePlanningSignals).sort()) {
186
- const freshness = stalePlanningSignals[surface];
187
- const reasonText =
188
- Array.isArray(freshness.reasons) && freshness.reasons.length > 0
189
- ? freshness.reasons.join(", ")
190
- : "unknown_staleness_reason";
191
- findings.warnings.push(
192
- `Stale ${surface} planning signal requires rerun before routing can rely on it (${reasonText}).`
193
- );
194
- if (!strictPromotion) {
195
- continue;
196
- }
197
- findings.blockers.push(
198
- `DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; stale ${surface} planning signal blocks promotion until ${surface} is rerun.`
199
- );
200
- addBlockingGateRecord(
201
- findings,
202
- "stalePlanningSignal",
203
- surface,
204
- null,
205
- `strict promotion requires rerun for stale ${surface} planning signal`
206
- );
207
- nextStageId = fallbackStageIfBeyond(
208
- nextStageId,
209
- PLANNING_SIGNAL_PROMOTION_FALLBACKS[surface] || nextStageId
210
- );
211
- }
212
-
213
- return nextStageId;
214
- }
215
-
216
- function selectBlockingGateIdentity(findings) {
217
- const candidates = Array.isArray(findings && findings.blockingGates) ? findings.blockingGates : [];
218
- if (candidates.length === 0) {
219
- return null;
220
- }
221
- const sorted = candidates
222
- .slice()
223
- .sort((left, right) => {
224
- const leftPriority = BLOCKING_GATE_PRIORITY.indexOf(left.id);
225
- const rightPriority = BLOCKING_GATE_PRIORITY.indexOf(right.id);
226
- const normalizedLeft = leftPriority === -1 ? Number.MAX_SAFE_INTEGER : leftPriority;
227
- const normalizedRight = rightPriority === -1 ? Number.MAX_SAFE_INTEGER : rightPriority;
228
- if (normalizedLeft !== normalizedRight) {
229
- return normalizedLeft - normalizedRight;
230
- }
231
- return String(left.surface || "").localeCompare(String(right.surface || ""));
232
- });
233
- return sorted[0];
234
- }
235
-
236
- function getSignalGate(signal, gateKey) {
237
- if (!signal || !signal.details || !signal.details.gates) {
238
- return null;
239
- }
240
- const gates = signal.details.gates;
241
- if (!gates || typeof gates !== "object") {
242
- return null;
243
- }
244
- if (!gateKey) {
245
- return null;
246
- }
247
- return gates[gateKey] && typeof gates[gateKey] === "object" ? gates[gateKey] : null;
248
- }
249
-
250
- function normalizeSignalStatus(status) {
251
- const normalized = String(status || "").trim().toUpperCase();
252
- if (normalized === STATUS.BLOCK || normalized === STATUS.WARN || normalized === STATUS.PASS) {
253
- return normalized;
254
- }
255
- return "";
256
- }
257
-
258
- function statusSeverity(status) {
259
- if (status === STATUS.BLOCK) {
260
- return 2;
261
- }
262
- if (status === STATUS.WARN) {
263
- return 1;
264
- }
265
- if (status === STATUS.PASS) {
266
- return 0;
267
- }
268
- return -1;
269
- }
270
-
271
- function clampGateStatusBySignal(gateStatus, signalStatus) {
272
- const normalizedGate = normalizeSignalStatus(gateStatus);
273
- const normalizedSignal = normalizeSignalStatus(signalStatus);
274
- if (!normalizedGate && normalizedSignal) {
275
- return normalizedSignal;
276
- }
277
- if (normalizedGate && !normalizedSignal) {
278
- return normalizedGate;
279
- }
280
- if (!normalizedGate && !normalizedSignal) {
281
- return STATUS.PASS;
282
- }
283
- return statusSeverity(normalizedGate) >= statusSeverity(normalizedSignal)
284
- ? normalizedGate
285
- : normalizedSignal;
286
- }
287
-
288
- function resolveEffectiveGateStatus(gate, signal) {
289
- const gateStatus = normalizeSignalStatus(gate && gate.status);
290
- if (gateStatus) {
291
- return gateStatus;
292
- }
293
- const signalStatus = normalizeSignalStatus(signal && signal.status);
294
- return signalStatus || STATUS.PASS;
295
- }
296
-
297
- function statusTokenMatches(status, acceptedTokens) {
298
- const normalized = String(status || "").trim().toUpperCase();
299
- return acceptedTokens.includes(normalized);
300
- }
301
-
302
- function inspectDisciplineState(changeDir) {
303
- const designPath = path.join(changeDir, "design.md");
304
- const pencilDesignPath = path.join(changeDir, "pencil-design.md");
305
- const pencilBindingsPath = path.join(changeDir, "pencil-bindings.md");
306
- const tasksPath = path.join(changeDir, "tasks.md");
307
- const designText = readTextIfExists(designPath);
308
- const tasksText = readTextIfExists(tasksPath);
309
- const designMarkers = parseDisciplineMarkers(designText);
310
- const taskMarkers = parseDisciplineMarkers(tasksText);
311
- const gateMode = resolveDisciplineGateMode();
312
- const hasAnyMarker =
313
- designMarkers.ordered.length > 0 ||
314
- taskMarkers.ordered.length > 0 ||
315
- designMarkers.malformed.length > 0 ||
316
- taskMarkers.malformed.length > 0;
317
-
318
- const designApproval =
319
- designMarkers.markers[DISCIPLINE_MARKER_NAMES.designApproval] ||
320
- taskMarkers.markers[DISCIPLINE_MARKER_NAMES.designApproval] ||
321
- null;
322
- const planSelfReview = taskMarkers.markers[DISCIPLINE_MARKER_NAMES.planSelfReview] || null;
323
- const operatorReviewAck = taskMarkers.markers[DISCIPLINE_MARKER_NAMES.operatorReviewAck] || null;
324
-
325
- const blockers = [];
326
- const warnings = [];
327
- const notes = [];
328
- const designApprovalArtifacts = [
329
- { label: "design.md", path: designPath },
330
- { label: "pencil-design.md", path: pencilDesignPath },
331
- { label: "pencil-bindings.md", path: pencilBindingsPath }
332
- ];
333
-
334
- for (const malformed of [...designMarkers.malformed, ...taskMarkers.malformed]) {
335
- warnings.push(`Malformed discipline marker at line ${malformed.line}: ${malformed.reason}`);
336
- }
337
-
338
- let designApprovalState = "missing";
339
- let designApprovalStale = false;
340
- if (designApproval) {
341
- if (!statusTokenMatches(designApproval.status, ["APPROVED", "PASS", "ACCEPTED"])) {
342
- designApprovalState = "rejected";
343
- blockers.push(
344
- `Design approval marker is not approved (${designApproval.status}); keep workflow in tasks/design.`
345
- );
346
- } else {
347
- designApprovalState = "approved";
348
- if (designApproval.time) {
349
- const approvalMs = Date.parse(designApproval.time);
350
- const staleArtifacts = [];
351
- if (Number.isFinite(approvalMs)) {
352
- for (const artifact of designApprovalArtifacts) {
353
- let artifactMtimeMs = 0;
354
- try {
355
- artifactMtimeMs = fs.statSync(artifact.path).mtimeMs;
356
- } catch (_error) {
357
- continue;
358
- }
359
- if (artifactMtimeMs > approvalMs) {
360
- staleArtifacts.push(artifact.label);
361
- }
362
- }
363
- }
364
- if (staleArtifacts.length > 0) {
365
- designApprovalState = "stale";
366
- designApprovalStale = true;
367
- blockers.push(
368
- `Design approval marker is stale because design artifacts changed after approval timestamp: ${staleArtifacts.join(", ")}.`
369
- );
370
- }
371
- } else {
372
- warnings.push("Design approval marker is missing timestamp; staleness checks are limited.");
373
- }
374
- }
375
- } else if (hasAnyMarker || gateMode.strict) {
376
- blockers.push(
377
- "Missing required design approval marker (`- design approval: APPROVED @ <ISO8601>`) for disciplined handoff."
378
- );
379
- } else {
380
- warnings.push(
381
- "Design approval marker is missing; legacy compatibility mode keeps this advisory. Set DA_VINCI_DISCIPLINE_REQUIRE_APPROVAL=1 to enforce."
382
- );
383
- notes.push("Legacy compatibility mode: missing discipline markers are advisory.");
384
- }
385
-
386
- if (!planSelfReview) {
387
- warnings.push("Missing plan self-review marker in tasks.md (`- plan self review: PASS @ <ISO8601>`).");
388
- } else if (!statusTokenMatches(planSelfReview.status, ["PASS", "APPROVED", "DONE"])) {
389
- warnings.push(`Plan self-review marker is not PASS (${planSelfReview.status}).`);
390
- }
391
-
392
- if (!operatorReviewAck) {
393
- warnings.push("Missing operator review acknowledgment marker in tasks.md (`- operator review ack: ACKNOWLEDGED @ <ISO8601>`).");
394
- } else if (!statusTokenMatches(operatorReviewAck.status, ["ACKNOWLEDGED", "ACK", "CONFIRMED", "APPROVED"])) {
395
- warnings.push(`Operator review acknowledgment marker is not acknowledged (${operatorReviewAck.status}).`);
396
- }
397
-
398
- return {
399
- strictMode: gateMode.strict,
400
- hasAnyMarker,
401
- designApproval: {
402
- state: designApprovalState,
403
- stale: designApprovalStale,
404
- marker: designApproval
405
- },
406
- planSelfReview: {
407
- marker: planSelfReview
408
- },
409
- operatorReviewAck: {
410
- marker: operatorReviewAck
411
- },
412
- malformed: [...designMarkers.malformed, ...taskMarkers.malformed],
413
- blockers,
414
- warnings,
415
- notes
416
- };
417
- }
418
-
419
- function mergeSignalBySurface(signals) {
420
- const summary = {};
421
- for (const signal of signals || []) {
422
- const key = String(signal.surface || "").trim();
423
- if (!key || summary[key]) {
424
- continue;
425
- }
426
- summary[key] = signal;
427
- }
428
- return summary;
429
- }
430
-
431
- function applyTaskExecutionAndReviewFindings(findings, signals) {
432
- const taskExecutionSignals = listSignalsBySurfacePrefix(signals, "task-execution.");
433
- const taskReviewSignals = listSignalsBySurfacePrefix(signals, "task-review.");
434
- const latestTaskExecution = mergeSignalBySurface(taskExecutionSignals);
435
- const latestTaskReview = mergeSignalBySurface(taskReviewSignals);
436
-
437
- for (const signal of Object.values(latestTaskExecution)) {
438
- const envelope = signal.details && signal.details.envelope ? signal.details.envelope : null;
439
- const outOfScopeWrites =
440
- signal.details && Array.isArray(signal.details.outOfScopeWrites)
441
- ? dedupeMessages(signal.details.outOfScopeWrites.map((item) => String(item || "").trim()).filter(Boolean))
442
- : [];
443
- const taskGroupId =
444
- (envelope && envelope.taskGroupId) ||
445
- String(signal.surface || "").replace(/^task-execution\./, "") ||
446
- "unknown";
447
- if (signal.status === STATUS.BLOCK) {
448
- findings.blockers.push(`Task group ${taskGroupId} is BLOCKED in implementer status envelope.`);
449
- } else if (signal.status === STATUS.WARN) {
450
- findings.warnings.push(`Task group ${taskGroupId} has unresolved implementer concerns/context needs.`);
451
- }
452
- if (outOfScopeWrites.length > 0) {
453
- findings.blockers.push(
454
- `Task group ${taskGroupId} reported out-of-scope writes: ${outOfScopeWrites.join(", ")}.`
455
- );
456
- }
457
- if (envelope && envelope.summary) {
458
- findings.notes.push(`Implementer summary ${taskGroupId}: ${envelope.summary}`);
459
- }
460
- }
461
-
462
- const reviewStateByGroup = {};
463
- for (const signal of Object.values(latestTaskReview)) {
464
- const envelope = signal.details && signal.details.envelope ? signal.details.envelope : null;
465
- const taskGroupId =
466
- (envelope && envelope.taskGroupId) ||
467
- String(signal.surface || "").replace(/^task-review\./, "").split(".")[0] ||
468
- "unknown";
469
- const stage = envelope && envelope.stage ? envelope.stage : String(signal.surface || "").split(".").pop();
470
- if (!reviewStateByGroup[taskGroupId]) {
471
- reviewStateByGroup[taskGroupId] = {};
472
- }
473
- reviewStateByGroup[taskGroupId][stage] = signal.status;
474
- if (signal.status === STATUS.BLOCK) {
475
- findings.blockers.push(`Task review ${taskGroupId}/${stage} is BLOCK.`);
476
- } else if (signal.status === STATUS.WARN) {
477
- findings.warnings.push(`Task review ${taskGroupId}/${stage} is WARN and requires follow-up.`);
478
- }
479
- }
480
-
481
- for (const [taskGroupId, state] of Object.entries(reviewStateByGroup)) {
482
- if (state.quality && !state.spec) {
483
- findings.blockers.push(
484
- `Task review ordering violation for ${taskGroupId}: quality review exists without a prior spec review result.`
485
- );
486
- continue;
487
- }
488
- if (state.quality && state.spec === STATUS.WARN) {
489
- findings.blockers.push(
490
- `Task review ordering violation for ${taskGroupId}: quality review was recorded before spec review reached PASS.`
491
- );
492
- continue;
493
- }
494
- if (state.quality && state.spec === STATUS.BLOCK) {
495
- findings.blockers.push(
496
- `Task review ordering violation for ${taskGroupId}: quality review was recorded while spec review is BLOCK.`
497
- );
498
- }
499
- }
500
- }
501
-
502
- function normalizeCheckpointStatus(status) {
503
- const normalized = String(status || "").toUpperCase();
504
- if (normalized === STATUS.PASS || normalized === STATUS.WARN || normalized === STATUS.BLOCK) {
505
- return normalized;
506
- }
507
- return STATUS.PASS;
508
- }
509
-
127
+ /**
128
+ * Collect the integrity audit with the already-computed routing signal summary preloaded.
129
+ *
130
+ * @param {string} projectRoot
131
+ * @param {string} workflowRoot
132
+ * @param {Object.<string, object>} signalSummary
133
+ * @returns {object | null}
134
+ */
510
135
  function collectIntegrityAudit(projectRoot, workflowRoot, signalSummary) {
511
136
  if (!pathExists(workflowRoot)) {
512
137
  return null;
@@ -519,6 +144,15 @@ function collectIntegrityAudit(projectRoot, workflowRoot, signalSummary) {
519
144
  );
520
145
  }
521
146
 
147
+ /**
148
+ * Collect the completion audit for an active change with the already-computed routing signal summary.
149
+ *
150
+ * @param {string} projectRoot
151
+ * @param {string} workflowRoot
152
+ * @param {string | null} changeId
153
+ * @param {Object.<string, object>} signalSummary
154
+ * @returns {object | null}
155
+ */
522
156
  function collectCompletionAudit(projectRoot, workflowRoot, changeId, signalSummary) {
523
157
  if (!changeId || !pathExists(workflowRoot)) {
524
158
  return null;
@@ -532,306 +166,13 @@ function collectCompletionAudit(projectRoot, workflowRoot, changeId, signalSumma
532
166
  );
533
167
  }
534
168
 
535
- function applyAuditFindings(stageId, findings, integrityAudit, completionAudit) {
536
- let nextStageId = stageId;
537
-
538
- if (integrityAudit && integrityAudit.status === "FAIL") {
539
- findings.warnings.push("Integrity audit currently reports FAIL.");
540
- } else if (integrityAudit && integrityAudit.status === "WARN") {
541
- findings.warnings.push("Integrity audit currently reports WARN.");
542
- }
543
-
544
- if ((nextStageId === "verify" || nextStageId === "complete") && completionAudit) {
545
- if (completionAudit.status === "PASS") {
546
- if (nextStageId === "verify") {
547
- nextStageId = "complete";
548
- }
549
- findings.notes.push("Completion audit reports PASS for the active change.");
550
- } else if (completionAudit.status === "WARN") {
551
- findings.warnings.push("Completion audit currently reports WARN.");
552
- if (nextStageId === "complete") {
553
- nextStageId = "verify";
554
- }
555
- } else if (completionAudit.status === "FAIL") {
556
- findings.blockers.push("Completion audit currently reports FAIL.");
557
- if (nextStageId === "complete") {
558
- nextStageId = "verify";
559
- }
560
- }
561
- }
562
-
563
- return nextStageId;
564
- }
565
-
566
- function applyExecutionSignalFindings(stageId, findings, signalSummary) {
567
- let nextStageId = stageId;
568
- const strictPromotion = isStrictPromotionEnabled();
569
- const signalParseIssue = signalSummary["signal-file-parse"];
570
- if (signalParseIssue) {
571
- const warningText =
572
- Array.isArray(signalParseIssue.warnings) && signalParseIssue.warnings[0]
573
- ? String(signalParseIssue.warnings[0])
574
- : "Malformed execution signal files were detected.";
575
- findings.warnings.push(warningText);
576
- }
577
-
578
- const diffSignal = signalSummary["diff-spec"];
579
- if (diffSignal && diffSignal.status && diffSignal.status !== STATUS.PASS) {
580
- findings.warnings.push(`Planning diff signal diff-spec reports ${diffSignal.status}.`);
581
- }
582
-
583
- const lintSpecSignal = signalSummary["lint-spec"];
584
- const lintSpecGateConfigs = [
585
- { id: "principleInheritance", label: "principleInheritance", fallbackStage: "breakdown" },
586
- { id: "clarify", label: "clarify", fallbackStage: "breakdown" },
587
- { id: "scenarioQuality", label: "scenarioQuality", fallbackStage: "breakdown" }
588
- ];
589
- let lintSpecGateObserved = false;
590
- let strongestLintSpecGateStatus = "";
591
- for (const config of lintSpecGateConfigs) {
592
- const gate = getSignalGate(lintSpecSignal, config.id);
593
- if (!gate) {
594
- continue;
595
- }
596
- lintSpecGateObserved = true;
597
- const gateStatus = resolveEffectiveGateStatus(gate, lintSpecSignal);
598
- if (statusSeverity(gateStatus) > statusSeverity(strongestLintSpecGateStatus)) {
599
- strongestLintSpecGateStatus = gateStatus;
600
- }
601
- const evidenceRefs = collectGateEvidenceRefs(gate);
602
- const evidenceSuffix =
603
- evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
604
- if (gateStatus === STATUS.BLOCK) {
605
- findings.blockers.push(
606
- `lint-spec gate ${config.label} is BLOCK and prevents planning promotion.${evidenceSuffix}`
607
- );
608
- addBlockingGateRecord(
609
- findings,
610
- config.id,
611
- "lint-spec",
612
- gate,
613
- `lint-spec gate ${config.label} is BLOCK`
614
- );
615
- nextStageId = fallbackStageIfBeyond(nextStageId, config.fallbackStage);
616
- } else if (gateStatus === STATUS.WARN) {
617
- findings.warnings.push(`lint-spec gate ${config.label} is WARN.${evidenceSuffix}`);
618
- if (strictPromotion) {
619
- findings.blockers.push(
620
- `DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-spec gate ${config.label} WARN blocks promotion.`
621
- );
622
- addBlockingGateRecord(
623
- findings,
624
- config.id,
625
- "lint-spec",
626
- gate,
627
- `strict promotion escalated lint-spec gate ${config.label} WARN`
628
- );
629
- nextStageId = fallbackStageIfBeyond(nextStageId, config.fallbackStage);
630
- }
631
- }
632
- for (const message of Array.isArray(gate.compatibility) ? gate.compatibility : []) {
633
- findings.notes.push(`lint-spec gate ${config.label} compatibility: ${message}`);
634
- }
635
- if (config.id === "clarify") {
636
- for (const bounded of Array.isArray(gate.bounded) ? gate.bounded : []) {
637
- findings.notes.push(`lint-spec gate clarify bounded ambiguity: ${bounded}`);
638
- }
639
- }
640
- }
641
- const lintSpecSignalStatus = normalizeSignalStatus(lintSpecSignal && lintSpecSignal.status);
642
- if (
643
- lintSpecSignal &&
644
- (!lintSpecGateObserved || statusSeverity(lintSpecSignalStatus) > statusSeverity(strongestLintSpecGateStatus))
645
- ) {
646
- if (lintSpecSignalStatus === STATUS.BLOCK) {
647
- findings.blockers.push("lint-spec signal is BLOCK.");
648
- addBlockingGateRecord(
649
- findings,
650
- "lint-spec",
651
- "lint-spec",
652
- lintSpecSignal.details && lintSpecSignal.details.gates ? lintSpecSignal.details.gates : null,
653
- "lint-spec signal is BLOCK"
654
- );
655
- nextStageId = fallbackStageIfBeyond(nextStageId, "breakdown");
656
- } else if (lintSpecSignalStatus === STATUS.WARN) {
657
- findings.warnings.push("lint-spec signal is WARN.");
658
- if (strictPromotion) {
659
- findings.blockers.push(
660
- "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-spec WARN blocks promotion."
661
- );
662
- addBlockingGateRecord(
663
- findings,
664
- "lint-spec",
665
- "lint-spec",
666
- lintSpecSignal.details && lintSpecSignal.details.gates ? lintSpecSignal.details.gates : null,
667
- "strict promotion escalated lint-spec WARN"
668
- );
669
- nextStageId = fallbackStageIfBeyond(nextStageId, "breakdown");
670
- }
671
- }
672
- }
673
-
674
- const analyzeSignal = signalSummary["scope-check"];
675
- const analyzeGate = getSignalGate(analyzeSignal, "analyze");
676
- let analyzeGateStatus = "";
677
- if (analyzeGate) {
678
- const evidenceRefs = collectGateEvidenceRefs(analyzeGate);
679
- const evidenceSuffix =
680
- evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
681
- analyzeGateStatus = resolveEffectiveGateStatus(analyzeGate, analyzeSignal);
682
- if (analyzeGateStatus === STATUS.BLOCK) {
683
- findings.blockers.push(`scope-check gate analyze is BLOCK.${evidenceSuffix}`);
684
- addBlockingGateRecord(
685
- findings,
686
- "analyze",
687
- "scope-check",
688
- analyzeGate,
689
- "scope-check gate analyze is BLOCK"
690
- );
691
- nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
692
- } else if (analyzeGateStatus === STATUS.WARN) {
693
- findings.warnings.push(`scope-check gate analyze is WARN.${evidenceSuffix}`);
694
- if (strictPromotion) {
695
- findings.blockers.push(
696
- "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; scope-check gate analyze WARN blocks promotion."
697
- );
698
- addBlockingGateRecord(
699
- findings,
700
- "analyze",
701
- "scope-check",
702
- analyzeGate,
703
- "strict promotion escalated scope-check gate analyze WARN"
704
- );
705
- nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
706
- }
707
- }
708
- for (const message of Array.isArray(analyzeGate.compatibility) ? analyzeGate.compatibility : []) {
709
- findings.notes.push(`scope-check gate analyze compatibility: ${message}`);
710
- }
711
- }
712
- const analyzeSignalStatus = normalizeSignalStatus(analyzeSignal && analyzeSignal.status);
713
- if (
714
- analyzeSignal &&
715
- (!analyzeGate || statusSeverity(analyzeSignalStatus) > statusSeverity(analyzeGateStatus))
716
- ) {
717
- if (analyzeSignalStatus === STATUS.BLOCK) {
718
- findings.blockers.push("scope-check signal is BLOCK.");
719
- addBlockingGateRecord(
720
- findings,
721
- "scope-check",
722
- "scope-check",
723
- analyzeSignal.details && analyzeSignal.details.gates ? analyzeSignal.details.gates : null,
724
- "scope-check signal is BLOCK"
725
- );
726
- nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
727
- } else if (analyzeSignalStatus === STATUS.WARN) {
728
- findings.warnings.push("scope-check signal is WARN.");
729
- if (strictPromotion) {
730
- findings.blockers.push(
731
- "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; scope-check WARN blocks promotion."
732
- );
733
- addBlockingGateRecord(
734
- findings,
735
- "scope-check",
736
- "scope-check",
737
- analyzeSignal.details && analyzeSignal.details.gates ? analyzeSignal.details.gates : null,
738
- "strict promotion escalated scope-check WARN"
739
- );
740
- nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
741
- }
742
- }
743
- }
744
-
745
- const lintTasksSignal = signalSummary["lint-tasks"];
746
- const taskCheckpointGate = getSignalGate(lintTasksSignal, "taskCheckpoint");
747
- const taskCheckpointGateStatus = taskCheckpointGate
748
- ? resolveEffectiveGateStatus(taskCheckpointGate, lintTasksSignal)
749
- : "";
750
- const lintTasksSignalStatus = normalizeSignalStatus(lintTasksSignal && lintTasksSignal.status);
751
- if (taskCheckpointGate && taskCheckpointGateStatus === STATUS.BLOCK) {
752
- const evidenceRefs = collectGateEvidenceRefs(taskCheckpointGate);
753
- const evidenceSuffix =
754
- evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
755
- findings.blockers.push(`lint-tasks task-checkpoint is BLOCK and prevents promotion into build.${evidenceSuffix}`);
756
- addBlockingGateRecord(
757
- findings,
758
- "taskCheckpoint",
759
- "lint-tasks",
760
- taskCheckpointGate,
761
- "lint-tasks task-checkpoint is BLOCK"
762
- );
763
- nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
764
- } else if (taskCheckpointGate && taskCheckpointGateStatus === STATUS.WARN) {
765
- const evidenceRefs = collectGateEvidenceRefs(taskCheckpointGate);
766
- const evidenceSuffix =
767
- evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
768
- findings.warnings.push(`lint-tasks task-checkpoint is WARN.${evidenceSuffix}`);
769
- if (strictPromotion) {
770
- findings.blockers.push(
771
- "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-tasks WARN blocks promotion into build."
772
- );
773
- addBlockingGateRecord(
774
- findings,
775
- "taskCheckpoint",
776
- "lint-tasks",
777
- taskCheckpointGate,
778
- "strict promotion escalated lint-tasks task-checkpoint WARN"
779
- );
780
- nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
781
- }
782
- }
783
- if (
784
- lintTasksSignal &&
785
- (!taskCheckpointGate || statusSeverity(lintTasksSignalStatus) > statusSeverity(taskCheckpointGateStatus))
786
- ) {
787
- if (lintTasksSignalStatus === STATUS.BLOCK) {
788
- findings.blockers.push("lint-tasks signal is BLOCK.");
789
- addBlockingGateRecord(
790
- findings,
791
- "lint-tasks",
792
- "lint-tasks",
793
- lintTasksSignal.details && lintTasksSignal.details.gates ? lintTasksSignal.details.gates : null,
794
- "lint-tasks signal is BLOCK"
795
- );
796
- nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
797
- } else if (lintTasksSignalStatus === STATUS.WARN) {
798
- findings.warnings.push("lint-tasks signal is WARN.");
799
- if (strictPromotion) {
800
- findings.blockers.push(
801
- "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-tasks WARN blocks promotion into build."
802
- );
803
- addBlockingGateRecord(
804
- findings,
805
- "lint-tasks",
806
- "lint-tasks",
807
- lintTasksSignal.details && lintTasksSignal.details.gates ? lintTasksSignal.details.gates : null,
808
- "strict promotion escalated lint-tasks WARN"
809
- );
810
- nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
811
- }
812
- }
813
- }
814
- if (taskCheckpointGate) {
815
- for (const message of Array.isArray(taskCheckpointGate.compatibility)
816
- ? taskCheckpointGate.compatibility
817
- : []) {
818
- findings.notes.push(`lint-tasks gate taskCheckpoint compatibility: ${message}`);
819
- }
820
- }
821
-
822
- const verificationSignal = signalSummary["verify-coverage"];
823
- if (verificationSignal && verificationSignal.status === STATUS.BLOCK) {
824
- findings.blockers.push("verify-coverage signal is BLOCK.");
825
- if (nextStageId === "complete") {
826
- nextStageId = "verify";
827
- }
828
- } else if (verificationSignal && verificationSignal.status === STATUS.WARN) {
829
- findings.warnings.push("verify-coverage signal is WARN.");
830
- }
831
-
832
- return nextStageId;
833
- }
834
-
169
+ /**
170
+ * Map the effective workflow stage to the next operator-facing route suggestion.
171
+ *
172
+ * @param {string} stageId
173
+ * @param {{ projectRoot?: string, changeId?: string, ambiguousChangeSelection?: boolean }} [context]
174
+ * @returns {{ route: string | null, command: string | null, reason: string }}
175
+ */
835
176
  function buildRouteRecommendation(stageId, context = {}) {
836
177
  const projectRoot = context.projectRoot || process.cwd();
837
178
  const changeId = context.changeId || "change-001";
@@ -890,872 +231,166 @@ function buildRouteRecommendation(stageId, context = {}) {
890
231
  }
891
232
  }
892
233
 
893
- function readCheckpointStatuses(changeDir) {
894
- const collected = {};
895
- const files = [
896
- path.join(changeDir, "design.md"),
897
- path.join(changeDir, "pencil-design.md"),
898
- path.join(changeDir, "tasks.md"),
899
- path.join(changeDir, "verification.md")
900
- ];
901
-
902
- for (const artifactPath of files) {
903
- const text = readTextIfExists(artifactPath);
904
- if (!text) {
905
- continue;
906
- }
907
- const statuses = parseCheckpointStatusMap(text);
908
- for (const [label, value] of Object.entries(statuses)) {
909
- const normalized = normalizeCheckpointLabel(label);
910
- if (!normalized) {
911
- continue;
912
- }
913
- collected[normalized] = String(value || "").toUpperCase();
914
- }
915
- }
916
-
917
- return collected;
918
- }
919
-
920
- function deriveStageFromArtifacts(artifactState, checkpointStatuses, findings) {
921
- if (!artifactState.workflowRootReady) {
922
- findings.blockers.push("Missing `.da-vinci/` directory.");
923
- return "bootstrap";
924
- }
925
-
926
- if (!artifactState.changeSelected) {
927
- findings.blockers.push("No active change selected under `.da-vinci/changes/`.");
928
- return "breakdown";
929
- }
930
-
931
- if (!artifactState.proposal || artifactState.specFiles.length === 0) {
932
- if (!artifactState.proposal) {
933
- findings.blockers.push("Missing `proposal.md` for the active change.");
934
- }
935
- if (artifactState.specFiles.length === 0) {
936
- findings.blockers.push("Missing `specs/*/spec.md` for the active change.");
937
- }
938
- return "breakdown";
939
- }
940
-
941
- if (
942
- !artifactState.design ||
943
- !artifactState.pencilDesign ||
944
- !artifactState.pencilBindings
945
- ) {
946
- if (!artifactState.design) {
947
- findings.blockers.push("Missing `design.md` for the active change.");
948
- }
949
- if (!artifactState.pencilDesign) {
950
- findings.blockers.push("Missing `pencil-design.md` for the active change.");
951
- }
952
- if (!artifactState.pencilBindings) {
953
- findings.blockers.push("Missing `pencil-bindings.md` for the active change.");
954
- }
955
- return "design";
956
- }
957
-
958
- const designCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.DESIGN];
959
- if (designCheckpoint === STATUS.BLOCK) {
960
- findings.blockers.push("`design checkpoint` is BLOCK.");
961
- return "design";
962
- }
963
- if (designCheckpoint === STATUS.WARN) {
964
- findings.warnings.push("`design checkpoint` is WARN.");
965
- }
966
-
967
- const designSourceCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.DESIGN_SOURCE];
968
- if (designSourceCheckpoint === STATUS.BLOCK) {
969
- findings.blockers.push("`design source checkpoint` is BLOCK.");
970
- return "design";
971
- }
972
- if (designSourceCheckpoint === STATUS.WARN) {
973
- findings.warnings.push("`design source checkpoint` is WARN.");
974
- }
975
-
976
- const runtimeGateCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.RUNTIME_GATE];
977
- if (runtimeGateCheckpoint === STATUS.BLOCK) {
978
- findings.blockers.push("`mcp runtime gate` is BLOCK.");
979
- return "design";
980
- }
981
- if (runtimeGateCheckpoint === STATUS.WARN) {
982
- findings.warnings.push("`mcp runtime gate` is WARN.");
983
- }
984
-
985
- const mappingCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.MAPPING];
986
- if (mappingCheckpoint === STATUS.BLOCK) {
987
- findings.blockers.push("`mapping checkpoint` is BLOCK.");
988
- return "design";
989
- }
990
- if (mappingCheckpoint === STATUS.WARN) {
991
- findings.warnings.push("`mapping checkpoint` is WARN.");
992
- }
993
-
994
- if (!artifactState.tasks) {
995
- findings.blockers.push("Missing `tasks.md` for the active change.");
996
- return "tasks";
997
- }
998
-
999
- const taskCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.TASK];
1000
- if (taskCheckpoint === STATUS.BLOCK) {
1001
- findings.blockers.push("`task checkpoint` is BLOCK.");
1002
- return "tasks";
1003
- }
1004
- if (taskCheckpoint === STATUS.WARN) {
1005
- findings.warnings.push("`task checkpoint` is WARN.");
1006
- }
1007
-
1008
- if (!artifactState.verification) {
1009
- findings.blockers.push("Missing `verification.md` for the active change.");
1010
- return "build";
1011
- }
1012
-
1013
- return "verify";
1014
- }
1015
-
1016
- function deriveTaskGroupMetadata(tasksMarkdownText, checkpointStatuses) {
1017
- if (!tasksMarkdownText) {
1018
- return [];
1019
- }
1020
-
1021
- const taskArtifact = parseTasksArtifact(tasksMarkdownText);
1022
- const lines = String(tasksMarkdownText || "").replace(/\r\n?/g, "\n").split("\n");
1023
- const sections = [];
1024
- let current = null;
1025
- for (const line of lines) {
1026
- const headingMatch = line.match(/^\s{0,3}##\s+(\d+(?:\.\d+)*)\.\s+(.+)$/);
1027
- if (headingMatch) {
1028
- if (current) {
1029
- sections.push(current);
1030
- }
1031
- current = {
1032
- id: headingMatch[1],
1033
- title: String(headingMatch[2] || "").trim(),
1034
- checklistItems: []
1035
- };
1036
- continue;
1037
- }
1038
- if (!current) {
1039
- continue;
1040
- }
1041
- const checklistMatch = line.match(/^\s*-\s*\[([ xX])\]\s+(.+)$/);
1042
- if (checklistMatch) {
1043
- current.checklistItems.push({
1044
- checked: String(checklistMatch[1] || "").toLowerCase() === "x",
1045
- text: String(checklistMatch[2] || "").trim()
1046
- });
1047
- }
1048
- }
1049
- if (current) {
1050
- sections.push(current);
1051
- }
1052
-
1053
- const normalizedTaskCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.TASK] || STATUS.WARN;
1054
- const groupMetadataById = new Map(
1055
- (taskArtifact.taskGroups || []).map((group) => [
1056
- group.id,
1057
- {
1058
- targetFiles: Array.isArray(group.targetFiles) ? group.targetFiles : [],
1059
- fileReferences: Array.isArray(group.fileReferences) ? group.fileReferences : [],
1060
- verificationActions: Array.isArray(group.verificationActions) ? group.verificationActions : [],
1061
- verificationCommands: Array.isArray(group.verificationCommands) ? group.verificationCommands : [],
1062
- executionIntent: Array.isArray(group.executionIntent) ? group.executionIntent : [],
1063
- reviewIntent: group.reviewIntent === true,
1064
- testingIntent: group.testingIntent === true,
1065
- codeChangeLikely: group.codeChangeLikely === true
1066
- }
1067
- ])
1068
- );
1069
- const metadata = sections.map((section, index) => {
1070
- const total = section.checklistItems.length;
1071
- const done = section.checklistItems.filter((item) => item.checked).length;
1072
- const completion = total > 0 ? Math.round((done / total) * 100) : 0;
1073
- const sectionStatus =
1074
- total === 0 ? "pending" : done === 0 ? "pending" : done < total ? "in_progress" : "completed";
1075
- const nextAction =
1076
- sectionStatus === "completed"
1077
- ? "advance to next task group"
1078
- : section.checklistItems.find((item) => !item.checked)?.text || "continue group work";
1079
- const groupMetadata = groupMetadataById.get(section.id) || {};
1080
-
1081
- return {
1082
- taskGroupId: section.id,
1083
- title: section.title,
1084
- status: sectionStatus,
1085
- completion,
1086
- checkpointOutcome: normalizedTaskCheckpoint,
1087
- evidence: section.checklistItems.filter((item) => item.checked).map((item) => item.text),
1088
- nextAction,
1089
- targetFiles: groupMetadata.targetFiles || [],
1090
- fileReferences: groupMetadata.fileReferences || [],
1091
- verificationActions: groupMetadata.verificationActions || [],
1092
- verificationCommands: groupMetadata.verificationCommands || [],
1093
- executionIntent: groupMetadata.executionIntent || [],
1094
- reviewIntent: groupMetadata.reviewIntent === true,
1095
- testingIntent: groupMetadata.testingIntent === true,
1096
- codeChangeLikely: groupMetadata.codeChangeLikely === true,
1097
- resumeCursor: {
1098
- groupIndex: index,
1099
- nextUncheckedItem:
1100
- section.checklistItems.find((item) => !item.checked)?.text ||
1101
- section.checklistItems[section.checklistItems.length - 1]?.text ||
1102
- null
1103
- }
1104
- };
1105
- });
1106
-
1107
- if (metadata.length === 0 && taskArtifact.taskGroups.length > 0) {
1108
- return taskArtifact.taskGroups.map((group, index) => {
1109
- const groupMetadata = groupMetadataById.get(group.id) || {};
1110
- return {
1111
- taskGroupId: group.id,
1112
- title: group.title,
1113
- status: "pending",
1114
- completion: 0,
1115
- checkpointOutcome: normalizedTaskCheckpoint,
1116
- evidence: [],
1117
- nextAction: "start task group",
1118
- targetFiles: groupMetadata.targetFiles || [],
1119
- fileReferences: groupMetadata.fileReferences || [],
1120
- verificationActions: groupMetadata.verificationActions || [],
1121
- verificationCommands: groupMetadata.verificationCommands || [],
1122
- executionIntent: groupMetadata.executionIntent || [],
1123
- reviewIntent: groupMetadata.reviewIntent === true,
1124
- testingIntent: groupMetadata.testingIntent === true,
1125
- codeChangeLikely: groupMetadata.codeChangeLikely === true,
1126
- resumeCursor: {
1127
- groupIndex: index,
1128
- nextUncheckedItem: null
1129
- }
1130
- };
1131
- });
1132
- }
1133
-
1134
- return metadata;
1135
- }
1136
-
1137
- function normalizeTaskGroupId(value) {
1138
- return String(value || "").trim();
1139
- }
1140
-
1141
- function normalizeResumeCursor(cursor, fallbackGroupIndex = null) {
1142
- const groupIndex =
1143
- cursor && Number.isInteger(cursor.groupIndex)
1144
- ? cursor.groupIndex
1145
- : Number.isInteger(fallbackGroupIndex)
1146
- ? fallbackGroupIndex
1147
- : null;
1148
- return {
1149
- groupIndex,
1150
- nextUncheckedItem:
1151
- cursor && Object.prototype.hasOwnProperty.call(cursor, "nextUncheckedItem")
1152
- ? cursor.nextUncheckedItem
1153
- : null
1154
- };
1155
- }
1156
-
1157
- function normalizeTaskGroupSeedMap(taskGroups) {
1158
- const byId = new Map();
1159
- for (const group of Array.isArray(taskGroups) ? taskGroups : []) {
1160
- const taskGroupId = normalizeTaskGroupId(group && (group.taskGroupId || group.id));
1161
- if (!taskGroupId || byId.has(taskGroupId)) {
1162
- continue;
1163
- }
1164
- byId.set(taskGroupId, group);
1165
- }
1166
- return byId;
1167
- }
1168
-
1169
- function findLatestSignalBySurface(signals, surface) {
1170
- const normalizedSurface = String(surface || "").trim();
1171
- if (!normalizedSurface) {
1172
- return null;
1173
- }
1174
- return (signals || []).find((signal) => String(signal.surface || "").trim() === normalizedSurface) || null;
1175
- }
1176
-
1177
- function summarizeSignalIssues(signal, envelopeItems) {
1178
- if (!signal) {
1179
- return [];
1180
- }
1181
- const fromEnvelope = Array.isArray(envelopeItems) ? envelopeItems : [];
1182
- const fromSignal = [
1183
- ...(Array.isArray(signal.failures) ? signal.failures : []),
1184
- ...(Array.isArray(signal.warnings) ? signal.warnings : [])
1185
- ];
1186
- return dedupeMessages([...fromEnvelope, ...fromSignal].map((item) => String(item || "").trim()).filter(Boolean));
1187
- }
1188
-
1189
- function buildTaskGroupImplementerState(taskGroupId, signals, fallbackState) {
1190
- const fallback = fallbackState && typeof fallbackState === "object" ? fallbackState : {};
1191
- const signal = findLatestSignalBySurface(signals, `task-execution.${taskGroupId}`);
1192
- const envelope = signal && signal.details && signal.details.envelope ? signal.details.envelope : null;
1193
- if (!signal) {
1194
- return {
1195
- present: false,
1196
- signalStatus: null,
1197
- implementerStatus: fallback.implementerStatus || null,
1198
- summary: fallback.summary || null,
1199
- changedFiles: Array.isArray(fallback.changedFiles) ? fallback.changedFiles : [],
1200
- testEvidence: Array.isArray(fallback.testEvidence) ? fallback.testEvidence : [],
1201
- concerns: Array.isArray(fallback.concerns) ? fallback.concerns : [],
1202
- blockers: Array.isArray(fallback.blockers) ? fallback.blockers : [],
1203
- outOfScopeWrites: Array.isArray(fallback.outOfScopeWrites) ? fallback.outOfScopeWrites : [],
1204
- recordedAt: fallback.recordedAt || null
1205
- };
1206
- }
1207
-
1208
- return {
1209
- present: true,
1210
- signalStatus: signal.status || null,
1211
- implementerStatus: envelope && envelope.status ? envelope.status : fallback.implementerStatus || null,
1212
- summary: envelope && envelope.summary ? envelope.summary : fallback.summary || null,
1213
- changedFiles:
1214
- envelope && Array.isArray(envelope.changedFiles)
1215
- ? envelope.changedFiles
1216
- : Array.isArray(fallback.changedFiles)
1217
- ? fallback.changedFiles
1218
- : [],
1219
- testEvidence:
1220
- envelope && Array.isArray(envelope.testEvidence)
1221
- ? envelope.testEvidence
1222
- : Array.isArray(fallback.testEvidence)
1223
- ? fallback.testEvidence
1224
- : [],
1225
- concerns: summarizeSignalIssues(signal, envelope && envelope.concerns),
1226
- blockers: summarizeSignalIssues(signal, envelope && envelope.blockers),
1227
- outOfScopeWrites:
1228
- signal.details && Array.isArray(signal.details.outOfScopeWrites)
1229
- ? dedupeMessages(signal.details.outOfScopeWrites.map((item) => String(item || "").trim()).filter(Boolean))
1230
- : Array.isArray(fallback.outOfScopeWrites)
1231
- ? fallback.outOfScopeWrites
1232
- : [],
1233
- recordedAt: (envelope && envelope.recordedAt) || signal.timestamp || fallback.recordedAt || null
1234
- };
1235
- }
1236
-
1237
- function buildTaskGroupReviewStageState(taskGroupId, stage, signals, fallbackState) {
1238
- const fallback = fallbackState && typeof fallbackState === "object" ? fallbackState : {};
1239
- const signal = findLatestSignalBySurface(signals, `task-review.${taskGroupId}.${stage}`);
1240
- const envelope = signal && signal.details && signal.details.envelope ? signal.details.envelope : null;
1241
- if (!signal) {
1242
- return {
1243
- present: false,
1244
- status: fallback.status || "missing",
1245
- summary: fallback.summary || null,
1246
- reviewer: fallback.reviewer || null,
1247
- issues: Array.isArray(fallback.issues) ? fallback.issues : [],
1248
- recordedAt: fallback.recordedAt || null
1249
- };
1250
- }
1251
-
1252
- return {
1253
- present: true,
1254
- status: signal.status || "missing",
1255
- summary: envelope && envelope.summary ? envelope.summary : fallback.summary || null,
1256
- reviewer: envelope && envelope.reviewer ? envelope.reviewer : fallback.reviewer || null,
1257
- issues: summarizeSignalIssues(signal, envelope && envelope.issues),
1258
- recordedAt: (envelope && envelope.recordedAt) || signal.timestamp || fallback.recordedAt || null
1259
- };
1260
- }
1261
-
1262
- function buildTaskGroupReviewState(taskGroup, signals, fallbackState) {
1263
- const fallback = fallbackState && typeof fallbackState === "object" ? fallbackState : {};
1264
- const taskGroupId = normalizeTaskGroupId(taskGroup.taskGroupId || taskGroup.id);
1265
- const required = taskGroup.reviewIntent === true;
1266
- const spec = buildTaskGroupReviewStageState(taskGroupId, "spec", signals, fallback.spec);
1267
- const quality = buildTaskGroupReviewStageState(taskGroupId, "quality", signals, fallback.quality);
1268
-
1269
- return {
1270
- required,
1271
- ordering: required ? "spec_then_quality" : "none",
1272
- spec,
1273
- quality
1274
- };
1275
- }
1276
-
1277
- function buildEffectiveTaskGroupState(group, planned, implementer, review) {
1278
- const fallbackCursor = normalizeResumeCursor(planned.resumeCursor, null);
1279
- const effective = {
1280
- status: planned.status,
1281
- nextAction: planned.nextAction,
1282
- resumeCursor: fallbackCursor,
1283
- source: "planned",
1284
- reason: "planned_checklist"
1285
- };
1286
-
1287
- if (implementer.present && implementer.outOfScopeWrites.length > 0) {
1288
- return {
1289
- status: "blocked",
1290
- nextAction:
1291
- `resolve out-of-scope writes for task group ${group.taskGroupId}: ${implementer.outOfScopeWrites.join(", ")}`,
1292
- resumeCursor: {
1293
- groupIndex: fallbackCursor.groupIndex,
1294
- nextUncheckedItem: null,
1295
- liveFocus: "out_of_scope_write"
1296
- },
1297
- source: "implementer",
1298
- reason: "out_of_scope_write"
1299
- };
1300
- }
1301
-
1302
- if (implementer.present && implementer.signalStatus === STATUS.BLOCK) {
1303
- return {
1304
- status: "blocked",
1305
- nextAction:
1306
- implementer.blockers[0] ||
1307
- implementer.summary ||
1308
- `resolve implementer blocker for task group ${group.taskGroupId}`,
1309
- resumeCursor: {
1310
- groupIndex: fallbackCursor.groupIndex,
1311
- nextUncheckedItem: null,
1312
- liveFocus: "implementer_block"
1313
- },
1314
- source: "implementer",
1315
- reason: "implementer_block"
1316
- };
1317
- }
1318
-
1319
- const reviewSignalsPresent = review.spec.present || review.quality.present;
1320
- const plannedCompletion = Number.isFinite(Number(planned.completion))
1321
- ? Number(planned.completion)
1322
- : 0;
1323
- const reviewNearComplete = planned.status !== "completed" && plannedCompletion >= 75;
1324
- const reviewHardDue = planned.status === "completed" || reviewNearComplete;
1325
- const reviewContextReady = review.required && (reviewSignalsPresent || reviewHardDue || implementer.present);
1326
-
1327
- if (reviewContextReady) {
1328
- if (
1329
- review.quality.present &&
1330
- (!review.spec.present || review.spec.status === "missing" || review.spec.status === STATUS.WARN)
1331
- ) {
1332
- return {
1333
- status: "blocked",
1334
- nextAction:
1335
- `remove or rerun out-of-order quality review for task group ${group.taskGroupId} after spec review PASS`,
1336
- resumeCursor: {
1337
- groupIndex: fallbackCursor.groupIndex,
1338
- nextUncheckedItem: null,
1339
- liveFocus: "review_ordering_violation"
1340
- },
1341
- source: "review",
1342
- reason: "review_ordering_violation"
1343
- };
1344
- }
1345
- if (review.spec.status === STATUS.BLOCK) {
1346
- return {
1347
- status: "blocked",
1348
- nextAction:
1349
- review.spec.issues[0] || review.spec.summary || `resolve spec review BLOCK for task group ${group.taskGroupId}`,
1350
- resumeCursor: {
1351
- groupIndex: fallbackCursor.groupIndex,
1352
- nextUncheckedItem: null,
1353
- liveFocus: "spec_review_block"
1354
- },
1355
- source: "review",
1356
- reason: "spec_review_block"
1357
- };
1358
- }
1359
- if (!review.spec.present || review.spec.status === "missing") {
1360
- if (reviewHardDue || reviewSignalsPresent) {
1361
- return {
1362
- status: "review_pending",
1363
- nextAction: `record spec review PASS or WARN for task group ${group.taskGroupId} before quality review`,
1364
- resumeCursor: {
1365
- groupIndex: fallbackCursor.groupIndex,
1366
- nextUncheckedItem: null,
1367
- liveFocus: "spec_review_missing"
1368
- },
1369
- source: "review",
1370
- reason: "spec_review_missing"
1371
- };
1372
- }
1373
- } else {
1374
- const specWarn = review.spec.status === STATUS.WARN;
1375
- if (review.quality.status === STATUS.BLOCK) {
1376
- return {
1377
- status: "blocked",
1378
- nextAction:
1379
- review.quality.issues[0] ||
1380
- review.quality.summary ||
1381
- `resolve quality review BLOCK for task group ${group.taskGroupId}`,
1382
- resumeCursor: {
1383
- groupIndex: fallbackCursor.groupIndex,
1384
- nextUncheckedItem: null,
1385
- liveFocus: "quality_review_block"
1386
- },
1387
- source: "review",
1388
- reason: "quality_review_block"
1389
- };
1390
- }
1391
- if (!review.quality.present || review.quality.status === "missing") {
1392
- if (reviewHardDue) {
1393
- return {
1394
- status: "review_pending",
1395
- nextAction: `record quality review PASS or WARN for task group ${group.taskGroupId}`,
1396
- resumeCursor: {
1397
- groupIndex: fallbackCursor.groupIndex,
1398
- nextUncheckedItem: null,
1399
- liveFocus: "quality_review_missing"
1400
- },
1401
- source: "review",
1402
- reason: "quality_review_missing"
1403
- };
1404
- }
1405
- if (specWarn) {
1406
- return {
1407
- status: "in_progress",
1408
- nextAction:
1409
- review.spec.issues[0] ||
1410
- review.spec.summary ||
1411
- `resolve spec review follow-up for task group ${group.taskGroupId}`,
1412
- resumeCursor: {
1413
- groupIndex: fallbackCursor.groupIndex,
1414
- nextUncheckedItem: null,
1415
- liveFocus: "spec_review_warn"
1416
- },
1417
- source: "review",
1418
- reason: "spec_review_warn"
1419
- };
1420
- }
1421
- } else if (review.quality.status === STATUS.WARN) {
1422
- return {
1423
- status: "in_progress",
1424
- nextAction:
1425
- review.quality.issues[0] ||
1426
- review.quality.summary ||
1427
- `resolve quality review follow-up for task group ${group.taskGroupId}`,
1428
- resumeCursor: {
1429
- groupIndex: fallbackCursor.groupIndex,
1430
- nextUncheckedItem: null,
1431
- liveFocus: "quality_review_warn"
1432
- },
1433
- source: "review",
1434
- reason: "quality_review_warn"
1435
- };
1436
- } else if (specWarn) {
1437
- return {
1438
- status: "in_progress",
1439
- nextAction:
1440
- review.spec.issues[0] ||
1441
- review.spec.summary ||
1442
- `resolve spec review follow-up for task group ${group.taskGroupId}`,
1443
- resumeCursor: {
1444
- groupIndex: fallbackCursor.groupIndex,
1445
- nextUncheckedItem: null,
1446
- liveFocus: "spec_review_warn"
1447
- },
1448
- source: "review",
1449
- reason: "spec_review_warn"
1450
- };
1451
- }
1452
- }
234
+ /**
235
+ * Collapse findings into the top-level workflow status token.
236
+ *
237
+ * @param {{ blockers: string[], warnings: string[] }} findings
238
+ * @returns {string}
239
+ */
240
+ function statusFromFindings(findings) {
241
+ if (findings.blockers.length > 0) {
242
+ return STATUS.BLOCK;
1453
243
  }
1454
-
1455
- if (implementer.present && implementer.signalStatus === STATUS.WARN) {
1456
- return {
1457
- status: "in_progress",
1458
- nextAction:
1459
- implementer.concerns[0] ||
1460
- implementer.summary ||
1461
- `resolve implementer concerns for task group ${group.taskGroupId}`,
1462
- resumeCursor: {
1463
- groupIndex: fallbackCursor.groupIndex,
1464
- nextUncheckedItem: null,
1465
- liveFocus: "implementer_warn"
1466
- },
1467
- source: "implementer",
1468
- reason: "implementer_warn"
1469
- };
244
+ if (findings.warnings.length > 0) {
245
+ return STATUS.WARN;
1470
246
  }
1471
-
1472
- return effective;
247
+ return STATUS.PASS;
1473
248
  }
1474
249
 
1475
- function deriveTaskGroupRuntimeState(plannedTaskGroups, signals, seedTaskGroups) {
1476
- const plannedGroups = Array.isArray(plannedTaskGroups) ? plannedTaskGroups : [];
1477
- const seedMap = normalizeTaskGroupSeedMap(seedTaskGroups);
1478
-
1479
- return plannedGroups.map((plannedGroup, index) => {
1480
- const taskGroupId = normalizeTaskGroupId(plannedGroup.taskGroupId || plannedGroup.id);
1481
- const seed = seedMap.get(taskGroupId) || {};
1482
- const planned = {
1483
- status: plannedGroup.status,
1484
- completion: plannedGroup.completion,
1485
- checkpointOutcome: plannedGroup.checkpointOutcome,
1486
- evidence: Array.isArray(plannedGroup.evidence) ? plannedGroup.evidence : [],
1487
- nextAction: plannedGroup.nextAction,
1488
- resumeCursor: normalizeResumeCursor(plannedGroup.resumeCursor, index)
1489
- };
1490
- const implementer = buildTaskGroupImplementerState(taskGroupId, signals, seed.implementer);
1491
- const review = buildTaskGroupReviewState(plannedGroup, signals, seed.review);
1492
- const effective = buildEffectiveTaskGroupState(plannedGroup, planned, implementer, review);
1493
-
1494
- return {
1495
- taskGroupId,
1496
- title: plannedGroup.title,
1497
- status: effective.status,
1498
- completion: planned.completion,
1499
- checkpointOutcome: planned.checkpointOutcome,
1500
- evidence: planned.evidence,
1501
- nextAction: effective.nextAction,
1502
- targetFiles: Array.isArray(plannedGroup.targetFiles) ? plannedGroup.targetFiles : [],
1503
- fileReferences: Array.isArray(plannedGroup.fileReferences) ? plannedGroup.fileReferences : [],
1504
- verificationActions: Array.isArray(plannedGroup.verificationActions)
1505
- ? plannedGroup.verificationActions
1506
- : [],
1507
- verificationCommands: Array.isArray(plannedGroup.verificationCommands)
1508
- ? plannedGroup.verificationCommands
1509
- : [],
1510
- executionIntent: Array.isArray(plannedGroup.executionIntent) ? plannedGroup.executionIntent : [],
1511
- reviewIntent: plannedGroup.reviewIntent === true,
1512
- testingIntent: plannedGroup.testingIntent === true,
1513
- codeChangeLikely: plannedGroup.codeChangeLikely === true,
1514
- resumeCursor: effective.resumeCursor,
1515
- planned,
1516
- implementer,
1517
- review,
1518
- effective
1519
- };
250
+ /**
251
+ * Convert an overlay result into the stable workflow-status result schema.
252
+ *
253
+ * @param {{
254
+ * projectRoot: string,
255
+ * changeId: string | null,
256
+ * overlay: {
257
+ * stageId: string,
258
+ * findings: { blockers: string[], warnings: string[], notes: string[] },
259
+ * gates: Object.<string, string>,
260
+ * taskGroups: Array<object>,
261
+ * executionProfile?: object,
262
+ * worktreePreflight?: object | null,
263
+ * blockingGate?: object | null,
264
+ * needsRerunSurfaces?: string[],
265
+ * stalePlanningSignals?: Object.<string, object>
266
+ * },
267
+ * checkpoints?: Object.<string, string>,
268
+ * audits?: { integrity: object | null, completion: object | null },
269
+ * routeContext?: object,
270
+ * source?: string,
271
+ * discipline?: object | null,
272
+ * verificationFreshness?: object | null
273
+ * }} params
274
+ * @returns {object}
275
+ */
276
+ function buildWorkflowResultFromOverlay(params) {
277
+ return buildWorkflowResult({
278
+ projectRoot: params.projectRoot,
279
+ changeId: params.changeId,
280
+ stageId: params.overlay.stageId,
281
+ findings: params.overlay.findings,
282
+ checkpoints: params.checkpoints || {},
283
+ gates: params.overlay.gates,
284
+ audits: params.audits || {
285
+ integrity: null,
286
+ completion: null
287
+ },
288
+ routeContext: params.routeContext,
289
+ source: params.source || "derived",
290
+ taskGroups: params.overlay.taskGroups,
291
+ discipline: params.discipline || null,
292
+ executionProfile: params.overlay.executionProfile || null,
293
+ worktreePreflight: params.overlay.worktreePreflight || null,
294
+ verificationFreshness: params.verificationFreshness || null,
295
+ blockingGate: params.overlay.blockingGate || null,
296
+ needsRerunSurfaces: params.overlay.needsRerunSurfaces,
297
+ stalePlanningSignals: params.overlay.stalePlanningSignals
1520
298
  });
1521
299
  }
1522
300
 
1523
- function buildTaskGroupMetadataPayload(changeId, checkpointStatuses, taskGroups) {
1524
- return {
1525
- version: TASK_GROUP_METADATA_VERSION,
1526
- changeId,
1527
- checkpointOutcome: checkpointStatuses[CHECKPOINT_LABELS.TASK] || STATUS.WARN,
1528
- taskGroups: Array.isArray(taskGroups) ? taskGroups : [],
1529
- updatedAt: new Date().toISOString()
1530
- };
1531
- }
1532
-
1533
- function loadTaskGroupMetadataFromPath(targetPath) {
1534
- if (!targetPath || !pathExists(targetPath)) {
1535
- return null;
1536
- }
1537
- try {
1538
- return JSON.parse(readTextIfExists(targetPath));
1539
- } catch (_error) {
1540
- return null;
1541
- }
1542
- }
1543
-
1544
- function resolvePersistedTaskGroupSeed(projectRoot, changeId, persistedRecord, plannedTaskGroups) {
1545
- const metadataRefs =
1546
- persistedRecord && persistedRecord.metadataRefs && typeof persistedRecord.metadataRefs === "object"
1547
- ? persistedRecord.metadataRefs
1548
- : {};
1549
- const canonicalPath =
1550
- metadataRefs.taskGroupsPath || resolveTaskGroupMetadataPath(projectRoot, changeId);
1551
- const notes = [];
1552
-
1553
- if (canonicalPath && pathExists(canonicalPath)) {
1554
- const actualDigest = digestForPath(canonicalPath);
1555
- const expectedDigest = metadataRefs.taskGroupsDigest || null;
1556
- if (expectedDigest && actualDigest && expectedDigest !== actualDigest) {
1557
- notes.push("Canonical task-group runtime state digest mismatch; rebuilding task-group state from artifacts.");
1558
- return {
1559
- taskGroups: plannedTaskGroups,
1560
- notes
1561
- };
1562
- }
1563
-
1564
- const loaded = canonicalPath === resolveTaskGroupMetadataPath(projectRoot, changeId)
1565
- ? readTaskGroupMetadata(projectRoot, changeId)
1566
- : loadTaskGroupMetadataFromPath(canonicalPath);
1567
- if (loaded && Array.isArray(loaded.taskGroups)) {
1568
- return {
1569
- taskGroups: loaded.taskGroups,
1570
- notes
1571
- };
1572
- }
1573
- notes.push("Canonical task-group runtime state is unreadable; rebuilding task-group state from artifacts.");
1574
- return {
1575
- taskGroups: plannedTaskGroups,
1576
- notes
1577
- };
1578
- }
1579
-
1580
- if (Array.isArray(persistedRecord && persistedRecord.taskGroups) && persistedRecord.taskGroups.length > 0) {
1581
- notes.push("Using legacy embedded task-group state as migration fallback.");
1582
- return {
1583
- taskGroups: persistedRecord.taskGroups,
1584
- notes
1585
- };
1586
- }
1587
-
1588
- notes.push("Canonical task-group runtime state is missing; rebuilding task-group state from artifacts.");
1589
- return {
1590
- taskGroups: plannedTaskGroups,
1591
- notes
1592
- };
1593
- }
1594
-
1595
- function buildGatesWithLiveOverlays(baseGates, completionAudit, disciplineState, verificationFreshness) {
1596
- const gates = baseGates && typeof baseGates === "object" ? { ...baseGates } : {};
1597
- if (completionAudit) {
1598
- gates[HANDOFF_GATES.VERIFY_TO_COMPLETE] =
1599
- completionAudit.status === "PASS" ? STATUS.PASS : STATUS.WARN;
1600
- }
1601
- if (disciplineState && disciplineState.blockers.length > 0) {
1602
- gates[HANDOFF_GATES.TASKS_TO_BUILD] = STATUS.BLOCK;
1603
- }
1604
- if (verificationFreshness && !verificationFreshness.fresh) {
1605
- gates[HANDOFF_GATES.VERIFY_TO_COMPLETE] = STATUS.BLOCK;
1606
- }
1607
- return gates;
1608
- }
1609
-
1610
- function finalizeWorkflowView(options = {}) {
1611
- const findings = {
1612
- blockers: Array.isArray(options.findings && options.findings.blockers)
1613
- ? options.findings.blockers.slice()
1614
- : [],
1615
- warnings: Array.isArray(options.findings && options.findings.warnings)
1616
- ? options.findings.warnings.slice()
1617
- : [],
1618
- notes: Array.isArray(options.findings && options.findings.notes)
1619
- ? options.findings.notes.slice()
1620
- : []
1621
- };
1622
- let stageId = options.stageId;
1623
- const integrityAudit = options.integrityAudit || null;
1624
- const completionAudit = options.completionAudit || null;
1625
- const disciplineState = options.disciplineState || null;
1626
- const verificationFreshness = options.verificationFreshness || null;
1627
- const planningSignalFreshness =
1628
- options.planningSignalFreshness && typeof options.planningSignalFreshness === "object"
1629
- ? options.planningSignalFreshness
1630
- : {
1631
- effectiveSignalSummary: options.signalSummary || {},
1632
- stalePlanningSignals: {},
1633
- needsRerunSurfaces: []
1634
- };
301
+ /**
302
+ * Shared base-view pipeline:
303
+ * 1. derive task-group runtime state from the chosen base seed,
304
+ * 2. apply workflow overlays,
305
+ * 3. normalize the final workflow result shape.
306
+ *
307
+ * @param {{
308
+ * projectRoot: string,
309
+ * changeId: string | null,
310
+ * baseView: {
311
+ * source: string,
312
+ * stageId: string,
313
+ * findings: { blockers: string[], warnings: string[], notes: string[] },
314
+ * baseGates: Object.<string, string>,
315
+ * completionAudit: object | null,
316
+ * taskGroupSeed: Array<object>
317
+ * },
318
+ * plannedTaskGroups: Array<object>,
319
+ * changeSignals: Array<object>,
320
+ * signalSummary: Object.<string, object>,
321
+ * planningSignalFreshness: object,
322
+ * integrityAudit: object | null,
323
+ * disciplineState: object | null,
324
+ * verificationFreshness: object | null,
325
+ * hasTasksArtifact: boolean,
326
+ * decisionTraceRecords: Array<object> | null,
327
+ * checkpoints: Object.<string, string>,
328
+ * routeContext: object
329
+ * }} [options]
330
+ * @returns {object}
331
+ */
332
+ function buildWorkflowResultForBaseView(options = {}) {
1635
333
  const taskGroups = deriveTaskGroupRuntimeState(
1636
334
  options.plannedTaskGroups,
1637
335
  options.changeSignals,
1638
- options.taskGroupSeed
1639
- );
1640
-
1641
- stageId = applyAuditFindings(stageId, findings, integrityAudit, completionAudit);
1642
- stageId = applyPlanningSignalFreshnessFindings(stageId, findings, planningSignalFreshness);
1643
- stageId = applyExecutionSignalFindings(stageId, findings, planningSignalFreshness.effectiveSignalSummary || {});
1644
- applyTaskExecutionAndReviewFindings(findings, options.changeSignals || []);
1645
-
1646
- if (disciplineState) {
1647
- findings.blockers.push(...disciplineState.blockers);
1648
- findings.warnings.push(...disciplineState.warnings);
1649
- findings.notes.push(...disciplineState.notes);
1650
- if (disciplineState.blockers.length > 0 && ["build", "verify", "complete"].includes(stageId)) {
1651
- stageId = options.hasTasksArtifact ? "tasks" : "design";
1652
- }
1653
- }
1654
-
1655
- if (verificationFreshness && !verificationFreshness.fresh && (stageId === "verify" || stageId === "complete")) {
1656
- findings.blockers.push(
1657
- "Completion-facing routing requires fresh verification evidence; stale evidence keeps the route in verify."
1658
- );
1659
- stageId = "verify";
1660
- }
1661
-
1662
- const gates = buildGatesWithLiveOverlays(
1663
- options.baseGates,
1664
- completionAudit,
1665
- disciplineState,
1666
- verificationFreshness
336
+ options.baseView.taskGroupSeed,
337
+ options.decisionTraceRecords,
338
+ recordWorkflowDecision
1667
339
  );
1668
- const executionProfile = deriveExecutionProfile({
1669
- stage: stageId,
1670
- taskGroups
340
+ const overlay = applyWorkflowOverlays({
341
+ projectRoot: options.projectRoot,
342
+ changeId: options.changeId,
343
+ stageId: options.baseView.stageId,
344
+ findings: options.baseView.findings,
345
+ baseGates: options.baseView.baseGates,
346
+ taskGroups,
347
+ changeSignals: options.changeSignals,
348
+ signalSummary: options.signalSummary,
349
+ planningSignalFreshness: options.planningSignalFreshness,
350
+ integrityAudit: options.integrityAudit,
351
+ completionAudit: options.baseView.completionAudit,
352
+ disciplineState: options.disciplineState,
353
+ verificationFreshness: options.verificationFreshness,
354
+ hasTasksArtifact: options.hasTasksArtifact,
355
+ decisionTraceRecords: options.decisionTraceRecords,
356
+ recordWorkflowDecision
1671
357
  });
1672
- let worktreePreflight = null;
1673
- if (options.changeId && (stageId === "build" || stageId === "verify")) {
1674
- worktreePreflight = runWorktreePreflight(options.projectRoot, {
1675
- parallelPreferred: executionProfile.mode === "bounded_parallel"
1676
- });
1677
- if (
1678
- executionProfile.mode === "bounded_parallel" &&
1679
- worktreePreflight.summary &&
1680
- worktreePreflight.summary.recommendedIsolation
1681
- ) {
1682
- executionProfile.effectiveMode = "serial";
1683
- executionProfile.rationale = dedupeMessages([
1684
- ...(executionProfile.rationale || []),
1685
- "worktree preflight recommends isolation; effective mode downgraded to serial"
1686
- ]);
1687
- findings.warnings.push(
1688
- "Bounded-parallel profile downgraded to serial until worktree isolation is ready or explicitly accepted."
1689
- );
1690
- }
1691
- }
1692
-
1693
- dedupeFindings(findings);
1694
- const blockingGate = selectBlockingGateIdentity(findings);
1695
358
 
1696
- return buildWorkflowResult({
359
+ return buildWorkflowResultFromOverlay({
1697
360
  projectRoot: options.projectRoot,
1698
361
  changeId: options.changeId,
1699
- stageId,
1700
- findings,
1701
- checkpoints: options.checkpoints || {},
1702
- gates,
362
+ checkpoints: options.checkpoints,
363
+ routeContext: options.routeContext,
364
+ source: options.baseView.source,
365
+ overlay,
1703
366
  audits: {
1704
- integrity: integrityAudit,
1705
- completion: completionAudit
367
+ integrity: options.integrityAudit,
368
+ completion: options.baseView.completionAudit
1706
369
  },
1707
- routeContext: options.routeContext,
1708
- source: options.source || "derived",
1709
- taskGroups,
1710
- discipline: disciplineState,
1711
- executionProfile,
1712
- worktreePreflight,
1713
- verificationFreshness,
1714
- blockingGate,
1715
- needsRerunSurfaces: planningSignalFreshness.needsRerunSurfaces,
1716
- stalePlanningSignals: planningSignalFreshness.stalePlanningSignals
370
+ discipline: options.disciplineState,
371
+ verificationFreshness: options.verificationFreshness
1717
372
  });
1718
373
  }
1719
374
 
1720
- function selectFocusedTaskGroup(taskGroups) {
1721
- const priority = {
1722
- blocked: 0,
1723
- review_pending: 1,
1724
- in_progress: 2,
1725
- pending: 3,
1726
- completed: 4
1727
- };
1728
- const groups = Array.isArray(taskGroups) ? taskGroups : [];
1729
- return groups
1730
- .slice()
1731
- .sort((left, right) => {
1732
- const leftRank = Object.prototype.hasOwnProperty.call(priority, left.status) ? priority[left.status] : 5;
1733
- const rightRank = Object.prototype.hasOwnProperty.call(priority, right.status) ? priority[right.status] : 5;
1734
- if (leftRank !== rightRank) {
1735
- return leftRank - rightRank;
1736
- }
1737
- const leftIndex =
1738
- left && left.resumeCursor && Number.isInteger(left.resumeCursor.groupIndex)
1739
- ? left.resumeCursor.groupIndex
1740
- : Number.MAX_SAFE_INTEGER;
1741
- const rightIndex =
1742
- right && right.resumeCursor && Number.isInteger(right.resumeCursor.groupIndex)
1743
- ? right.resumeCursor.groupIndex
1744
- : Number.MAX_SAFE_INTEGER;
1745
- return leftIndex - rightIndex;
1746
- })[0] || null;
1747
- }
1748
-
1749
- function statusFromFindings(findings) {
1750
- if (findings.blockers.length > 0) {
1751
- return STATUS.BLOCK;
1752
- }
1753
- if (findings.warnings.length > 0) {
1754
- return STATUS.WARN;
1755
- }
1756
- return STATUS.PASS;
1757
- }
1758
-
375
+ /**
376
+ * Main workflow-status orchestrator.
377
+ *
378
+ * Responsibilities:
379
+ * 1. resolve the active project/change context,
380
+ * 2. gather artifact, signal, audit, and freshness inputs,
381
+ * 3. choose a trusted persisted or artifact-derived base view,
382
+ * 4. apply runtime overlays,
383
+ * 5. persist refreshed derived snapshots when appropriate.
384
+ *
385
+ * @param {string} projectPathInput
386
+ * @param {{
387
+ * changeId?: string,
388
+ * staleWindowMs?: number,
389
+ * env?: object,
390
+ * traceSurface?: string
391
+ * }} [options]
392
+ * @returns {object}
393
+ */
1759
394
  function deriveWorkflowStatus(projectPathInput, options = {}) {
1760
395
  const projectRoot = path.resolve(projectPathInput || process.cwd());
1761
396
  const requestedChangeId = options.changeId ? String(options.changeId).trim() : "";
@@ -1767,23 +402,30 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
1767
402
 
1768
403
  if (!pathExists(projectRoot)) {
1769
404
  findings.blockers.push(`Project path does not exist: ${projectRoot}`);
1770
- return buildWorkflowResult({
1771
- projectRoot,
1772
- changeId: null,
1773
- stageId: "bootstrap",
1774
- findings,
1775
- checkpoints: {},
1776
- gates: {},
1777
- audits: {
1778
- integrity: null,
1779
- completion: null
1780
- },
1781
- routeContext: {
405
+ return finalizeResultWithWorkflowDecisionTracing(
406
+ buildWorkflowResult({
1782
407
  projectRoot,
1783
- changeId: requestedChangeId || "change-001",
1784
- ambiguousChangeSelection: false
408
+ changeId: null,
409
+ stageId: "bootstrap",
410
+ findings,
411
+ checkpoints: {},
412
+ gates: {},
413
+ audits: {
414
+ integrity: null,
415
+ completion: null
416
+ },
417
+ routeContext: {
418
+ projectRoot,
419
+ changeId: requestedChangeId || "change-001",
420
+ ambiguousChangeSelection: false
421
+ }
422
+ }),
423
+ {
424
+ env: options.env,
425
+ surface: options.traceSurface,
426
+ records: null
1785
427
  }
1786
- });
428
+ );
1787
429
  }
1788
430
 
1789
431
  const workflowRoot = path.join(projectRoot, ".da-vinci");
@@ -1820,6 +462,13 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
1820
462
  }
1821
463
 
1822
464
  const activeChangeId = activeChangeDir ? path.basename(activeChangeDir) : null;
465
+ const decisionTraceRecords =
466
+ shouldTraceWorkflowDecisions({
467
+ env: options.env,
468
+ surface: options.traceSurface
469
+ }) && !ambiguousChangeSelection
470
+ ? []
471
+ : null;
1823
472
  const artifactState = {
1824
473
  workflowRootReady: pathExists(workflowRoot),
1825
474
  changeSelected: Boolean(activeChangeDir),
@@ -1858,15 +507,6 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
1858
507
  }
1859
508
  : null;
1860
509
  const integrityAudit = collectIntegrityAudit(projectRoot, workflowRoot, routingSignalSummary);
1861
- const designCheckpointStatus = normalizeCheckpointStatus(checkpointStatuses[CHECKPOINT_LABELS.DESIGN]);
1862
- const designSourceCheckpointStatus = normalizeCheckpointStatus(
1863
- checkpointStatuses[CHECKPOINT_LABELS.DESIGN_SOURCE]
1864
- );
1865
- const runtimeGateCheckpointStatus = normalizeCheckpointStatus(
1866
- checkpointStatuses[CHECKPOINT_LABELS.RUNTIME_GATE]
1867
- );
1868
- const mappingCheckpointStatus = normalizeCheckpointStatus(checkpointStatuses[CHECKPOINT_LABELS.MAPPING]);
1869
- const taskCheckpointStatus = normalizeCheckpointStatus(checkpointStatuses[CHECKPOINT_LABELS.TASK]);
1870
510
  const verificationFreshness = activeChangeId
1871
511
  ? collectVerificationFreshness(projectRoot, {
1872
512
  changeId: activeChangeId,
@@ -1880,27 +520,44 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
1880
520
  ambiguousChangeSelection
1881
521
  };
1882
522
 
523
+ // Persisted snapshots can be reused only when the fingerprint still matches
524
+ // current artifact/signal truth; otherwise we fall back to the derived path.
1883
525
  if (activeChangeId) {
1884
526
  const persistedSelection = selectPersistedStateForChange(projectRoot, activeChangeId, {
1885
527
  staleWindowMs: options.staleWindowMs
1886
528
  });
1887
529
  if (persistedSelection.usable && persistedSelection.changeRecord) {
530
+ const advisoryAgeAccepted =
531
+ Array.isArray(persistedSelection.advisoryNotes) && persistedSelection.advisoryNotes.length > 0;
532
+ recordWorkflowDecision(decisionTraceRecords, {
533
+ decisionFamily: "persisted_state_trust",
534
+ decisionKey: advisoryAgeAccepted ? "accepted_age_advisory" : "accepted_digest_match",
535
+ outcome: "accepted",
536
+ reasonSummary: advisoryAgeAccepted
537
+ ? "Persisted workflow snapshot remains trusted because artifact content digests still match despite advisory age."
538
+ : "Persisted workflow snapshot is trusted because artifact content digests still match.",
539
+ context: {
540
+ statePath: formatPathRef(projectRoot, persistedSelection.statePath),
541
+ persistedVersion:
542
+ persistedSelection.persisted && Number.isFinite(Number(persistedSelection.persisted.version))
543
+ ? Number(persistedSelection.persisted.version)
544
+ : null,
545
+ advisoryAge: advisoryAgeAccepted,
546
+ fingerprintMatched: true
547
+ },
548
+ evidenceRefs: [`state:${formatPathRef(projectRoot, persistedSelection.statePath)}`]
549
+ });
1888
550
  const persistedRecord = persistedSelection.changeRecord;
1889
- const stageRecord = getStageById(persistedRecord.stage) || getStageById("bootstrap");
1890
- const completionAudit =
1891
- stageRecord.id === "verify" || stageRecord.id === "complete"
1892
- ? collectCompletionAudit(projectRoot, workflowRoot, activeChangeId, routingSignalSummary)
1893
- : null;
1894
551
  const persistedSeed = resolvePersistedTaskGroupSeed(
1895
552
  projectRoot,
1896
553
  activeChangeId,
1897
554
  persistedRecord,
1898
- plannedTaskGroups
555
+ plannedTaskGroups,
556
+ decisionTraceRecords,
557
+ recordWorkflowDecision
1899
558
  );
1900
- return finalizeWorkflowView({
1901
- projectRoot,
1902
- changeId: activeChangeId,
1903
- stageId: stageRecord.id,
559
+ const persistedBaseView = buildPersistedWorkflowBaseView({
560
+ persistedRecord,
1904
561
  findings: {
1905
562
  blockers: Array.isArray(persistedRecord.failures) ? persistedRecord.failures.slice() : [],
1906
563
  warnings: Array.isArray(persistedRecord.warnings) ? persistedRecord.warnings.slice() : [],
@@ -1911,24 +568,36 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
1911
568
  "workflow-status is using trusted persisted workflow state."
1912
569
  ]
1913
570
  },
1914
- baseGates:
1915
- persistedRecord && persistedRecord.gates && typeof persistedRecord.gates === "object"
1916
- ? { ...persistedRecord.gates }
1917
- : {},
1918
- checkpoints: checkpointStatuses,
1919
- routeContext,
1920
- source: "persisted",
1921
571
  taskGroupSeed: persistedSeed.taskGroups,
1922
- plannedTaskGroups,
1923
- changeSignals,
1924
- signalSummary,
1925
- planningSignalFreshness,
1926
- integrityAudit,
1927
- completionAudit,
1928
- disciplineState,
1929
- verificationFreshness,
1930
- hasTasksArtifact: artifactState.tasks
572
+ resolveCompletionAudit(stageId) {
573
+ return stageId === "verify" || stageId === "complete"
574
+ ? collectCompletionAudit(projectRoot, workflowRoot, activeChangeId, routingSignalSummary)
575
+ : null;
576
+ }
1931
577
  });
578
+ return finalizeResultWithWorkflowDecisionTracing(
579
+ buildWorkflowResultForBaseView({
580
+ projectRoot,
581
+ changeId: activeChangeId,
582
+ baseView: persistedBaseView,
583
+ plannedTaskGroups,
584
+ changeSignals,
585
+ signalSummary,
586
+ planningSignalFreshness,
587
+ integrityAudit,
588
+ disciplineState,
589
+ verificationFreshness,
590
+ hasTasksArtifact: artifactState.tasks,
591
+ decisionTraceRecords,
592
+ checkpoints: checkpointStatuses,
593
+ routeContext
594
+ }),
595
+ {
596
+ env: options.env,
597
+ surface: options.traceSurface,
598
+ records: decisionTraceRecords
599
+ }
600
+ );
1932
601
  }
1933
602
 
1934
603
  if (!persistedSelection.usable && persistedSelection.reason) {
@@ -1942,6 +611,22 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
1942
611
  const message = reasonMessage[persistedSelection.reason];
1943
612
  if (message) {
1944
613
  findings.notes.push(message);
614
+ recordWorkflowDecision(decisionTraceRecords, {
615
+ decisionFamily: "persisted_state_trust",
616
+ decisionKey: PERSISTED_STATE_TRACE_KEYS[persistedSelection.reason],
617
+ outcome: "fallback",
618
+ reasonSummary: message,
619
+ context: {
620
+ statePath: formatPathRef(projectRoot, persistedSelection.statePath),
621
+ persistedVersion:
622
+ persistedSelection.persisted && Number.isFinite(Number(persistedSelection.persisted.version))
623
+ ? Number(persistedSelection.persisted.version)
624
+ : null,
625
+ fingerprintMatched:
626
+ persistedSelection.reason === "fingerprint-mismatch" ? false : null
627
+ },
628
+ evidenceRefs: [`state:${formatPathRef(projectRoot, persistedSelection.statePath)}`]
629
+ });
1945
630
  }
1946
631
  }
1947
632
  }
@@ -1951,55 +636,36 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
1951
636
  warnings: findings.warnings.slice(),
1952
637
  notes: findings.notes.slice()
1953
638
  };
1954
- const stageId = deriveStageFromArtifacts(artifactState, checkpointStatuses, baseFindings);
1955
- const completionAudit =
1956
- stageId === "verify" || stageId === "complete"
1957
- ? collectCompletionAudit(projectRoot, workflowRoot, activeChangeId, routingSignalSummary)
1958
- : null;
1959
- const baseGates = {
1960
- [HANDOFF_GATES.BREAKDOWN_TO_DESIGN]:
1961
- artifactState.proposal && artifactState.specFiles.length > 0 ? STATUS.PASS : STATUS.BLOCK,
1962
- [HANDOFF_GATES.DESIGN_TO_TASKS]: mergeStatuses([
1963
- artifactState.design && artifactState.pencilDesign && artifactState.pencilBindings
1964
- ? STATUS.PASS
1965
- : STATUS.BLOCK,
1966
- designCheckpointStatus,
1967
- designSourceCheckpointStatus,
1968
- runtimeGateCheckpointStatus,
1969
- mappingCheckpointStatus
1970
- ]),
1971
- [HANDOFF_GATES.TASKS_TO_BUILD]: mergeStatuses([
1972
- artifactState.tasks ? STATUS.PASS : STATUS.BLOCK,
1973
- designCheckpointStatus,
1974
- designSourceCheckpointStatus,
1975
- runtimeGateCheckpointStatus,
1976
- mappingCheckpointStatus,
1977
- taskCheckpointStatus
1978
- ]),
1979
- [HANDOFF_GATES.BUILD_TO_VERIFY]: artifactState.verification ? STATUS.PASS : STATUS.BLOCK,
1980
- [HANDOFF_GATES.VERIFY_TO_COMPLETE]:
1981
- completionAudit && completionAudit.status === "PASS" ? STATUS.PASS : STATUS.WARN
1982
- };
1983
639
 
1984
- const derivedResult = finalizeWorkflowView({
1985
- projectRoot,
1986
- changeId: activeChangeId,
1987
- stageId,
640
+ // The derived path rebuilds base routing from artifacts, then re-applies
641
+ // live overlays before refreshing canonical task-group metadata and the
642
+ // persisted workflow snapshot.
643
+ const derivedBaseView = buildDerivedWorkflowBaseView({
644
+ artifactState,
645
+ checkpointStatuses,
1988
646
  findings: baseFindings,
1989
- baseGates,
1990
- checkpoints: checkpointStatuses,
1991
- routeContext,
1992
- source: "derived",
1993
647
  taskGroupSeed: plannedTaskGroups,
648
+ resolveCompletionAudit(stageId) {
649
+ return stageId === "verify" || stageId === "complete"
650
+ ? collectCompletionAudit(projectRoot, workflowRoot, activeChangeId, routingSignalSummary)
651
+ : null;
652
+ }
653
+ });
654
+ const derivedResult = buildWorkflowResultForBaseView({
655
+ projectRoot,
656
+ changeId: activeChangeId,
657
+ baseView: derivedBaseView,
1994
658
  plannedTaskGroups,
1995
659
  changeSignals,
1996
660
  signalSummary,
1997
661
  planningSignalFreshness,
1998
662
  integrityAudit,
1999
- completionAudit,
2000
663
  disciplineState,
2001
664
  verificationFreshness,
2002
- hasTasksArtifact: artifactState.tasks
665
+ hasTasksArtifact: artifactState.tasks,
666
+ decisionTraceRecords,
667
+ checkpoints: checkpointStatuses,
668
+ routeContext
2003
669
  });
2004
670
 
2005
671
  if (activeChangeId && !ambiguousChangeSelection) {
@@ -2069,9 +735,37 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
2069
735
  ]);
2070
736
  }
2071
737
 
2072
- return derivedResult;
738
+ return finalizeResultWithWorkflowDecisionTracing(derivedResult, {
739
+ env: options.env,
740
+ surface: options.traceSurface,
741
+ records: decisionTraceRecords
742
+ });
2073
743
  }
2074
744
 
745
+ /**
746
+ * Build the public workflow-status payload.
747
+ *
748
+ * @param {{
749
+ * projectRoot: string,
750
+ * changeId: string | null,
751
+ * stageId: string,
752
+ * findings: { blockers: string[], warnings: string[], notes: string[] },
753
+ * checkpoints: Object.<string, string>,
754
+ * gates: Object.<string, string>,
755
+ * audits: { integrity: object | null, completion: object | null },
756
+ * routeContext: object,
757
+ * source?: string,
758
+ * taskGroups?: Array<object>,
759
+ * discipline?: object | null,
760
+ * executionProfile?: object | null,
761
+ * worktreePreflight?: object | null,
762
+ * verificationFreshness?: object | null,
763
+ * blockingGate?: object | null,
764
+ * needsRerunSurfaces?: string[],
765
+ * stalePlanningSignals?: Object.<string, object>
766
+ * }} params
767
+ * @returns {object}
768
+ */
2075
769
  function buildWorkflowResult(params) {
2076
770
  const stage = getStageById(params.stageId) || getStageById("bootstrap");
2077
771
  const nextStep = buildRouteRecommendation(params.stageId, params.routeContext);
@@ -2107,6 +801,12 @@ function buildWorkflowResult(params) {
2107
801
  };
2108
802
  }
2109
803
 
804
+ /**
805
+ * Render the human-readable `workflow-status` report.
806
+ *
807
+ * @param {object} result
808
+ * @returns {string}
809
+ */
2110
810
  function formatWorkflowStatusReport(result) {
2111
811
  const lines = [
2112
812
  "Da Vinci workflow-status",
@@ -2192,6 +892,12 @@ function formatWorkflowStatusReport(result) {
2192
892
  return lines.join("\n");
2193
893
  }
2194
894
 
895
+ /**
896
+ * Render the human-readable `next-step` report.
897
+ *
898
+ * @param {object} result
899
+ * @returns {string}
900
+ */
2195
901
  function formatNextStepReport(result) {
2196
902
  const lines = [
2197
903
  "Da Vinci next-step",