@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 +17 -0
- package/README.md +7 -7
- package/README.zh-CN.md +7 -7
- package/lib/cli.js +12 -3
- package/lib/workflow-decision-trace.js +335 -0
- package/lib/workflow-state.js +408 -63
- package/package.json +1 -1
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.
|
|
37
|
+
- `@xenonbyte/da-vinci-workflow@0.2.8`
|
|
38
38
|
|
|
39
|
-
Release highlights for `0.2.
|
|
39
|
+
Release highlights for `0.2.8`:
|
|
40
40
|
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
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.
|
|
40
|
+
- `@xenonbyte/da-vinci-workflow@0.2.8`
|
|
41
41
|
|
|
42
|
-
`0.2.
|
|
42
|
+
`0.2.8` 版本重点:
|
|
43
43
|
|
|
44
|
-
- `
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
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 downgrade 与 worktree-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, {
|
|
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, {
|
|
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
|
+
};
|
package/lib/workflow-state.js
CHANGED
|
@@ -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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
1771
|
-
|
|
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:
|
|
1784
|
-
|
|
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
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
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) {
|