@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.
- package/CHANGELOG.md +33 -0
- package/README.md +7 -7
- package/README.zh-CN.md +7 -7
- package/docs/dv-command-reference.md +3 -1
- package/docs/zh-CN/dv-command-reference.md +3 -1
- package/lib/cli.js +33 -5
- package/lib/isolated-worker-handoff.js +181 -0
- package/lib/supervisor-review.js +117 -6
- package/lib/task-execution.js +88 -16
- package/lib/task-review.js +12 -7
- package/lib/workflow-decision-trace.js +335 -0
- package/lib/workflow-state.js +462 -63
- package/package.json +3 -2
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;
|
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
1717
|
-
|
|
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:
|
|
1730
|
-
|
|
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
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
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) {
|