@xenonbyte/da-vinci-workflow 0.2.6 → 0.2.8

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.
@@ -25,6 +25,7 @@ const {
25
25
  const {
26
26
  selectPersistedStateForChange,
27
27
  persistDerivedWorkflowResult,
28
+ resolveWorkflowStatePath,
28
29
  resolveTaskGroupMetadataPath,
29
30
  writeTaskGroupMetadata,
30
31
  readTaskGroupMetadata,
@@ -40,10 +41,38 @@ const { evaluatePlanningSignalFreshness } = require("./planning-signal-freshness
40
41
  const { deriveExecutionProfile } = require("./execution-profile");
41
42
  const { collectVerificationFreshness } = require("./verify");
42
43
  const { runWorktreePreflight } = require("./worktree-preflight");
44
+ const {
45
+ formatPathRef,
46
+ emitWorkflowDecisionTraces,
47
+ shouldTraceWorkflowDecisions
48
+ } = require("./workflow-decision-trace");
43
49
 
44
50
  const MAX_REPORTED_MESSAGES = 3;
45
51
  // Task-group metadata is versioned independently from workflow route snapshots.
46
52
  const TASK_GROUP_METADATA_VERSION = 2;
53
+ const TRACEABLE_TASK_GROUP_FOCUS_REASONS = new Set([
54
+ "implementer_block",
55
+ "implementer_warn",
56
+ "spec_review_missing",
57
+ "spec_review_block",
58
+ "spec_review_warn",
59
+ "quality_review_missing",
60
+ "quality_review_block",
61
+ "quality_review_warn"
62
+ ]);
63
+ const PERSISTED_STATE_TRACE_KEYS = Object.freeze({
64
+ missing: "fallback_missing",
65
+ "parse-error": "fallback_parse_error",
66
+ "version-mismatch": "fallback_version_mismatch",
67
+ "change-missing": "fallback_change_missing",
68
+ "fingerprint-mismatch": "fallback_fingerprint_mismatch"
69
+ });
70
+ const TASK_GROUP_SEED_TRACE_KEYS = Object.freeze({
71
+ missing: "seed_missing",
72
+ unreadable: "seed_unreadable",
73
+ "digest-mismatch": "seed_digest_mismatch",
74
+ legacy: "seed_legacy_embedded"
75
+ });
47
76
  const BLOCKING_GATE_PRIORITY = Object.freeze([
48
77
  "clarify",
49
78
  "scenarioQuality",
@@ -61,6 +90,99 @@ const PLANNING_SIGNAL_PROMOTION_FALLBACKS = Object.freeze({
61
90
  "lint-tasks": "tasks"
62
91
  });
63
92
 
93
+ function recordWorkflowDecision(records, record) {
94
+ if (!Array.isArray(records) || !record || typeof record !== "object") {
95
+ return;
96
+ }
97
+ records.push(record);
98
+ }
99
+
100
+ function buildTaskGroupFocusEvidenceRefs(taskGroupId, reason) {
101
+ if (reason === "implementer_block" || reason === "implementer_warn") {
102
+ return [`signal:task-execution.${taskGroupId}`];
103
+ }
104
+ if (
105
+ reason === "spec_review_block" ||
106
+ reason === "spec_review_warn"
107
+ ) {
108
+ return [`signal:task-review.${taskGroupId}.spec`];
109
+ }
110
+ if (
111
+ reason === "quality_review_block" ||
112
+ reason === "quality_review_warn"
113
+ ) {
114
+ return [`signal:task-review.${taskGroupId}.quality`];
115
+ }
116
+ return [];
117
+ }
118
+
119
+ function buildTaskGroupFocusReasonSummary(taskGroupId, reason) {
120
+ switch (reason) {
121
+ case "implementer_block":
122
+ return `Implementer BLOCK overrides planned checklist focus for task group ${taskGroupId}.`;
123
+ case "implementer_warn":
124
+ return `Implementer WARN overrides planned checklist focus for task group ${taskGroupId}.`;
125
+ case "spec_review_missing":
126
+ return `Missing spec review takes focus over planned checklist work for task group ${taskGroupId}.`;
127
+ case "spec_review_block":
128
+ return `Spec review BLOCK takes focus over planned checklist work for task group ${taskGroupId}.`;
129
+ case "spec_review_warn":
130
+ return `Spec review WARN follow-up takes focus over planned checklist work for task group ${taskGroupId}.`;
131
+ case "quality_review_missing":
132
+ return `Missing quality review takes focus over planned checklist work for task group ${taskGroupId}.`;
133
+ case "quality_review_block":
134
+ return `Quality review BLOCK takes focus over planned checklist work for task group ${taskGroupId}.`;
135
+ case "quality_review_warn":
136
+ return `Quality review WARN follow-up takes focus over planned checklist work for task group ${taskGroupId}.`;
137
+ default:
138
+ return "";
139
+ }
140
+ }
141
+
142
+ function collectVerificationFreshnessEvidenceRefs(verificationFreshness) {
143
+ const surfaces =
144
+ verificationFreshness && verificationFreshness.surfaces && typeof verificationFreshness.surfaces === "object"
145
+ ? verificationFreshness.surfaces
146
+ : {};
147
+ return Object.keys(surfaces)
148
+ .filter((surface) => surfaces[surface] && surfaces[surface].stale && surfaces[surface].present)
149
+ .sort()
150
+ .map((surface) => `signal:${surface}`);
151
+ }
152
+
153
+ function finalizeResultWithWorkflowDecisionTracing(result, options = {}) {
154
+ const traceResult = emitWorkflowDecisionTraces({
155
+ env: options.env,
156
+ surface: options.surface,
157
+ projectRoot: result && result.projectRoot ? result.projectRoot : options.projectRoot,
158
+ changeId: result ? result.changeId : null,
159
+ stage: result ? result.stage : "",
160
+ records: options.records
161
+ });
162
+ if (
163
+ result &&
164
+ typeof result === "object" &&
165
+ traceResult &&
166
+ traceResult.enabled &&
167
+ (traceResult.rejectedCount > 0 || traceResult.error)
168
+ ) {
169
+ result.traceDiagnostics = {
170
+ enabled: true,
171
+ written: traceResult.written,
172
+ rejectedCount: traceResult.rejectedCount,
173
+ rejections: Array.isArray(traceResult.rejections) ? traceResult.rejections : [],
174
+ tracePath: traceResult.tracePath,
175
+ error:
176
+ traceResult.error && traceResult.error.message
177
+ ? traceResult.error.message
178
+ : traceResult.error
179
+ ? String(traceResult.error)
180
+ : null
181
+ };
182
+ }
183
+ return result;
184
+ }
185
+
64
186
  function summarizeAudit(result) {
65
187
  if (!result) {
66
188
  return null;
@@ -174,7 +296,7 @@ function collectPlanningSignalFreshnessState(projectRoot, changeId, signalSummar
174
296
  };
175
297
  }
176
298
 
177
- function applyPlanningSignalFreshnessFindings(stageId, findings, planningSignalFreshness) {
299
+ function applyPlanningSignalFreshnessFindings(stageId, findings, planningSignalFreshness, decisionTraceRecords) {
178
300
  let nextStageId = stageId;
179
301
  const strictPromotion = isStrictPromotionEnabled();
180
302
  const stalePlanningSignals =
@@ -192,6 +314,20 @@ function applyPlanningSignalFreshnessFindings(stageId, findings, planningSignalF
192
314
  `Stale ${surface} planning signal requires rerun before routing can rely on it (${reasonText}).`
193
315
  );
194
316
  if (!strictPromotion) {
317
+ recordWorkflowDecision(decisionTraceRecords, {
318
+ decisionFamily: "planning_signal_freshness",
319
+ decisionKey: "stale_signal_rerun_required",
320
+ outcome: "rerun_required",
321
+ reasonSummary: `Stale ${surface} planning signal requires rerun before routing can rely on it.`,
322
+ context: {
323
+ planningSurface: surface,
324
+ strictPromotion: false,
325
+ signalStatus: freshness.signalStatus || null,
326
+ staleByMs: Number.isFinite(freshness.staleByMs) ? freshness.staleByMs : null,
327
+ reasons: Array.isArray(freshness.reasons) ? freshness.reasons : []
328
+ },
329
+ evidenceRefs: [`signal:${surface}`]
330
+ });
195
331
  continue;
196
332
  }
197
333
  findings.blockers.push(
@@ -204,10 +340,23 @@ function applyPlanningSignalFreshnessFindings(stageId, findings, planningSignalF
204
340
  null,
205
341
  `strict promotion requires rerun for stale ${surface} planning signal`
206
342
  );
207
- nextStageId = fallbackStageIfBeyond(
208
- nextStageId,
209
- PLANNING_SIGNAL_PROMOTION_FALLBACKS[surface] || nextStageId
210
- );
343
+ const fallbackStageId = PLANNING_SIGNAL_PROMOTION_FALLBACKS[surface] || nextStageId;
344
+ recordWorkflowDecision(decisionTraceRecords, {
345
+ decisionFamily: "planning_signal_freshness",
346
+ decisionKey: "stale_signal_strict_fallback",
347
+ outcome: "downgraded",
348
+ reasonSummary: `Strict promotion forces routing fallback because ${surface} planning signal is stale.`,
349
+ context: {
350
+ planningSurface: surface,
351
+ strictPromotion: true,
352
+ signalStatus: freshness.signalStatus || null,
353
+ fallbackStage: fallbackStageId,
354
+ staleByMs: Number.isFinite(freshness.staleByMs) ? freshness.staleByMs : null,
355
+ reasons: Array.isArray(freshness.reasons) ? freshness.reasons : []
356
+ },
357
+ evidenceRefs: [`signal:${surface}`]
358
+ });
359
+ nextStageId = fallbackStageIfBeyond(nextStageId, fallbackStageId);
211
360
  }
212
361
 
213
362
  return nextStageId;
@@ -436,6 +585,10 @@ function applyTaskExecutionAndReviewFindings(findings, signals) {
436
585
 
437
586
  for (const signal of Object.values(latestTaskExecution)) {
438
587
  const envelope = signal.details && signal.details.envelope ? signal.details.envelope : null;
588
+ const outOfScopeWrites =
589
+ signal.details && Array.isArray(signal.details.outOfScopeWrites)
590
+ ? dedupeMessages(signal.details.outOfScopeWrites.map((item) => String(item || "").trim()).filter(Boolean))
591
+ : [];
439
592
  const taskGroupId =
440
593
  (envelope && envelope.taskGroupId) ||
441
594
  String(signal.surface || "").replace(/^task-execution\./, "") ||
@@ -445,6 +598,11 @@ function applyTaskExecutionAndReviewFindings(findings, signals) {
445
598
  } else if (signal.status === STATUS.WARN) {
446
599
  findings.warnings.push(`Task group ${taskGroupId} has unresolved implementer concerns/context needs.`);
447
600
  }
601
+ if (outOfScopeWrites.length > 0) {
602
+ findings.blockers.push(
603
+ `Task group ${taskGroupId} reported out-of-scope writes: ${outOfScopeWrites.join(", ")}.`
604
+ );
605
+ }
448
606
  if (envelope && envelope.summary) {
449
607
  findings.notes.push(`Implementer summary ${taskGroupId}: ${envelope.summary}`);
450
608
  }
@@ -476,6 +634,12 @@ function applyTaskExecutionAndReviewFindings(findings, signals) {
476
634
  );
477
635
  continue;
478
636
  }
637
+ if (state.quality && state.spec === STATUS.WARN) {
638
+ findings.blockers.push(
639
+ `Task review ordering violation for ${taskGroupId}: quality review was recorded before spec review reached PASS.`
640
+ );
641
+ continue;
642
+ }
479
643
  if (state.quality && state.spec === STATUS.BLOCK) {
480
644
  findings.blockers.push(
481
645
  `Task review ordering violation for ${taskGroupId}: quality review was recorded while spec review is BLOCK.`
@@ -1185,6 +1349,7 @@ function buildTaskGroupImplementerState(taskGroupId, signals, fallbackState) {
1185
1349
  testEvidence: Array.isArray(fallback.testEvidence) ? fallback.testEvidence : [],
1186
1350
  concerns: Array.isArray(fallback.concerns) ? fallback.concerns : [],
1187
1351
  blockers: Array.isArray(fallback.blockers) ? fallback.blockers : [],
1352
+ outOfScopeWrites: Array.isArray(fallback.outOfScopeWrites) ? fallback.outOfScopeWrites : [],
1188
1353
  recordedAt: fallback.recordedAt || null
1189
1354
  };
1190
1355
  }
@@ -1208,6 +1373,12 @@ function buildTaskGroupImplementerState(taskGroupId, signals, fallbackState) {
1208
1373
  : [],
1209
1374
  concerns: summarizeSignalIssues(signal, envelope && envelope.concerns),
1210
1375
  blockers: summarizeSignalIssues(signal, envelope && envelope.blockers),
1376
+ outOfScopeWrites:
1377
+ signal.details && Array.isArray(signal.details.outOfScopeWrites)
1378
+ ? dedupeMessages(signal.details.outOfScopeWrites.map((item) => String(item || "").trim()).filter(Boolean))
1379
+ : Array.isArray(fallback.outOfScopeWrites)
1380
+ ? fallback.outOfScopeWrites
1381
+ : [],
1211
1382
  recordedAt: (envelope && envelope.recordedAt) || signal.timestamp || fallback.recordedAt || null
1212
1383
  };
1213
1384
  }
@@ -1262,6 +1433,21 @@ function buildEffectiveTaskGroupState(group, planned, implementer, review) {
1262
1433
  reason: "planned_checklist"
1263
1434
  };
1264
1435
 
1436
+ if (implementer.present && implementer.outOfScopeWrites.length > 0) {
1437
+ return {
1438
+ status: "blocked",
1439
+ nextAction:
1440
+ `resolve out-of-scope writes for task group ${group.taskGroupId}: ${implementer.outOfScopeWrites.join(", ")}`,
1441
+ resumeCursor: {
1442
+ groupIndex: fallbackCursor.groupIndex,
1443
+ nextUncheckedItem: null,
1444
+ liveFocus: "out_of_scope_write"
1445
+ },
1446
+ source: "implementer",
1447
+ reason: "out_of_scope_write"
1448
+ };
1449
+ }
1450
+
1265
1451
  if (implementer.present && implementer.signalStatus === STATUS.BLOCK) {
1266
1452
  return {
1267
1453
  status: "blocked",
@@ -1288,6 +1474,23 @@ function buildEffectiveTaskGroupState(group, planned, implementer, review) {
1288
1474
  const reviewContextReady = review.required && (reviewSignalsPresent || reviewHardDue || implementer.present);
1289
1475
 
1290
1476
  if (reviewContextReady) {
1477
+ if (
1478
+ review.quality.present &&
1479
+ (!review.spec.present || review.spec.status === "missing" || review.spec.status === STATUS.WARN)
1480
+ ) {
1481
+ return {
1482
+ status: "blocked",
1483
+ nextAction:
1484
+ `remove or rerun out-of-order quality review for task group ${group.taskGroupId} after spec review PASS`,
1485
+ resumeCursor: {
1486
+ groupIndex: fallbackCursor.groupIndex,
1487
+ nextUncheckedItem: null,
1488
+ liveFocus: "review_ordering_violation"
1489
+ },
1490
+ source: "review",
1491
+ reason: "review_ordering_violation"
1492
+ };
1493
+ }
1291
1494
  if (review.spec.status === STATUS.BLOCK) {
1292
1495
  return {
1293
1496
  status: "blocked",
@@ -1418,7 +1621,7 @@ function buildEffectiveTaskGroupState(group, planned, implementer, review) {
1418
1621
  return effective;
1419
1622
  }
1420
1623
 
1421
- function deriveTaskGroupRuntimeState(plannedTaskGroups, signals, seedTaskGroups) {
1624
+ function deriveTaskGroupRuntimeState(plannedTaskGroups, signals, seedTaskGroups, decisionTraceRecords) {
1422
1625
  const plannedGroups = Array.isArray(plannedTaskGroups) ? plannedTaskGroups : [];
1423
1626
  const seedMap = normalizeTaskGroupSeedMap(seedTaskGroups);
1424
1627
 
@@ -1436,6 +1639,22 @@ function deriveTaskGroupRuntimeState(plannedTaskGroups, signals, seedTaskGroups)
1436
1639
  const implementer = buildTaskGroupImplementerState(taskGroupId, signals, seed.implementer);
1437
1640
  const review = buildTaskGroupReviewState(plannedGroup, signals, seed.review);
1438
1641
  const effective = buildEffectiveTaskGroupState(plannedGroup, planned, implementer, review);
1642
+ if (TRACEABLE_TASK_GROUP_FOCUS_REASONS.has(effective.reason)) {
1643
+ recordWorkflowDecision(decisionTraceRecords, {
1644
+ decisionFamily: "task_group_focus_resolution",
1645
+ decisionKey: effective.reason,
1646
+ outcome: "selected_focus",
1647
+ reasonSummary: buildTaskGroupFocusReasonSummary(taskGroupId, effective.reason),
1648
+ context: {
1649
+ taskGroupId,
1650
+ plannedStatus: planned.status || null,
1651
+ effectiveStatus: effective.status || null,
1652
+ liveFocus: effective.resumeCursor ? effective.resumeCursor.liveFocus || null : null,
1653
+ nextAction: effective.nextAction || null
1654
+ },
1655
+ evidenceRefs: buildTaskGroupFocusEvidenceRefs(taskGroupId, effective.reason)
1656
+ });
1657
+ }
1439
1658
 
1440
1659
  return {
1441
1660
  taskGroupId,
@@ -1487,7 +1706,13 @@ function loadTaskGroupMetadataFromPath(targetPath) {
1487
1706
  }
1488
1707
  }
1489
1708
 
1490
- function resolvePersistedTaskGroupSeed(projectRoot, changeId, persistedRecord, plannedTaskGroups) {
1709
+ function resolvePersistedTaskGroupSeed(
1710
+ projectRoot,
1711
+ changeId,
1712
+ persistedRecord,
1713
+ plannedTaskGroups,
1714
+ decisionTraceRecords
1715
+ ) {
1491
1716
  const metadataRefs =
1492
1717
  persistedRecord && persistedRecord.metadataRefs && typeof persistedRecord.metadataRefs === "object"
1493
1718
  ? persistedRecord.metadataRefs
@@ -1500,7 +1725,21 @@ function resolvePersistedTaskGroupSeed(projectRoot, changeId, persistedRecord, p
1500
1725
  const actualDigest = digestForPath(canonicalPath);
1501
1726
  const expectedDigest = metadataRefs.taskGroupsDigest || null;
1502
1727
  if (expectedDigest && actualDigest && expectedDigest !== actualDigest) {
1503
- notes.push("Canonical task-group runtime state digest mismatch; rebuilding task-group state from artifacts.");
1728
+ const message =
1729
+ "Canonical task-group runtime state digest mismatch; rebuilding task-group state from artifacts.";
1730
+ notes.push(message);
1731
+ recordWorkflowDecision(decisionTraceRecords, {
1732
+ decisionFamily: "task_group_seed_fallback",
1733
+ decisionKey: TASK_GROUP_SEED_TRACE_KEYS["digest-mismatch"],
1734
+ outcome: "fallback",
1735
+ reasonSummary: message,
1736
+ context: {
1737
+ metadataPath: formatPathRef(projectRoot, canonicalPath),
1738
+ taskGroupCount: Array.isArray(plannedTaskGroups) ? plannedTaskGroups.length : 0,
1739
+ expectedDigestPresent: true
1740
+ },
1741
+ evidenceRefs: [`state:${formatPathRef(projectRoot, canonicalPath)}`]
1742
+ });
1504
1743
  return {
1505
1744
  taskGroups: plannedTaskGroups,
1506
1745
  notes
@@ -1516,7 +1755,22 @@ function resolvePersistedTaskGroupSeed(projectRoot, changeId, persistedRecord, p
1516
1755
  notes
1517
1756
  };
1518
1757
  }
1519
- notes.push("Canonical task-group runtime state is unreadable; rebuilding task-group state from artifacts.");
1758
+ {
1759
+ const message =
1760
+ "Canonical task-group runtime state is unreadable; rebuilding task-group state from artifacts.";
1761
+ notes.push(message);
1762
+ recordWorkflowDecision(decisionTraceRecords, {
1763
+ decisionFamily: "task_group_seed_fallback",
1764
+ decisionKey: TASK_GROUP_SEED_TRACE_KEYS.unreadable,
1765
+ outcome: "fallback",
1766
+ reasonSummary: message,
1767
+ context: {
1768
+ metadataPath: formatPathRef(projectRoot, canonicalPath),
1769
+ taskGroupCount: Array.isArray(plannedTaskGroups) ? plannedTaskGroups.length : 0
1770
+ },
1771
+ evidenceRefs: [`state:${formatPathRef(projectRoot, canonicalPath)}`]
1772
+ });
1773
+ }
1520
1774
  return {
1521
1775
  taskGroups: plannedTaskGroups,
1522
1776
  notes
@@ -1524,14 +1778,45 @@ function resolvePersistedTaskGroupSeed(projectRoot, changeId, persistedRecord, p
1524
1778
  }
1525
1779
 
1526
1780
  if (Array.isArray(persistedRecord && persistedRecord.taskGroups) && persistedRecord.taskGroups.length > 0) {
1527
- notes.push("Using legacy embedded task-group state as migration fallback.");
1781
+ {
1782
+ const message = "Using legacy embedded task-group state as migration fallback.";
1783
+ notes.push(message);
1784
+ recordWorkflowDecision(decisionTraceRecords, {
1785
+ decisionFamily: "task_group_seed_fallback",
1786
+ decisionKey: TASK_GROUP_SEED_TRACE_KEYS.legacy,
1787
+ outcome: "fallback",
1788
+ reasonSummary: message,
1789
+ context: {
1790
+ metadataPath: canonicalPath ? formatPathRef(projectRoot, canonicalPath) : null,
1791
+ taskGroupCount: persistedRecord.taskGroups.length
1792
+ },
1793
+ evidenceRefs: [`state:${formatPathRef(projectRoot, resolveWorkflowStatePath(projectRoot))}`]
1794
+ });
1795
+ }
1528
1796
  return {
1529
1797
  taskGroups: persistedRecord.taskGroups,
1530
1798
  notes
1531
1799
  };
1532
1800
  }
1533
1801
 
1534
- notes.push("Canonical task-group runtime state is missing; rebuilding task-group state from artifacts.");
1802
+ {
1803
+ const message = "Canonical task-group runtime state is missing; rebuilding task-group state from artifacts.";
1804
+ notes.push(message);
1805
+ recordWorkflowDecision(decisionTraceRecords, {
1806
+ decisionFamily: "task_group_seed_fallback",
1807
+ decisionKey: TASK_GROUP_SEED_TRACE_KEYS.missing,
1808
+ outcome: "fallback",
1809
+ reasonSummary: message,
1810
+ context: {
1811
+ metadataPath: canonicalPath ? formatPathRef(projectRoot, canonicalPath) : null,
1812
+ taskGroupCount: Array.isArray(plannedTaskGroups) ? plannedTaskGroups.length : 0
1813
+ },
1814
+ evidenceRefs:
1815
+ canonicalPath && String(canonicalPath).trim()
1816
+ ? [`state:${formatPathRef(projectRoot, canonicalPath)}`]
1817
+ : []
1818
+ });
1819
+ }
1535
1820
  return {
1536
1821
  taskGroups: plannedTaskGroups,
1537
1822
  notes
@@ -1578,14 +1863,23 @@ function finalizeWorkflowView(options = {}) {
1578
1863
  stalePlanningSignals: {},
1579
1864
  needsRerunSurfaces: []
1580
1865
  };
1866
+ const decisionTraceRecords = Array.isArray(options.decisionTraceRecords)
1867
+ ? options.decisionTraceRecords
1868
+ : null;
1581
1869
  const taskGroups = deriveTaskGroupRuntimeState(
1582
1870
  options.plannedTaskGroups,
1583
1871
  options.changeSignals,
1584
- options.taskGroupSeed
1872
+ options.taskGroupSeed,
1873
+ decisionTraceRecords
1585
1874
  );
1586
1875
 
1587
1876
  stageId = applyAuditFindings(stageId, findings, integrityAudit, completionAudit);
1588
- stageId = applyPlanningSignalFreshnessFindings(stageId, findings, planningSignalFreshness);
1877
+ stageId = applyPlanningSignalFreshnessFindings(
1878
+ stageId,
1879
+ findings,
1880
+ planningSignalFreshness,
1881
+ decisionTraceRecords
1882
+ );
1589
1883
  stageId = applyExecutionSignalFindings(stageId, findings, planningSignalFreshness.effectiveSignalSummary || {});
1590
1884
  applyTaskExecutionAndReviewFindings(findings, options.changeSignals || []);
1591
1885
 
@@ -1599,10 +1893,31 @@ function finalizeWorkflowView(options = {}) {
1599
1893
  }
1600
1894
 
1601
1895
  if (verificationFreshness && !verificationFreshness.fresh && (stageId === "verify" || stageId === "complete")) {
1896
+ const stageBeforeFreshness = stageId;
1602
1897
  findings.blockers.push(
1603
1898
  "Completion-facing routing requires fresh verification evidence; stale evidence keeps the route in verify."
1604
1899
  );
1605
1900
  stageId = "verify";
1901
+ if (stageBeforeFreshness === "complete") {
1902
+ recordWorkflowDecision(decisionTraceRecords, {
1903
+ decisionFamily: "verification_freshness_downgrade",
1904
+ decisionKey: "verification_freshness_stale",
1905
+ outcome: "downgraded",
1906
+ reasonSummary: "Completion-facing routing stays in verify because verification evidence is stale.",
1907
+ context: {
1908
+ fromStage: "complete",
1909
+ toStage: "verify",
1910
+ baselineIso: verificationFreshness.baselineIso || null,
1911
+ staleReasonCount: Array.isArray(verificationFreshness.staleReasons)
1912
+ ? verificationFreshness.staleReasons.length
1913
+ : 0,
1914
+ requiredSurfaces: Array.isArray(verificationFreshness.requiredSurfaces)
1915
+ ? verificationFreshness.requiredSurfaces
1916
+ : []
1917
+ },
1918
+ evidenceRefs: collectVerificationFreshnessEvidenceRefs(verificationFreshness)
1919
+ });
1920
+ }
1606
1921
  }
1607
1922
 
1608
1923
  const gates = buildGatesWithLiveOverlays(
@@ -1633,6 +1948,26 @@ function finalizeWorkflowView(options = {}) {
1633
1948
  findings.warnings.push(
1634
1949
  "Bounded-parallel profile downgraded to serial until worktree isolation is ready or explicitly accepted."
1635
1950
  );
1951
+ recordWorkflowDecision(decisionTraceRecords, {
1952
+ decisionFamily: "worktree_isolation_downgrade",
1953
+ decisionKey: "effective_serial_after_preflight",
1954
+ outcome: "downgraded",
1955
+ reasonSummary: "Worktree preflight downgraded advisory bounded parallel execution to effective serial mode.",
1956
+ context: {
1957
+ advisoryMode: executionProfile.mode,
1958
+ effectiveMode: executionProfile.effectiveMode || "serial",
1959
+ preflightStatus: worktreePreflight.status || null,
1960
+ recommendedIsolation: Boolean(
1961
+ worktreePreflight.summary && worktreePreflight.summary.recommendedIsolation
1962
+ ),
1963
+ dirtyEntries:
1964
+ worktreePreflight.summary &&
1965
+ Number.isFinite(Number(worktreePreflight.summary.dirtyEntries))
1966
+ ? Number(worktreePreflight.summary.dirtyEntries)
1967
+ : 0
1968
+ },
1969
+ evidenceRefs: ["surface:worktree-preflight"]
1970
+ });
1636
1971
  }
1637
1972
  }
1638
1973
 
@@ -1713,23 +2048,30 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
1713
2048
 
1714
2049
  if (!pathExists(projectRoot)) {
1715
2050
  findings.blockers.push(`Project path does not exist: ${projectRoot}`);
1716
- return buildWorkflowResult({
1717
- projectRoot,
1718
- changeId: null,
1719
- stageId: "bootstrap",
1720
- findings,
1721
- checkpoints: {},
1722
- gates: {},
1723
- audits: {
1724
- integrity: null,
1725
- completion: null
1726
- },
1727
- routeContext: {
2051
+ return finalizeResultWithWorkflowDecisionTracing(
2052
+ buildWorkflowResult({
1728
2053
  projectRoot,
1729
- changeId: requestedChangeId || "change-001",
1730
- ambiguousChangeSelection: false
2054
+ changeId: null,
2055
+ stageId: "bootstrap",
2056
+ findings,
2057
+ checkpoints: {},
2058
+ gates: {},
2059
+ audits: {
2060
+ integrity: null,
2061
+ completion: null
2062
+ },
2063
+ routeContext: {
2064
+ projectRoot,
2065
+ changeId: requestedChangeId || "change-001",
2066
+ ambiguousChangeSelection: false
2067
+ }
2068
+ }),
2069
+ {
2070
+ env: options.env,
2071
+ surface: options.traceSurface,
2072
+ records: null
1731
2073
  }
1732
- });
2074
+ );
1733
2075
  }
1734
2076
 
1735
2077
  const workflowRoot = path.join(projectRoot, ".da-vinci");
@@ -1766,6 +2108,13 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
1766
2108
  }
1767
2109
 
1768
2110
  const activeChangeId = activeChangeDir ? path.basename(activeChangeDir) : null;
2111
+ const decisionTraceRecords =
2112
+ shouldTraceWorkflowDecisions({
2113
+ env: options.env,
2114
+ surface: options.traceSurface
2115
+ }) && !ambiguousChangeSelection
2116
+ ? []
2117
+ : null;
1769
2118
  const artifactState = {
1770
2119
  workflowRootReady: pathExists(workflowRoot),
1771
2120
  changeSelected: Boolean(activeChangeDir),
@@ -1831,6 +2180,26 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
1831
2180
  staleWindowMs: options.staleWindowMs
1832
2181
  });
1833
2182
  if (persistedSelection.usable && persistedSelection.changeRecord) {
2183
+ const advisoryAgeAccepted =
2184
+ Array.isArray(persistedSelection.advisoryNotes) && persistedSelection.advisoryNotes.length > 0;
2185
+ recordWorkflowDecision(decisionTraceRecords, {
2186
+ decisionFamily: "persisted_state_trust",
2187
+ decisionKey: advisoryAgeAccepted ? "accepted_age_advisory" : "accepted_digest_match",
2188
+ outcome: "accepted",
2189
+ reasonSummary: advisoryAgeAccepted
2190
+ ? "Persisted workflow snapshot remains trusted because artifact content digests still match despite advisory age."
2191
+ : "Persisted workflow snapshot is trusted because artifact content digests still match.",
2192
+ context: {
2193
+ statePath: formatPathRef(projectRoot, persistedSelection.statePath),
2194
+ persistedVersion:
2195
+ persistedSelection.persisted && Number.isFinite(Number(persistedSelection.persisted.version))
2196
+ ? Number(persistedSelection.persisted.version)
2197
+ : null,
2198
+ advisoryAge: advisoryAgeAccepted,
2199
+ fingerprintMatched: true
2200
+ },
2201
+ evidenceRefs: [`state:${formatPathRef(projectRoot, persistedSelection.statePath)}`]
2202
+ });
1834
2203
  const persistedRecord = persistedSelection.changeRecord;
1835
2204
  const stageRecord = getStageById(persistedRecord.stage) || getStageById("bootstrap");
1836
2205
  const completionAudit =
@@ -1841,40 +2210,49 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
1841
2210
  projectRoot,
1842
2211
  activeChangeId,
1843
2212
  persistedRecord,
1844
- plannedTaskGroups
1845
- );
1846
- return finalizeWorkflowView({
1847
- projectRoot,
1848
- changeId: activeChangeId,
1849
- stageId: stageRecord.id,
1850
- findings: {
1851
- blockers: Array.isArray(persistedRecord.failures) ? persistedRecord.failures.slice() : [],
1852
- warnings: Array.isArray(persistedRecord.warnings) ? persistedRecord.warnings.slice() : [],
1853
- notes: [
1854
- ...sanitizePersistedNotes(persistedRecord.notes),
1855
- ...(Array.isArray(persistedSelection.advisoryNotes) ? persistedSelection.advisoryNotes : []),
1856
- ...persistedSeed.notes,
1857
- "workflow-status is using trusted persisted workflow state."
1858
- ]
1859
- },
1860
- baseGates:
1861
- persistedRecord && persistedRecord.gates && typeof persistedRecord.gates === "object"
1862
- ? { ...persistedRecord.gates }
1863
- : {},
1864
- checkpoints: checkpointStatuses,
1865
- routeContext,
1866
- source: "persisted",
1867
- taskGroupSeed: persistedSeed.taskGroups,
1868
2213
  plannedTaskGroups,
1869
- changeSignals,
1870
- signalSummary,
1871
- planningSignalFreshness,
1872
- integrityAudit,
1873
- completionAudit,
1874
- disciplineState,
1875
- verificationFreshness,
1876
- hasTasksArtifact: artifactState.tasks
1877
- });
2214
+ decisionTraceRecords
2215
+ );
2216
+ return finalizeResultWithWorkflowDecisionTracing(
2217
+ finalizeWorkflowView({
2218
+ projectRoot,
2219
+ changeId: activeChangeId,
2220
+ stageId: stageRecord.id,
2221
+ findings: {
2222
+ blockers: Array.isArray(persistedRecord.failures) ? persistedRecord.failures.slice() : [],
2223
+ warnings: Array.isArray(persistedRecord.warnings) ? persistedRecord.warnings.slice() : [],
2224
+ notes: [
2225
+ ...sanitizePersistedNotes(persistedRecord.notes),
2226
+ ...(Array.isArray(persistedSelection.advisoryNotes) ? persistedSelection.advisoryNotes : []),
2227
+ ...persistedSeed.notes,
2228
+ "workflow-status is using trusted persisted workflow state."
2229
+ ]
2230
+ },
2231
+ baseGates:
2232
+ persistedRecord && persistedRecord.gates && typeof persistedRecord.gates === "object"
2233
+ ? { ...persistedRecord.gates }
2234
+ : {},
2235
+ checkpoints: checkpointStatuses,
2236
+ routeContext,
2237
+ source: "persisted",
2238
+ taskGroupSeed: persistedSeed.taskGroups,
2239
+ plannedTaskGroups,
2240
+ changeSignals,
2241
+ signalSummary,
2242
+ planningSignalFreshness,
2243
+ integrityAudit,
2244
+ completionAudit,
2245
+ disciplineState,
2246
+ verificationFreshness,
2247
+ hasTasksArtifact: artifactState.tasks,
2248
+ decisionTraceRecords
2249
+ }),
2250
+ {
2251
+ env: options.env,
2252
+ surface: options.traceSurface,
2253
+ records: decisionTraceRecords
2254
+ }
2255
+ );
1878
2256
  }
1879
2257
 
1880
2258
  if (!persistedSelection.usable && persistedSelection.reason) {
@@ -1888,6 +2266,22 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
1888
2266
  const message = reasonMessage[persistedSelection.reason];
1889
2267
  if (message) {
1890
2268
  findings.notes.push(message);
2269
+ recordWorkflowDecision(decisionTraceRecords, {
2270
+ decisionFamily: "persisted_state_trust",
2271
+ decisionKey: PERSISTED_STATE_TRACE_KEYS[persistedSelection.reason],
2272
+ outcome: "fallback",
2273
+ reasonSummary: message,
2274
+ context: {
2275
+ statePath: formatPathRef(projectRoot, persistedSelection.statePath),
2276
+ persistedVersion:
2277
+ persistedSelection.persisted && Number.isFinite(Number(persistedSelection.persisted.version))
2278
+ ? Number(persistedSelection.persisted.version)
2279
+ : null,
2280
+ fingerprintMatched:
2281
+ persistedSelection.reason === "fingerprint-mismatch" ? false : null
2282
+ },
2283
+ evidenceRefs: [`state:${formatPathRef(projectRoot, persistedSelection.statePath)}`]
2284
+ });
1891
2285
  }
1892
2286
  }
1893
2287
  }
@@ -1945,7 +2339,8 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
1945
2339
  completionAudit,
1946
2340
  disciplineState,
1947
2341
  verificationFreshness,
1948
- hasTasksArtifact: artifactState.tasks
2342
+ hasTasksArtifact: artifactState.tasks,
2343
+ decisionTraceRecords
1949
2344
  });
1950
2345
 
1951
2346
  if (activeChangeId && !ambiguousChangeSelection) {
@@ -2015,7 +2410,11 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
2015
2410
  ]);
2016
2411
  }
2017
2412
 
2018
- return derivedResult;
2413
+ return finalizeResultWithWorkflowDecisionTracing(derivedResult, {
2414
+ env: options.env,
2415
+ surface: options.traceSurface,
2416
+ records: decisionTraceRecords
2417
+ });
2019
2418
  }
2020
2419
 
2021
2420
  function buildWorkflowResult(params) {