@xenonbyte/da-vinci-workflow 0.2.7 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.2.8 - 2026-04-05
4
+
5
+ ### Added
6
+ - optional workflow decision tracing via `.da-vinci/logs/workflow-decisions/YYYY-MM-DD.ndjson`, gated by `DA_VINCI_TRACE_WORKFLOW_DECISIONS=1`
7
+ - compact trace coverage for persisted-state trust, canonical task-group seed fallback, task-group focus override, stale planning-signal fallback, verification freshness downgrade, and worktree-isolation downgrade
8
+ - targeted regression coverage in `scripts/test-workflow-decision-tracing.js` for eligible surfaces, silence rules, sink-write failures, and trace-schema validation behavior
9
+
10
+ ### Changed
11
+ - `workflow-status` and `next-step` now emit bounded decision traces only on the explicitly allowed surfaces, while keeping route/state truth unchanged
12
+ - workflow trace records now reject invalid family/key/outcome combinations with visible diagnostic feedback instead of silently disappearing
13
+ - current release notes in `README.md` and `README.zh-CN.md` now point to the `0.2.8` workflow decision tracing release
14
+
15
+ ### Fixed
16
+ - missing review or verification evidence no longer produces fake `evidenceRefs` in workflow decision traces
17
+ - persisted-state fallback traces no longer imply a fingerprint comparison happened for non-fingerprint fallback paths
18
+ - workflow tracing diagnostics remain non-blocking even when trace persistence fails or candidate trace records are invalid
19
+
3
20
  ## v0.2.7 - 2026-04-05
4
21
 
5
22
  ### Added
package/README.md CHANGED
@@ -34,15 +34,15 @@ Use `da-vinci maintainer-readiness` as the canonical maintainer diagnosis surfac
34
34
 
35
35
  Latest published npm package:
36
36
 
37
- - `@xenonbyte/da-vinci-workflow@0.2.7`
37
+ - `@xenonbyte/da-vinci-workflow@0.2.8`
38
38
 
39
- Release highlights for `0.2.7`:
39
+ Release highlights for `0.2.8`:
40
40
 
41
- - bounded worker isolation is now formalized as a contract-only OpenSpec change, with explicit task-group ownership, isolated workspace, handoff, sequencing, writeback, and downgrade rules
42
- - `task-execution`, `task-review`, and `workflow-state` now align with that contract by blocking out-of-scope writes, preserving review ordering, and rejecting `partial=true` + `DONE`
43
- - the reviewer bridge now runs `codex exec` with closed stdin plus explicit prompt separation, and preserves raw diagnostics when structured reviewer output is missing
44
- - contract CI now exercises bounded-worker-isolation phase 1-4 tests directly instead of relying only on docs/asset consistency checks
45
- - supervisor-review smoke fixtures now use valid exported screenshots, and the real reviewer bridge integration passes end to end
41
+ - optional workflow decision tracing is now available for `workflow-status` and `next-step` through `DA_VINCI_TRACE_WORKFLOW_DECISIONS=1`, with records written to `.da-vinci/logs/workflow-decisions/YYYY-MM-DD.ndjson`
42
+ - the initial trace-family allowlist now covers persisted-state trust, task-group seed fallback, task-group focus override, stale planning-signal fallback, verification freshness downgrade, and worktree-isolation downgrade
43
+ - workflow decision traces remain bounded and diagnostic-only: they do not change routing truth, do not run on non-eligible commands, and do not block normal command execution when trace persistence fails
44
+ - trace records now reject invalid schema combinations with visible diagnostics instead of silently disappearing
45
+ - trace payloads no longer claim missing evidence exists, and persisted fallback traces no longer imply a fingerprint comparison happened when it did not
46
46
 
47
47
  ## Discipline And Orchestration Upgrade
48
48
 
package/README.zh-CN.md CHANGED
@@ -37,15 +37,15 @@ Da Vinci 是一个把产品需求一路推进到结构化规格、Pencil 设计
37
37
 
38
38
  最新已发布 npm 包:
39
39
 
40
- - `@xenonbyte/da-vinci-workflow@0.2.7`
40
+ - `@xenonbyte/da-vinci-workflow@0.2.8`
41
41
 
42
- `0.2.7` 版本重点:
42
+ `0.2.8` 版本重点:
43
43
 
44
- - `bounded-worker-isolation-contract` 现已作为 contract-only OpenSpec 变更落地,明确 task-group ownership、isolated workspace、handoff、sequencing、writeback downgrade 规则
45
- - `task-execution`、`task-review`、`workflow-state` 现已与该 contract 对齐:会阻断 out-of-scope writes,保持 review 顺序,并拒绝 `partial=true` `DONE` 组合
46
- - reviewer bridge 现在会以显式 prompt 分隔并关闭 stdin 的方式运行 `codex exec`,同时在 reviewer 缺失结构化输出时保留原始诊断
47
- - contract CI 现在会直接跑 bounded-worker-isolation phase 1-4 回归,不再只依赖文档/命令资产一致性检查
48
- - supervisor-review smoke fixture 现在使用有效截图,真实 reviewer bridge integration 已能端到端通过
44
+ - 现在可以通过 `DA_VINCI_TRACE_WORKFLOW_DECISIONS=1` `workflow-status` `next-step` 开启可选的 workflow decision tracing,记录会写入 `.da-vinci/logs/workflow-decisions/YYYY-MM-DD.ndjson`
45
+ - 首批 trace-family allowlist 已覆盖 persisted-state trust、task-group seed fallback、task-group focus override、stale planning-signal fallback、verification freshness downgradeworktree-isolation downgrade
46
+ - workflow decision trace 仍然是 bounded diagnostic-only:不会改变 routing truth,不会在非 eligible command 上发射,也不会因为 trace 持久化失败而阻断正常命令
47
+ - trace record 现在会对非法 schema 组合给出可见诊断,不再静默丢失
48
+ - trace payload 不再为缺失证据伪造 `evidenceRefs`,persisted fallback trace 也不再错误暗示某些未发生的 fingerprint comparison
49
49
 
50
50
  ## Discipline And Orchestration 升级
51
51
 
package/lib/cli.js CHANGED
@@ -1097,7 +1097,11 @@ async function runCli(argv) {
1097
1097
  if (command === "workflow-status") {
1098
1098
  const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1099
1099
  const changeId = getOption(argv, "--change");
1100
- const result = deriveWorkflowStatus(projectPath, { changeId });
1100
+ const result = deriveWorkflowStatus(projectPath, {
1101
+ changeId,
1102
+ traceSurface: "workflow-status",
1103
+ env: process.env
1104
+ });
1101
1105
 
1102
1106
  if (argv.includes("--json")) {
1103
1107
  console.log(JSON.stringify(result, null, 2));
@@ -1111,7 +1115,11 @@ async function runCli(argv) {
1111
1115
  if (command === "next-step") {
1112
1116
  const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1113
1117
  const changeId = getOption(argv, "--change");
1114
- const result = deriveWorkflowStatus(projectPath, { changeId });
1118
+ const result = deriveWorkflowStatus(projectPath, {
1119
+ changeId,
1120
+ traceSurface: "next-step",
1121
+ env: process.env
1122
+ });
1115
1123
 
1116
1124
  if (argv.includes("--json")) {
1117
1125
  console.log(
@@ -1124,7 +1132,8 @@ async function runCli(argv) {
1124
1132
  discipline: result.discipline || null,
1125
1133
  executionProfile: result.executionProfile || null,
1126
1134
  worktreePreflight: result.worktreePreflight || null,
1127
- verificationFreshness: result.verificationFreshness || null
1135
+ verificationFreshness: result.verificationFreshness || null,
1136
+ traceDiagnostics: result.traceDiagnostics || null
1128
1137
  },
1129
1138
  null,
1130
1139
  2
@@ -0,0 +1,335 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { normalizeRelativePath } = require("./utils");
4
+
5
+ const TRACE_ENABLE_ENV = "DA_VINCI_TRACE_WORKFLOW_DECISIONS";
6
+ const ELIGIBLE_SURFACES = new Set(["workflow-status", "next-step"]);
7
+ const DECISION_FAMILIES = new Set([
8
+ "persisted_state_trust",
9
+ "task_group_seed_fallback",
10
+ "task_group_focus_resolution",
11
+ "planning_signal_freshness",
12
+ "verification_freshness_downgrade",
13
+ "worktree_isolation_downgrade"
14
+ ]);
15
+ const OUTCOMES = new Set([
16
+ "accepted",
17
+ "fallback",
18
+ "selected_focus",
19
+ "rerun_required",
20
+ "downgraded",
21
+ "suppressed"
22
+ ]);
23
+ const DECISION_KEYS_BY_FAMILY = Object.freeze({
24
+ persisted_state_trust: new Set([
25
+ "accepted_digest_match",
26
+ "accepted_age_advisory",
27
+ "fallback_missing",
28
+ "fallback_parse_error",
29
+ "fallback_version_mismatch",
30
+ "fallback_change_missing",
31
+ "fallback_fingerprint_mismatch"
32
+ ]),
33
+ task_group_seed_fallback: new Set([
34
+ "seed_missing",
35
+ "seed_unreadable",
36
+ "seed_digest_mismatch",
37
+ "seed_legacy_embedded"
38
+ ]),
39
+ task_group_focus_resolution: new Set([
40
+ "implementer_block",
41
+ "implementer_warn",
42
+ "spec_review_missing",
43
+ "spec_review_block",
44
+ "spec_review_warn",
45
+ "quality_review_missing",
46
+ "quality_review_block",
47
+ "quality_review_warn"
48
+ ]),
49
+ planning_signal_freshness: new Set([
50
+ "stale_signal_rerun_required",
51
+ "stale_signal_strict_fallback"
52
+ ]),
53
+ verification_freshness_downgrade: new Set(["verification_freshness_stale"]),
54
+ worktree_isolation_downgrade: new Set(["effective_serial_after_preflight"])
55
+ });
56
+
57
+ const MAX_CONTEXT_ARRAY_LENGTH = 8;
58
+ const MAX_EVIDENCE_REFS = 8;
59
+ const MAX_TEXT_LENGTH = 240;
60
+
61
+ function truncateText(value, maxLength = MAX_TEXT_LENGTH) {
62
+ const text = String(value || "").trim();
63
+ if (!text) {
64
+ return "";
65
+ }
66
+ if (text.length <= maxLength) {
67
+ return text;
68
+ }
69
+ return `${text.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
70
+ }
71
+
72
+ function formatDateToken(date = new Date()) {
73
+ const year = date.getFullYear();
74
+ const month = `${date.getMonth() + 1}`.padStart(2, "0");
75
+ const day = `${date.getDate()}`.padStart(2, "0");
76
+ return `${year}-${month}-${day}`;
77
+ }
78
+
79
+ function isWorkflowDecisionTracingEnabled(env = process.env) {
80
+ return String(env && env[TRACE_ENABLE_ENV] ? env[TRACE_ENABLE_ENV] : "").trim() === "1";
81
+ }
82
+
83
+ function isEligibleWorkflowTraceSurface(surface) {
84
+ return ELIGIBLE_SURFACES.has(String(surface || "").trim());
85
+ }
86
+
87
+ function shouldTraceWorkflowDecisions(options = {}) {
88
+ return (
89
+ isWorkflowDecisionTracingEnabled(options.env) &&
90
+ isEligibleWorkflowTraceSurface(options.surface)
91
+ );
92
+ }
93
+
94
+ function resolveWorkflowDecisionTracePath(projectRoot, date = new Date()) {
95
+ return path.join(
96
+ path.resolve(projectRoot || process.cwd()),
97
+ ".da-vinci",
98
+ "logs",
99
+ "workflow-decisions",
100
+ `${formatDateToken(date)}.ndjson`
101
+ );
102
+ }
103
+
104
+ function formatPathRef(projectRoot, candidatePath) {
105
+ const resolved = path.resolve(String(candidatePath || ""));
106
+ const root = path.resolve(projectRoot || process.cwd());
107
+ const relative = path.relative(root, resolved);
108
+ if (!relative || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
109
+ return normalizeRelativePath(relative || ".");
110
+ }
111
+ return resolved.split(path.sep).join("/");
112
+ }
113
+
114
+ function normalizeContextValue(value) {
115
+ if (value === null) {
116
+ return null;
117
+ }
118
+ if (typeof value === "string") {
119
+ const text = truncateText(value);
120
+ return text || undefined;
121
+ }
122
+ if (typeof value === "number") {
123
+ return Number.isFinite(value) ? value : undefined;
124
+ }
125
+ if (typeof value === "boolean") {
126
+ return value;
127
+ }
128
+ if (Array.isArray(value)) {
129
+ const items = value
130
+ .map((item) => truncateText(item))
131
+ .filter(Boolean)
132
+ .slice(0, MAX_CONTEXT_ARRAY_LENGTH);
133
+ return items.length > 0 ? items : undefined;
134
+ }
135
+ return undefined;
136
+ }
137
+
138
+ function normalizeContext(context) {
139
+ const normalized = {};
140
+ if (!context || typeof context !== "object" || Array.isArray(context)) {
141
+ return normalized;
142
+ }
143
+
144
+ for (const [key, value] of Object.entries(context)) {
145
+ const normalizedValue = normalizeContextValue(value);
146
+ if (normalizedValue !== undefined) {
147
+ normalized[key] = normalizedValue;
148
+ }
149
+ }
150
+
151
+ return normalized;
152
+ }
153
+
154
+ function normalizeEvidenceRefs(evidenceRefs) {
155
+ return Array.from(
156
+ new Set(
157
+ (Array.isArray(evidenceRefs) ? evidenceRefs : [])
158
+ .map((item) => truncateText(item))
159
+ .filter(Boolean)
160
+ )
161
+ ).slice(0, MAX_EVIDENCE_REFS);
162
+ }
163
+
164
+ function inspectWorkflowDecisionRecord(record) {
165
+ if (!record || typeof record !== "object") {
166
+ return {
167
+ normalized: null,
168
+ rejection: "record must be an object"
169
+ };
170
+ }
171
+
172
+ const decisionFamily = String(record.decisionFamily || "").trim();
173
+ const decisionKey = String(record.decisionKey || "").trim();
174
+ const outcome = String(record.outcome || "").trim();
175
+ const reasonSummary = truncateText(record.reasonSummary);
176
+
177
+ if (!DECISION_FAMILIES.has(decisionFamily)) {
178
+ return {
179
+ normalized: null,
180
+ rejection: `unsupported decisionFamily: ${decisionFamily || "(empty)"}`
181
+ };
182
+ }
183
+ if (!OUTCOMES.has(outcome)) {
184
+ return {
185
+ normalized: null,
186
+ rejection: `unsupported outcome for ${decisionFamily}: ${outcome || "(empty)"}`
187
+ };
188
+ }
189
+ const allowedKeys = DECISION_KEYS_BY_FAMILY[decisionFamily];
190
+ if (!allowedKeys || !allowedKeys.has(decisionKey)) {
191
+ return {
192
+ normalized: null,
193
+ rejection: `unsupported decisionKey for ${decisionFamily}: ${decisionKey || "(empty)"}`
194
+ };
195
+ }
196
+ if (!reasonSummary) {
197
+ return {
198
+ normalized: null,
199
+ rejection: `reasonSummary is required for ${decisionFamily}/${decisionKey}`
200
+ };
201
+ }
202
+
203
+ return {
204
+ normalized: {
205
+ decisionFamily,
206
+ decisionKey,
207
+ outcome,
208
+ reasonSummary,
209
+ context: normalizeContext(record.context),
210
+ evidenceRefs: normalizeEvidenceRefs(record.evidenceRefs)
211
+ },
212
+ rejection: null
213
+ };
214
+ }
215
+
216
+ function emitWorkflowDecisionTraces(options = {}) {
217
+ if (!shouldTraceWorkflowDecisions(options)) {
218
+ return {
219
+ enabled: false,
220
+ written: 0,
221
+ tracePath: null,
222
+ error: null,
223
+ rejectedCount: 0,
224
+ rejections: []
225
+ };
226
+ }
227
+
228
+ const changeId = String(options.changeId || "").trim();
229
+ const stage = String(options.stage || "").trim();
230
+ if (!changeId || !stage) {
231
+ return {
232
+ enabled: true,
233
+ written: 0,
234
+ tracePath: null,
235
+ error: null,
236
+ rejectedCount: 0,
237
+ rejections: []
238
+ };
239
+ }
240
+
241
+ const rejections = [];
242
+ const normalizedRecords = [];
243
+ for (const record of Array.isArray(options.records) ? options.records : []) {
244
+ const inspected = inspectWorkflowDecisionRecord(record);
245
+ if (inspected.normalized) {
246
+ normalizedRecords.push(inspected.normalized);
247
+ continue;
248
+ }
249
+ if (inspected.rejection) {
250
+ rejections.push(inspected.rejection);
251
+ }
252
+ }
253
+ if (normalizedRecords.length === 0) {
254
+ return {
255
+ enabled: true,
256
+ written: 0,
257
+ tracePath: null,
258
+ error: null,
259
+ rejectedCount: rejections.length,
260
+ rejections: rejections.slice(0, 3)
261
+ };
262
+ }
263
+
264
+ const now = options.now instanceof Date ? options.now : new Date();
265
+ const timestamp = now.toISOString();
266
+ const surface = String(options.surface || "").trim();
267
+ const tracePath = resolveWorkflowDecisionTracePath(options.projectRoot, now);
268
+ const lines = [];
269
+ const seen = new Set();
270
+
271
+ for (const record of normalizedRecords) {
272
+ const payload = {
273
+ timestamp,
274
+ surface,
275
+ changeId,
276
+ stage,
277
+ decisionFamily: record.decisionFamily,
278
+ decisionKey: record.decisionKey,
279
+ outcome: record.outcome,
280
+ reasonSummary: record.reasonSummary,
281
+ context: record.context,
282
+ evidenceRefs: record.evidenceRefs
283
+ };
284
+ const serialized = JSON.stringify(payload);
285
+ if (seen.has(serialized)) {
286
+ continue;
287
+ }
288
+ seen.add(serialized);
289
+ lines.push(serialized);
290
+ }
291
+
292
+ if (lines.length === 0) {
293
+ return {
294
+ enabled: true,
295
+ written: 0,
296
+ tracePath: null,
297
+ error: null
298
+ };
299
+ }
300
+
301
+ try {
302
+ fs.mkdirSync(path.dirname(tracePath), { recursive: true });
303
+ fs.appendFileSync(tracePath, `${lines.join("\n")}\n`, "utf8");
304
+ return {
305
+ enabled: true,
306
+ written: lines.length,
307
+ tracePath,
308
+ error: null,
309
+ rejectedCount: rejections.length,
310
+ rejections: rejections.slice(0, 3)
311
+ };
312
+ } catch (error) {
313
+ return {
314
+ enabled: true,
315
+ written: 0,
316
+ tracePath,
317
+ error,
318
+ rejectedCount: rejections.length,
319
+ rejections: rejections.slice(0, 3)
320
+ };
321
+ }
322
+ }
323
+
324
+ module.exports = {
325
+ TRACE_ENABLE_ENV,
326
+ DECISION_FAMILIES,
327
+ DECISION_KEYS_BY_FAMILY,
328
+ OUTCOMES,
329
+ formatPathRef,
330
+ isWorkflowDecisionTracingEnabled,
331
+ isEligibleWorkflowTraceSurface,
332
+ shouldTraceWorkflowDecisions,
333
+ resolveWorkflowDecisionTracePath,
334
+ emitWorkflowDecisionTraces
335
+ };
@@ -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;
@@ -1472,7 +1621,7 @@ function buildEffectiveTaskGroupState(group, planned, implementer, review) {
1472
1621
  return effective;
1473
1622
  }
1474
1623
 
1475
- function deriveTaskGroupRuntimeState(plannedTaskGroups, signals, seedTaskGroups) {
1624
+ function deriveTaskGroupRuntimeState(plannedTaskGroups, signals, seedTaskGroups, decisionTraceRecords) {
1476
1625
  const plannedGroups = Array.isArray(plannedTaskGroups) ? plannedTaskGroups : [];
1477
1626
  const seedMap = normalizeTaskGroupSeedMap(seedTaskGroups);
1478
1627
 
@@ -1490,6 +1639,22 @@ function deriveTaskGroupRuntimeState(plannedTaskGroups, signals, seedTaskGroups)
1490
1639
  const implementer = buildTaskGroupImplementerState(taskGroupId, signals, seed.implementer);
1491
1640
  const review = buildTaskGroupReviewState(plannedGroup, signals, seed.review);
1492
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
+ }
1493
1658
 
1494
1659
  return {
1495
1660
  taskGroupId,
@@ -1541,7 +1706,13 @@ function loadTaskGroupMetadataFromPath(targetPath) {
1541
1706
  }
1542
1707
  }
1543
1708
 
1544
- function resolvePersistedTaskGroupSeed(projectRoot, changeId, persistedRecord, plannedTaskGroups) {
1709
+ function resolvePersistedTaskGroupSeed(
1710
+ projectRoot,
1711
+ changeId,
1712
+ persistedRecord,
1713
+ plannedTaskGroups,
1714
+ decisionTraceRecords
1715
+ ) {
1545
1716
  const metadataRefs =
1546
1717
  persistedRecord && persistedRecord.metadataRefs && typeof persistedRecord.metadataRefs === "object"
1547
1718
  ? persistedRecord.metadataRefs
@@ -1554,7 +1725,21 @@ function resolvePersistedTaskGroupSeed(projectRoot, changeId, persistedRecord, p
1554
1725
  const actualDigest = digestForPath(canonicalPath);
1555
1726
  const expectedDigest = metadataRefs.taskGroupsDigest || null;
1556
1727
  if (expectedDigest && actualDigest && expectedDigest !== actualDigest) {
1557
- 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
+ });
1558
1743
  return {
1559
1744
  taskGroups: plannedTaskGroups,
1560
1745
  notes
@@ -1570,7 +1755,22 @@ function resolvePersistedTaskGroupSeed(projectRoot, changeId, persistedRecord, p
1570
1755
  notes
1571
1756
  };
1572
1757
  }
1573
- 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
+ }
1574
1774
  return {
1575
1775
  taskGroups: plannedTaskGroups,
1576
1776
  notes
@@ -1578,14 +1778,45 @@ function resolvePersistedTaskGroupSeed(projectRoot, changeId, persistedRecord, p
1578
1778
  }
1579
1779
 
1580
1780
  if (Array.isArray(persistedRecord && persistedRecord.taskGroups) && persistedRecord.taskGroups.length > 0) {
1581
- 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
+ }
1582
1796
  return {
1583
1797
  taskGroups: persistedRecord.taskGroups,
1584
1798
  notes
1585
1799
  };
1586
1800
  }
1587
1801
 
1588
- 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
+ }
1589
1820
  return {
1590
1821
  taskGroups: plannedTaskGroups,
1591
1822
  notes
@@ -1632,14 +1863,23 @@ function finalizeWorkflowView(options = {}) {
1632
1863
  stalePlanningSignals: {},
1633
1864
  needsRerunSurfaces: []
1634
1865
  };
1866
+ const decisionTraceRecords = Array.isArray(options.decisionTraceRecords)
1867
+ ? options.decisionTraceRecords
1868
+ : null;
1635
1869
  const taskGroups = deriveTaskGroupRuntimeState(
1636
1870
  options.plannedTaskGroups,
1637
1871
  options.changeSignals,
1638
- options.taskGroupSeed
1872
+ options.taskGroupSeed,
1873
+ decisionTraceRecords
1639
1874
  );
1640
1875
 
1641
1876
  stageId = applyAuditFindings(stageId, findings, integrityAudit, completionAudit);
1642
- stageId = applyPlanningSignalFreshnessFindings(stageId, findings, planningSignalFreshness);
1877
+ stageId = applyPlanningSignalFreshnessFindings(
1878
+ stageId,
1879
+ findings,
1880
+ planningSignalFreshness,
1881
+ decisionTraceRecords
1882
+ );
1643
1883
  stageId = applyExecutionSignalFindings(stageId, findings, planningSignalFreshness.effectiveSignalSummary || {});
1644
1884
  applyTaskExecutionAndReviewFindings(findings, options.changeSignals || []);
1645
1885
 
@@ -1653,10 +1893,31 @@ function finalizeWorkflowView(options = {}) {
1653
1893
  }
1654
1894
 
1655
1895
  if (verificationFreshness && !verificationFreshness.fresh && (stageId === "verify" || stageId === "complete")) {
1896
+ const stageBeforeFreshness = stageId;
1656
1897
  findings.blockers.push(
1657
1898
  "Completion-facing routing requires fresh verification evidence; stale evidence keeps the route in verify."
1658
1899
  );
1659
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
+ }
1660
1921
  }
1661
1922
 
1662
1923
  const gates = buildGatesWithLiveOverlays(
@@ -1687,6 +1948,26 @@ function finalizeWorkflowView(options = {}) {
1687
1948
  findings.warnings.push(
1688
1949
  "Bounded-parallel profile downgraded to serial until worktree isolation is ready or explicitly accepted."
1689
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
+ });
1690
1971
  }
1691
1972
  }
1692
1973
 
@@ -1767,23 +2048,30 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
1767
2048
 
1768
2049
  if (!pathExists(projectRoot)) {
1769
2050
  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: {
2051
+ return finalizeResultWithWorkflowDecisionTracing(
2052
+ buildWorkflowResult({
1782
2053
  projectRoot,
1783
- changeId: requestedChangeId || "change-001",
1784
- 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
1785
2073
  }
1786
- });
2074
+ );
1787
2075
  }
1788
2076
 
1789
2077
  const workflowRoot = path.join(projectRoot, ".da-vinci");
@@ -1820,6 +2108,13 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
1820
2108
  }
1821
2109
 
1822
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;
1823
2118
  const artifactState = {
1824
2119
  workflowRootReady: pathExists(workflowRoot),
1825
2120
  changeSelected: Boolean(activeChangeDir),
@@ -1885,6 +2180,26 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
1885
2180
  staleWindowMs: options.staleWindowMs
1886
2181
  });
1887
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
+ });
1888
2203
  const persistedRecord = persistedSelection.changeRecord;
1889
2204
  const stageRecord = getStageById(persistedRecord.stage) || getStageById("bootstrap");
1890
2205
  const completionAudit =
@@ -1895,40 +2210,49 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
1895
2210
  projectRoot,
1896
2211
  activeChangeId,
1897
2212
  persistedRecord,
1898
- plannedTaskGroups
1899
- );
1900
- return finalizeWorkflowView({
1901
- projectRoot,
1902
- changeId: activeChangeId,
1903
- stageId: stageRecord.id,
1904
- findings: {
1905
- blockers: Array.isArray(persistedRecord.failures) ? persistedRecord.failures.slice() : [],
1906
- warnings: Array.isArray(persistedRecord.warnings) ? persistedRecord.warnings.slice() : [],
1907
- notes: [
1908
- ...sanitizePersistedNotes(persistedRecord.notes),
1909
- ...(Array.isArray(persistedSelection.advisoryNotes) ? persistedSelection.advisoryNotes : []),
1910
- ...persistedSeed.notes,
1911
- "workflow-status is using trusted persisted workflow state."
1912
- ]
1913
- },
1914
- baseGates:
1915
- persistedRecord && persistedRecord.gates && typeof persistedRecord.gates === "object"
1916
- ? { ...persistedRecord.gates }
1917
- : {},
1918
- checkpoints: checkpointStatuses,
1919
- routeContext,
1920
- source: "persisted",
1921
- taskGroupSeed: persistedSeed.taskGroups,
1922
2213
  plannedTaskGroups,
1923
- changeSignals,
1924
- signalSummary,
1925
- planningSignalFreshness,
1926
- integrityAudit,
1927
- completionAudit,
1928
- disciplineState,
1929
- verificationFreshness,
1930
- hasTasksArtifact: artifactState.tasks
1931
- });
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
+ );
1932
2256
  }
1933
2257
 
1934
2258
  if (!persistedSelection.usable && persistedSelection.reason) {
@@ -1942,6 +2266,22 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
1942
2266
  const message = reasonMessage[persistedSelection.reason];
1943
2267
  if (message) {
1944
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
+ });
1945
2285
  }
1946
2286
  }
1947
2287
  }
@@ -1999,7 +2339,8 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
1999
2339
  completionAudit,
2000
2340
  disciplineState,
2001
2341
  verificationFreshness,
2002
- hasTasksArtifact: artifactState.tasks
2342
+ hasTasksArtifact: artifactState.tasks,
2343
+ decisionTraceRecords
2003
2344
  });
2004
2345
 
2005
2346
  if (activeChangeId && !ambiguousChangeSelection) {
@@ -2069,7 +2410,11 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
2069
2410
  ]);
2070
2411
  }
2071
2412
 
2072
- return derivedResult;
2413
+ return finalizeResultWithWorkflowDecisionTracing(derivedResult, {
2414
+ env: options.env,
2415
+ surface: options.traceSurface,
2416
+ records: decisionTraceRecords
2417
+ });
2073
2418
  }
2074
2419
 
2075
2420
  function buildWorkflowResult(params) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenonbyte/da-vinci-workflow",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Requirement-to-design-to-code workflow skill for Codex, Claude, and Gemini",
5
5
  "bin": {
6
6
  "da-vinci": "bin/da-vinci.js",