@xenonbyte/da-vinci-workflow 0.2.5 → 0.2.7

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.
@@ -7,7 +7,7 @@ const {
7
7
  parseDisciplineMarkers,
8
8
  DISCIPLINE_MARKER_NAMES
9
9
  } = require("./audit-parsers");
10
- const { pathExists, readTextIfExists } = require("./utils");
10
+ const { pathExists, readTextIfExists, dedupeMessages } = require("./utils");
11
11
  const {
12
12
  parseTasksArtifact,
13
13
  listImmediateDirs,
@@ -25,8 +25,10 @@ const {
25
25
  const {
26
26
  selectPersistedStateForChange,
27
27
  persistDerivedWorkflowResult,
28
+ resolveTaskGroupMetadataPath,
28
29
  writeTaskGroupMetadata,
29
30
  readTaskGroupMetadata,
31
+ digestForPath,
30
32
  sanitizePersistedNotes
31
33
  } = require("./workflow-persisted-state");
32
34
  const {
@@ -34,11 +36,30 @@ const {
34
36
  summarizeSignalsBySurface,
35
37
  listSignalsBySurfacePrefix
36
38
  } = require("./execution-signals");
39
+ const { evaluatePlanningSignalFreshness } = require("./planning-signal-freshness");
37
40
  const { deriveExecutionProfile } = require("./execution-profile");
38
41
  const { collectVerificationFreshness } = require("./verify");
39
42
  const { runWorktreePreflight } = require("./worktree-preflight");
40
43
 
41
44
  const MAX_REPORTED_MESSAGES = 3;
45
+ // Task-group metadata is versioned independently from workflow route snapshots.
46
+ const TASK_GROUP_METADATA_VERSION = 2;
47
+ const BLOCKING_GATE_PRIORITY = Object.freeze([
48
+ "clarify",
49
+ "scenarioQuality",
50
+ "analyze",
51
+ "taskCheckpoint",
52
+ "stalePlanningSignal",
53
+ "principleInheritance",
54
+ "lint-tasks",
55
+ "lint-spec",
56
+ "scope-check"
57
+ ]);
58
+ const PLANNING_SIGNAL_PROMOTION_FALLBACKS = Object.freeze({
59
+ "lint-spec": "breakdown",
60
+ "scope-check": "tasks",
61
+ "lint-tasks": "tasks"
62
+ });
42
63
 
43
64
  function summarizeAudit(result) {
44
65
  if (!result) {
@@ -53,10 +74,6 @@ function summarizeAudit(result) {
53
74
  };
54
75
  }
55
76
 
56
- function dedupeMessages(items) {
57
- return Array.from(new Set((items || []).filter(Boolean)));
58
- }
59
-
60
77
  function dedupeFindings(findings) {
61
78
  findings.blockers = dedupeMessages(findings.blockers);
62
79
  findings.warnings = dedupeMessages(findings.warnings);
@@ -71,6 +88,212 @@ function resolveDisciplineGateMode() {
71
88
  };
72
89
  }
73
90
 
91
+ function isStrictPromotionEnabled() {
92
+ const raw = String(process.env.DA_VINCI_DISCIPLINE_STRICT_PROMOTION || "").trim();
93
+ return raw === "1" || /^true$/i.test(raw);
94
+ }
95
+
96
+ function fallbackStageIfBeyond(currentStageId, targetStageId) {
97
+ const current = getStageById(currentStageId);
98
+ const target = getStageById(targetStageId);
99
+ if (!current || !target) {
100
+ return currentStageId;
101
+ }
102
+ if (current.order > target.order) {
103
+ return target.id;
104
+ }
105
+ return current.id;
106
+ }
107
+
108
+ function ensureGateTracking(findings) {
109
+ if (!Array.isArray(findings.blockingGates)) {
110
+ findings.blockingGates = [];
111
+ }
112
+ }
113
+
114
+ function collectGateEvidenceRefs(gate) {
115
+ return Array.isArray(gate && gate.evidence) ? gate.evidence.slice(0, MAX_REPORTED_MESSAGES) : [];
116
+ }
117
+
118
+ function addBlockingGateRecord(findings, gateId, surface, gate, reason) {
119
+ ensureGateTracking(findings);
120
+ findings.blockingGates.push({
121
+ id: gateId,
122
+ surface,
123
+ reason: String(reason || "").trim(),
124
+ evidence: collectGateEvidenceRefs(gate)
125
+ });
126
+ }
127
+
128
+ function collectPlanningSignalFreshnessState(projectRoot, changeId, signalSummary) {
129
+ const effectiveSignalSummary =
130
+ signalSummary && typeof signalSummary === "object"
131
+ ? { ...signalSummary }
132
+ : {};
133
+ const stalePlanningSignals = {};
134
+
135
+ if (!changeId) {
136
+ return {
137
+ effectiveSignalSummary,
138
+ stalePlanningSignals,
139
+ needsRerunSurfaces: []
140
+ };
141
+ }
142
+
143
+ for (const surface of Object.keys(PLANNING_SIGNAL_PROMOTION_FALLBACKS)) {
144
+ const signal = effectiveSignalSummary[surface];
145
+ if (!signal) {
146
+ continue;
147
+ }
148
+ const freshness = evaluatePlanningSignalFreshness(projectRoot, {
149
+ changeId,
150
+ surface,
151
+ signal
152
+ });
153
+ if (!freshness.applicable || freshness.fresh) {
154
+ continue;
155
+ }
156
+ stalePlanningSignals[surface] = {
157
+ surface,
158
+ reasons: Array.isArray(freshness.reasons) ? freshness.reasons.slice() : [],
159
+ signalStatus: normalizeSignalStatus(signal.status),
160
+ signalTimestamp: signal && signal.timestamp ? String(signal.timestamp) : null,
161
+ signalTimestampMs: freshness.signalTimestampMs,
162
+ latestArtifactMtimeMs: freshness.latestArtifactMtimeMs,
163
+ latestArtifactTimestamp:
164
+ freshness.latestArtifactMtimeMs > 0 ? new Date(freshness.latestArtifactMtimeMs).toISOString() : null,
165
+ staleByMs: freshness.staleByMs
166
+ };
167
+ delete effectiveSignalSummary[surface];
168
+ }
169
+
170
+ return {
171
+ effectiveSignalSummary,
172
+ stalePlanningSignals,
173
+ needsRerunSurfaces: Object.keys(stalePlanningSignals).sort()
174
+ };
175
+ }
176
+
177
+ function applyPlanningSignalFreshnessFindings(stageId, findings, planningSignalFreshness) {
178
+ let nextStageId = stageId;
179
+ const strictPromotion = isStrictPromotionEnabled();
180
+ const stalePlanningSignals =
181
+ planningSignalFreshness && planningSignalFreshness.stalePlanningSignals
182
+ ? planningSignalFreshness.stalePlanningSignals
183
+ : {};
184
+
185
+ for (const surface of Object.keys(stalePlanningSignals).sort()) {
186
+ const freshness = stalePlanningSignals[surface];
187
+ const reasonText =
188
+ Array.isArray(freshness.reasons) && freshness.reasons.length > 0
189
+ ? freshness.reasons.join(", ")
190
+ : "unknown_staleness_reason";
191
+ findings.warnings.push(
192
+ `Stale ${surface} planning signal requires rerun before routing can rely on it (${reasonText}).`
193
+ );
194
+ if (!strictPromotion) {
195
+ continue;
196
+ }
197
+ findings.blockers.push(
198
+ `DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; stale ${surface} planning signal blocks promotion until ${surface} is rerun.`
199
+ );
200
+ addBlockingGateRecord(
201
+ findings,
202
+ "stalePlanningSignal",
203
+ surface,
204
+ null,
205
+ `strict promotion requires rerun for stale ${surface} planning signal`
206
+ );
207
+ nextStageId = fallbackStageIfBeyond(
208
+ nextStageId,
209
+ PLANNING_SIGNAL_PROMOTION_FALLBACKS[surface] || nextStageId
210
+ );
211
+ }
212
+
213
+ return nextStageId;
214
+ }
215
+
216
+ function selectBlockingGateIdentity(findings) {
217
+ const candidates = Array.isArray(findings && findings.blockingGates) ? findings.blockingGates : [];
218
+ if (candidates.length === 0) {
219
+ return null;
220
+ }
221
+ const sorted = candidates
222
+ .slice()
223
+ .sort((left, right) => {
224
+ const leftPriority = BLOCKING_GATE_PRIORITY.indexOf(left.id);
225
+ const rightPriority = BLOCKING_GATE_PRIORITY.indexOf(right.id);
226
+ const normalizedLeft = leftPriority === -1 ? Number.MAX_SAFE_INTEGER : leftPriority;
227
+ const normalizedRight = rightPriority === -1 ? Number.MAX_SAFE_INTEGER : rightPriority;
228
+ if (normalizedLeft !== normalizedRight) {
229
+ return normalizedLeft - normalizedRight;
230
+ }
231
+ return String(left.surface || "").localeCompare(String(right.surface || ""));
232
+ });
233
+ return sorted[0];
234
+ }
235
+
236
+ function getSignalGate(signal, gateKey) {
237
+ if (!signal || !signal.details || !signal.details.gates) {
238
+ return null;
239
+ }
240
+ const gates = signal.details.gates;
241
+ if (!gates || typeof gates !== "object") {
242
+ return null;
243
+ }
244
+ if (!gateKey) {
245
+ return null;
246
+ }
247
+ return gates[gateKey] && typeof gates[gateKey] === "object" ? gates[gateKey] : null;
248
+ }
249
+
250
+ function normalizeSignalStatus(status) {
251
+ const normalized = String(status || "").trim().toUpperCase();
252
+ if (normalized === STATUS.BLOCK || normalized === STATUS.WARN || normalized === STATUS.PASS) {
253
+ return normalized;
254
+ }
255
+ return "";
256
+ }
257
+
258
+ function statusSeverity(status) {
259
+ if (status === STATUS.BLOCK) {
260
+ return 2;
261
+ }
262
+ if (status === STATUS.WARN) {
263
+ return 1;
264
+ }
265
+ if (status === STATUS.PASS) {
266
+ return 0;
267
+ }
268
+ return -1;
269
+ }
270
+
271
+ function clampGateStatusBySignal(gateStatus, signalStatus) {
272
+ const normalizedGate = normalizeSignalStatus(gateStatus);
273
+ const normalizedSignal = normalizeSignalStatus(signalStatus);
274
+ if (!normalizedGate && normalizedSignal) {
275
+ return normalizedSignal;
276
+ }
277
+ if (normalizedGate && !normalizedSignal) {
278
+ return normalizedGate;
279
+ }
280
+ if (!normalizedGate && !normalizedSignal) {
281
+ return STATUS.PASS;
282
+ }
283
+ return statusSeverity(normalizedGate) >= statusSeverity(normalizedSignal)
284
+ ? normalizedGate
285
+ : normalizedSignal;
286
+ }
287
+
288
+ function resolveEffectiveGateStatus(gate, signal) {
289
+ const gateStatus = normalizeSignalStatus(gate && gate.status);
290
+ if (gateStatus) {
291
+ return gateStatus;
292
+ }
293
+ const signalStatus = normalizeSignalStatus(signal && signal.status);
294
+ return signalStatus || STATUS.PASS;
295
+ }
296
+
74
297
  function statusTokenMatches(status, acceptedTokens) {
75
298
  const normalized = String(status || "").trim().toUpperCase();
76
299
  return acceptedTokens.includes(normalized);
@@ -213,6 +436,10 @@ function applyTaskExecutionAndReviewFindings(findings, signals) {
213
436
 
214
437
  for (const signal of Object.values(latestTaskExecution)) {
215
438
  const envelope = signal.details && signal.details.envelope ? signal.details.envelope : null;
439
+ const outOfScopeWrites =
440
+ signal.details && Array.isArray(signal.details.outOfScopeWrites)
441
+ ? dedupeMessages(signal.details.outOfScopeWrites.map((item) => String(item || "").trim()).filter(Boolean))
442
+ : [];
216
443
  const taskGroupId =
217
444
  (envelope && envelope.taskGroupId) ||
218
445
  String(signal.surface || "").replace(/^task-execution\./, "") ||
@@ -222,6 +449,11 @@ function applyTaskExecutionAndReviewFindings(findings, signals) {
222
449
  } else if (signal.status === STATUS.WARN) {
223
450
  findings.warnings.push(`Task group ${taskGroupId} has unresolved implementer concerns/context needs.`);
224
451
  }
452
+ if (outOfScopeWrites.length > 0) {
453
+ findings.blockers.push(
454
+ `Task group ${taskGroupId} reported out-of-scope writes: ${outOfScopeWrites.join(", ")}.`
455
+ );
456
+ }
225
457
  if (envelope && envelope.summary) {
226
458
  findings.notes.push(`Implementer summary ${taskGroupId}: ${envelope.summary}`);
227
459
  }
@@ -247,9 +479,21 @@ function applyTaskExecutionAndReviewFindings(findings, signals) {
247
479
  }
248
480
 
249
481
  for (const [taskGroupId, state] of Object.entries(reviewStateByGroup)) {
250
- if (state.quality && state.quality === STATUS.PASS && state.spec !== STATUS.PASS) {
482
+ if (state.quality && !state.spec) {
483
+ findings.blockers.push(
484
+ `Task review ordering violation for ${taskGroupId}: quality review exists without a prior spec review result.`
485
+ );
486
+ continue;
487
+ }
488
+ if (state.quality && state.spec === STATUS.WARN) {
489
+ findings.blockers.push(
490
+ `Task review ordering violation for ${taskGroupId}: quality review was recorded before spec review reached PASS.`
491
+ );
492
+ continue;
493
+ }
494
+ if (state.quality && state.spec === STATUS.BLOCK) {
251
495
  findings.blockers.push(
252
- `Task review ordering violation for ${taskGroupId}: quality review passed before spec review PASS.`
496
+ `Task review ordering violation for ${taskGroupId}: quality review was recorded while spec review is BLOCK.`
253
497
  );
254
498
  }
255
499
  }
@@ -321,6 +565,7 @@ function applyAuditFindings(stageId, findings, integrityAudit, completionAudit)
321
565
 
322
566
  function applyExecutionSignalFindings(stageId, findings, signalSummary) {
323
567
  let nextStageId = stageId;
568
+ const strictPromotion = isStrictPromotionEnabled();
324
569
  const signalParseIssue = signalSummary["signal-file-parse"];
325
570
  if (signalParseIssue) {
326
571
  const warningText =
@@ -335,22 +580,242 @@ function applyExecutionSignalFindings(stageId, findings, signalSummary) {
335
580
  findings.warnings.push(`Planning diff signal diff-spec reports ${diffSignal.status}.`);
336
581
  }
337
582
 
338
- const lintTasksSignal = signalSummary["lint-tasks"];
339
- const strictPromotion =
340
- String(process.env.DA_VINCI_DISCIPLINE_STRICT_PROMOTION || "").trim() === "1" ||
341
- /^true$/i.test(String(process.env.DA_VINCI_DISCIPLINE_STRICT_PROMOTION || "").trim());
342
- if (lintTasksSignal && lintTasksSignal.status === STATUS.BLOCK) {
343
- findings.blockers.push("lint-tasks signal is BLOCK and prevents promotion into build.");
344
- if (nextStageId === "build" || nextStageId === "verify" || nextStageId === "complete") {
345
- nextStageId = "tasks";
583
+ const lintSpecSignal = signalSummary["lint-spec"];
584
+ const lintSpecGateConfigs = [
585
+ { id: "principleInheritance", label: "principleInheritance", fallbackStage: "breakdown" },
586
+ { id: "clarify", label: "clarify", fallbackStage: "breakdown" },
587
+ { id: "scenarioQuality", label: "scenarioQuality", fallbackStage: "breakdown" }
588
+ ];
589
+ let lintSpecGateObserved = false;
590
+ let strongestLintSpecGateStatus = "";
591
+ for (const config of lintSpecGateConfigs) {
592
+ const gate = getSignalGate(lintSpecSignal, config.id);
593
+ if (!gate) {
594
+ continue;
595
+ }
596
+ lintSpecGateObserved = true;
597
+ const gateStatus = resolveEffectiveGateStatus(gate, lintSpecSignal);
598
+ if (statusSeverity(gateStatus) > statusSeverity(strongestLintSpecGateStatus)) {
599
+ strongestLintSpecGateStatus = gateStatus;
600
+ }
601
+ const evidenceRefs = collectGateEvidenceRefs(gate);
602
+ const evidenceSuffix =
603
+ evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
604
+ if (gateStatus === STATUS.BLOCK) {
605
+ findings.blockers.push(
606
+ `lint-spec gate ${config.label} is BLOCK and prevents planning promotion.${evidenceSuffix}`
607
+ );
608
+ addBlockingGateRecord(
609
+ findings,
610
+ config.id,
611
+ "lint-spec",
612
+ gate,
613
+ `lint-spec gate ${config.label} is BLOCK`
614
+ );
615
+ nextStageId = fallbackStageIfBeyond(nextStageId, config.fallbackStage);
616
+ } else if (gateStatus === STATUS.WARN) {
617
+ findings.warnings.push(`lint-spec gate ${config.label} is WARN.${evidenceSuffix}`);
618
+ if (strictPromotion) {
619
+ findings.blockers.push(
620
+ `DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-spec gate ${config.label} WARN blocks promotion.`
621
+ );
622
+ addBlockingGateRecord(
623
+ findings,
624
+ config.id,
625
+ "lint-spec",
626
+ gate,
627
+ `strict promotion escalated lint-spec gate ${config.label} WARN`
628
+ );
629
+ nextStageId = fallbackStageIfBeyond(nextStageId, config.fallbackStage);
630
+ }
631
+ }
632
+ for (const message of Array.isArray(gate.compatibility) ? gate.compatibility : []) {
633
+ findings.notes.push(`lint-spec gate ${config.label} compatibility: ${message}`);
634
+ }
635
+ if (config.id === "clarify") {
636
+ for (const bounded of Array.isArray(gate.bounded) ? gate.bounded : []) {
637
+ findings.notes.push(`lint-spec gate clarify bounded ambiguity: ${bounded}`);
638
+ }
639
+ }
640
+ }
641
+ const lintSpecSignalStatus = normalizeSignalStatus(lintSpecSignal && lintSpecSignal.status);
642
+ if (
643
+ lintSpecSignal &&
644
+ (!lintSpecGateObserved || statusSeverity(lintSpecSignalStatus) > statusSeverity(strongestLintSpecGateStatus))
645
+ ) {
646
+ if (lintSpecSignalStatus === STATUS.BLOCK) {
647
+ findings.blockers.push("lint-spec signal is BLOCK.");
648
+ addBlockingGateRecord(
649
+ findings,
650
+ "lint-spec",
651
+ "lint-spec",
652
+ lintSpecSignal.details && lintSpecSignal.details.gates ? lintSpecSignal.details.gates : null,
653
+ "lint-spec signal is BLOCK"
654
+ );
655
+ nextStageId = fallbackStageIfBeyond(nextStageId, "breakdown");
656
+ } else if (lintSpecSignalStatus === STATUS.WARN) {
657
+ findings.warnings.push("lint-spec signal is WARN.");
658
+ if (strictPromotion) {
659
+ findings.blockers.push(
660
+ "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-spec WARN blocks promotion."
661
+ );
662
+ addBlockingGateRecord(
663
+ findings,
664
+ "lint-spec",
665
+ "lint-spec",
666
+ lintSpecSignal.details && lintSpecSignal.details.gates ? lintSpecSignal.details.gates : null,
667
+ "strict promotion escalated lint-spec WARN"
668
+ );
669
+ nextStageId = fallbackStageIfBeyond(nextStageId, "breakdown");
670
+ }
671
+ }
672
+ }
673
+
674
+ const analyzeSignal = signalSummary["scope-check"];
675
+ const analyzeGate = getSignalGate(analyzeSignal, "analyze");
676
+ let analyzeGateStatus = "";
677
+ if (analyzeGate) {
678
+ const evidenceRefs = collectGateEvidenceRefs(analyzeGate);
679
+ const evidenceSuffix =
680
+ evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
681
+ analyzeGateStatus = resolveEffectiveGateStatus(analyzeGate, analyzeSignal);
682
+ if (analyzeGateStatus === STATUS.BLOCK) {
683
+ findings.blockers.push(`scope-check gate analyze is BLOCK.${evidenceSuffix}`);
684
+ addBlockingGateRecord(
685
+ findings,
686
+ "analyze",
687
+ "scope-check",
688
+ analyzeGate,
689
+ "scope-check gate analyze is BLOCK"
690
+ );
691
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
692
+ } else if (analyzeGateStatus === STATUS.WARN) {
693
+ findings.warnings.push(`scope-check gate analyze is WARN.${evidenceSuffix}`);
694
+ if (strictPromotion) {
695
+ findings.blockers.push(
696
+ "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; scope-check gate analyze WARN blocks promotion."
697
+ );
698
+ addBlockingGateRecord(
699
+ findings,
700
+ "analyze",
701
+ "scope-check",
702
+ analyzeGate,
703
+ "strict promotion escalated scope-check gate analyze WARN"
704
+ );
705
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
706
+ }
707
+ }
708
+ for (const message of Array.isArray(analyzeGate.compatibility) ? analyzeGate.compatibility : []) {
709
+ findings.notes.push(`scope-check gate analyze compatibility: ${message}`);
346
710
  }
347
- } else if (lintTasksSignal && lintTasksSignal.status === STATUS.WARN) {
348
- findings.warnings.push("lint-tasks signal is WARN.");
349
- if (strictPromotion && (nextStageId === "build" || nextStageId === "verify" || nextStageId === "complete")) {
711
+ }
712
+ const analyzeSignalStatus = normalizeSignalStatus(analyzeSignal && analyzeSignal.status);
713
+ if (
714
+ analyzeSignal &&
715
+ (!analyzeGate || statusSeverity(analyzeSignalStatus) > statusSeverity(analyzeGateStatus))
716
+ ) {
717
+ if (analyzeSignalStatus === STATUS.BLOCK) {
718
+ findings.blockers.push("scope-check signal is BLOCK.");
719
+ addBlockingGateRecord(
720
+ findings,
721
+ "scope-check",
722
+ "scope-check",
723
+ analyzeSignal.details && analyzeSignal.details.gates ? analyzeSignal.details.gates : null,
724
+ "scope-check signal is BLOCK"
725
+ );
726
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
727
+ } else if (analyzeSignalStatus === STATUS.WARN) {
728
+ findings.warnings.push("scope-check signal is WARN.");
729
+ if (strictPromotion) {
730
+ findings.blockers.push(
731
+ "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; scope-check WARN blocks promotion."
732
+ );
733
+ addBlockingGateRecord(
734
+ findings,
735
+ "scope-check",
736
+ "scope-check",
737
+ analyzeSignal.details && analyzeSignal.details.gates ? analyzeSignal.details.gates : null,
738
+ "strict promotion escalated scope-check WARN"
739
+ );
740
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
741
+ }
742
+ }
743
+ }
744
+
745
+ const lintTasksSignal = signalSummary["lint-tasks"];
746
+ const taskCheckpointGate = getSignalGate(lintTasksSignal, "taskCheckpoint");
747
+ const taskCheckpointGateStatus = taskCheckpointGate
748
+ ? resolveEffectiveGateStatus(taskCheckpointGate, lintTasksSignal)
749
+ : "";
750
+ const lintTasksSignalStatus = normalizeSignalStatus(lintTasksSignal && lintTasksSignal.status);
751
+ if (taskCheckpointGate && taskCheckpointGateStatus === STATUS.BLOCK) {
752
+ const evidenceRefs = collectGateEvidenceRefs(taskCheckpointGate);
753
+ const evidenceSuffix =
754
+ evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
755
+ findings.blockers.push(`lint-tasks task-checkpoint is BLOCK and prevents promotion into build.${evidenceSuffix}`);
756
+ addBlockingGateRecord(
757
+ findings,
758
+ "taskCheckpoint",
759
+ "lint-tasks",
760
+ taskCheckpointGate,
761
+ "lint-tasks task-checkpoint is BLOCK"
762
+ );
763
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
764
+ } else if (taskCheckpointGate && taskCheckpointGateStatus === STATUS.WARN) {
765
+ const evidenceRefs = collectGateEvidenceRefs(taskCheckpointGate);
766
+ const evidenceSuffix =
767
+ evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
768
+ findings.warnings.push(`lint-tasks task-checkpoint is WARN.${evidenceSuffix}`);
769
+ if (strictPromotion) {
350
770
  findings.blockers.push(
351
771
  "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-tasks WARN blocks promotion into build."
352
772
  );
353
- nextStageId = "tasks";
773
+ addBlockingGateRecord(
774
+ findings,
775
+ "taskCheckpoint",
776
+ "lint-tasks",
777
+ taskCheckpointGate,
778
+ "strict promotion escalated lint-tasks task-checkpoint WARN"
779
+ );
780
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
781
+ }
782
+ }
783
+ if (
784
+ lintTasksSignal &&
785
+ (!taskCheckpointGate || statusSeverity(lintTasksSignalStatus) > statusSeverity(taskCheckpointGateStatus))
786
+ ) {
787
+ if (lintTasksSignalStatus === STATUS.BLOCK) {
788
+ findings.blockers.push("lint-tasks signal is BLOCK.");
789
+ addBlockingGateRecord(
790
+ findings,
791
+ "lint-tasks",
792
+ "lint-tasks",
793
+ lintTasksSignal.details && lintTasksSignal.details.gates ? lintTasksSignal.details.gates : null,
794
+ "lint-tasks signal is BLOCK"
795
+ );
796
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
797
+ } else if (lintTasksSignalStatus === STATUS.WARN) {
798
+ findings.warnings.push("lint-tasks signal is WARN.");
799
+ if (strictPromotion) {
800
+ findings.blockers.push(
801
+ "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-tasks WARN blocks promotion into build."
802
+ );
803
+ addBlockingGateRecord(
804
+ findings,
805
+ "lint-tasks",
806
+ "lint-tasks",
807
+ lintTasksSignal.details && lintTasksSignal.details.gates ? lintTasksSignal.details.gates : null,
808
+ "strict promotion escalated lint-tasks WARN"
809
+ );
810
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
811
+ }
812
+ }
813
+ }
814
+ if (taskCheckpointGate) {
815
+ for (const message of Array.isArray(taskCheckpointGate.compatibility)
816
+ ? taskCheckpointGate.compatibility
817
+ : []) {
818
+ findings.notes.push(`lint-tasks gate taskCheckpoint compatibility: ${message}`);
354
819
  }
355
820
  }
356
821
 
@@ -669,6 +1134,618 @@ function deriveTaskGroupMetadata(tasksMarkdownText, checkpointStatuses) {
669
1134
  return metadata;
670
1135
  }
671
1136
 
1137
+ function normalizeTaskGroupId(value) {
1138
+ return String(value || "").trim();
1139
+ }
1140
+
1141
+ function normalizeResumeCursor(cursor, fallbackGroupIndex = null) {
1142
+ const groupIndex =
1143
+ cursor && Number.isInteger(cursor.groupIndex)
1144
+ ? cursor.groupIndex
1145
+ : Number.isInteger(fallbackGroupIndex)
1146
+ ? fallbackGroupIndex
1147
+ : null;
1148
+ return {
1149
+ groupIndex,
1150
+ nextUncheckedItem:
1151
+ cursor && Object.prototype.hasOwnProperty.call(cursor, "nextUncheckedItem")
1152
+ ? cursor.nextUncheckedItem
1153
+ : null
1154
+ };
1155
+ }
1156
+
1157
+ function normalizeTaskGroupSeedMap(taskGroups) {
1158
+ const byId = new Map();
1159
+ for (const group of Array.isArray(taskGroups) ? taskGroups : []) {
1160
+ const taskGroupId = normalizeTaskGroupId(group && (group.taskGroupId || group.id));
1161
+ if (!taskGroupId || byId.has(taskGroupId)) {
1162
+ continue;
1163
+ }
1164
+ byId.set(taskGroupId, group);
1165
+ }
1166
+ return byId;
1167
+ }
1168
+
1169
+ function findLatestSignalBySurface(signals, surface) {
1170
+ const normalizedSurface = String(surface || "").trim();
1171
+ if (!normalizedSurface) {
1172
+ return null;
1173
+ }
1174
+ return (signals || []).find((signal) => String(signal.surface || "").trim() === normalizedSurface) || null;
1175
+ }
1176
+
1177
+ function summarizeSignalIssues(signal, envelopeItems) {
1178
+ if (!signal) {
1179
+ return [];
1180
+ }
1181
+ const fromEnvelope = Array.isArray(envelopeItems) ? envelopeItems : [];
1182
+ const fromSignal = [
1183
+ ...(Array.isArray(signal.failures) ? signal.failures : []),
1184
+ ...(Array.isArray(signal.warnings) ? signal.warnings : [])
1185
+ ];
1186
+ return dedupeMessages([...fromEnvelope, ...fromSignal].map((item) => String(item || "").trim()).filter(Boolean));
1187
+ }
1188
+
1189
+ function buildTaskGroupImplementerState(taskGroupId, signals, fallbackState) {
1190
+ const fallback = fallbackState && typeof fallbackState === "object" ? fallbackState : {};
1191
+ const signal = findLatestSignalBySurface(signals, `task-execution.${taskGroupId}`);
1192
+ const envelope = signal && signal.details && signal.details.envelope ? signal.details.envelope : null;
1193
+ if (!signal) {
1194
+ return {
1195
+ present: false,
1196
+ signalStatus: null,
1197
+ implementerStatus: fallback.implementerStatus || null,
1198
+ summary: fallback.summary || null,
1199
+ changedFiles: Array.isArray(fallback.changedFiles) ? fallback.changedFiles : [],
1200
+ testEvidence: Array.isArray(fallback.testEvidence) ? fallback.testEvidence : [],
1201
+ concerns: Array.isArray(fallback.concerns) ? fallback.concerns : [],
1202
+ blockers: Array.isArray(fallback.blockers) ? fallback.blockers : [],
1203
+ outOfScopeWrites: Array.isArray(fallback.outOfScopeWrites) ? fallback.outOfScopeWrites : [],
1204
+ recordedAt: fallback.recordedAt || null
1205
+ };
1206
+ }
1207
+
1208
+ return {
1209
+ present: true,
1210
+ signalStatus: signal.status || null,
1211
+ implementerStatus: envelope && envelope.status ? envelope.status : fallback.implementerStatus || null,
1212
+ summary: envelope && envelope.summary ? envelope.summary : fallback.summary || null,
1213
+ changedFiles:
1214
+ envelope && Array.isArray(envelope.changedFiles)
1215
+ ? envelope.changedFiles
1216
+ : Array.isArray(fallback.changedFiles)
1217
+ ? fallback.changedFiles
1218
+ : [],
1219
+ testEvidence:
1220
+ envelope && Array.isArray(envelope.testEvidence)
1221
+ ? envelope.testEvidence
1222
+ : Array.isArray(fallback.testEvidence)
1223
+ ? fallback.testEvidence
1224
+ : [],
1225
+ concerns: summarizeSignalIssues(signal, envelope && envelope.concerns),
1226
+ blockers: summarizeSignalIssues(signal, envelope && envelope.blockers),
1227
+ outOfScopeWrites:
1228
+ signal.details && Array.isArray(signal.details.outOfScopeWrites)
1229
+ ? dedupeMessages(signal.details.outOfScopeWrites.map((item) => String(item || "").trim()).filter(Boolean))
1230
+ : Array.isArray(fallback.outOfScopeWrites)
1231
+ ? fallback.outOfScopeWrites
1232
+ : [],
1233
+ recordedAt: (envelope && envelope.recordedAt) || signal.timestamp || fallback.recordedAt || null
1234
+ };
1235
+ }
1236
+
1237
+ function buildTaskGroupReviewStageState(taskGroupId, stage, signals, fallbackState) {
1238
+ const fallback = fallbackState && typeof fallbackState === "object" ? fallbackState : {};
1239
+ const signal = findLatestSignalBySurface(signals, `task-review.${taskGroupId}.${stage}`);
1240
+ const envelope = signal && signal.details && signal.details.envelope ? signal.details.envelope : null;
1241
+ if (!signal) {
1242
+ return {
1243
+ present: false,
1244
+ status: fallback.status || "missing",
1245
+ summary: fallback.summary || null,
1246
+ reviewer: fallback.reviewer || null,
1247
+ issues: Array.isArray(fallback.issues) ? fallback.issues : [],
1248
+ recordedAt: fallback.recordedAt || null
1249
+ };
1250
+ }
1251
+
1252
+ return {
1253
+ present: true,
1254
+ status: signal.status || "missing",
1255
+ summary: envelope && envelope.summary ? envelope.summary : fallback.summary || null,
1256
+ reviewer: envelope && envelope.reviewer ? envelope.reviewer : fallback.reviewer || null,
1257
+ issues: summarizeSignalIssues(signal, envelope && envelope.issues),
1258
+ recordedAt: (envelope && envelope.recordedAt) || signal.timestamp || fallback.recordedAt || null
1259
+ };
1260
+ }
1261
+
1262
+ function buildTaskGroupReviewState(taskGroup, signals, fallbackState) {
1263
+ const fallback = fallbackState && typeof fallbackState === "object" ? fallbackState : {};
1264
+ const taskGroupId = normalizeTaskGroupId(taskGroup.taskGroupId || taskGroup.id);
1265
+ const required = taskGroup.reviewIntent === true;
1266
+ const spec = buildTaskGroupReviewStageState(taskGroupId, "spec", signals, fallback.spec);
1267
+ const quality = buildTaskGroupReviewStageState(taskGroupId, "quality", signals, fallback.quality);
1268
+
1269
+ return {
1270
+ required,
1271
+ ordering: required ? "spec_then_quality" : "none",
1272
+ spec,
1273
+ quality
1274
+ };
1275
+ }
1276
+
1277
+ function buildEffectiveTaskGroupState(group, planned, implementer, review) {
1278
+ const fallbackCursor = normalizeResumeCursor(planned.resumeCursor, null);
1279
+ const effective = {
1280
+ status: planned.status,
1281
+ nextAction: planned.nextAction,
1282
+ resumeCursor: fallbackCursor,
1283
+ source: "planned",
1284
+ reason: "planned_checklist"
1285
+ };
1286
+
1287
+ if (implementer.present && implementer.outOfScopeWrites.length > 0) {
1288
+ return {
1289
+ status: "blocked",
1290
+ nextAction:
1291
+ `resolve out-of-scope writes for task group ${group.taskGroupId}: ${implementer.outOfScopeWrites.join(", ")}`,
1292
+ resumeCursor: {
1293
+ groupIndex: fallbackCursor.groupIndex,
1294
+ nextUncheckedItem: null,
1295
+ liveFocus: "out_of_scope_write"
1296
+ },
1297
+ source: "implementer",
1298
+ reason: "out_of_scope_write"
1299
+ };
1300
+ }
1301
+
1302
+ if (implementer.present && implementer.signalStatus === STATUS.BLOCK) {
1303
+ return {
1304
+ status: "blocked",
1305
+ nextAction:
1306
+ implementer.blockers[0] ||
1307
+ implementer.summary ||
1308
+ `resolve implementer blocker for task group ${group.taskGroupId}`,
1309
+ resumeCursor: {
1310
+ groupIndex: fallbackCursor.groupIndex,
1311
+ nextUncheckedItem: null,
1312
+ liveFocus: "implementer_block"
1313
+ },
1314
+ source: "implementer",
1315
+ reason: "implementer_block"
1316
+ };
1317
+ }
1318
+
1319
+ const reviewSignalsPresent = review.spec.present || review.quality.present;
1320
+ const plannedCompletion = Number.isFinite(Number(planned.completion))
1321
+ ? Number(planned.completion)
1322
+ : 0;
1323
+ const reviewNearComplete = planned.status !== "completed" && plannedCompletion >= 75;
1324
+ const reviewHardDue = planned.status === "completed" || reviewNearComplete;
1325
+ const reviewContextReady = review.required && (reviewSignalsPresent || reviewHardDue || implementer.present);
1326
+
1327
+ if (reviewContextReady) {
1328
+ if (
1329
+ review.quality.present &&
1330
+ (!review.spec.present || review.spec.status === "missing" || review.spec.status === STATUS.WARN)
1331
+ ) {
1332
+ return {
1333
+ status: "blocked",
1334
+ nextAction:
1335
+ `remove or rerun out-of-order quality review for task group ${group.taskGroupId} after spec review PASS`,
1336
+ resumeCursor: {
1337
+ groupIndex: fallbackCursor.groupIndex,
1338
+ nextUncheckedItem: null,
1339
+ liveFocus: "review_ordering_violation"
1340
+ },
1341
+ source: "review",
1342
+ reason: "review_ordering_violation"
1343
+ };
1344
+ }
1345
+ if (review.spec.status === STATUS.BLOCK) {
1346
+ return {
1347
+ status: "blocked",
1348
+ nextAction:
1349
+ review.spec.issues[0] || review.spec.summary || `resolve spec review BLOCK for task group ${group.taskGroupId}`,
1350
+ resumeCursor: {
1351
+ groupIndex: fallbackCursor.groupIndex,
1352
+ nextUncheckedItem: null,
1353
+ liveFocus: "spec_review_block"
1354
+ },
1355
+ source: "review",
1356
+ reason: "spec_review_block"
1357
+ };
1358
+ }
1359
+ if (!review.spec.present || review.spec.status === "missing") {
1360
+ if (reviewHardDue || reviewSignalsPresent) {
1361
+ return {
1362
+ status: "review_pending",
1363
+ nextAction: `record spec review PASS or WARN for task group ${group.taskGroupId} before quality review`,
1364
+ resumeCursor: {
1365
+ groupIndex: fallbackCursor.groupIndex,
1366
+ nextUncheckedItem: null,
1367
+ liveFocus: "spec_review_missing"
1368
+ },
1369
+ source: "review",
1370
+ reason: "spec_review_missing"
1371
+ };
1372
+ }
1373
+ } else {
1374
+ const specWarn = review.spec.status === STATUS.WARN;
1375
+ if (review.quality.status === STATUS.BLOCK) {
1376
+ return {
1377
+ status: "blocked",
1378
+ nextAction:
1379
+ review.quality.issues[0] ||
1380
+ review.quality.summary ||
1381
+ `resolve quality review BLOCK for task group ${group.taskGroupId}`,
1382
+ resumeCursor: {
1383
+ groupIndex: fallbackCursor.groupIndex,
1384
+ nextUncheckedItem: null,
1385
+ liveFocus: "quality_review_block"
1386
+ },
1387
+ source: "review",
1388
+ reason: "quality_review_block"
1389
+ };
1390
+ }
1391
+ if (!review.quality.present || review.quality.status === "missing") {
1392
+ if (reviewHardDue) {
1393
+ return {
1394
+ status: "review_pending",
1395
+ nextAction: `record quality review PASS or WARN for task group ${group.taskGroupId}`,
1396
+ resumeCursor: {
1397
+ groupIndex: fallbackCursor.groupIndex,
1398
+ nextUncheckedItem: null,
1399
+ liveFocus: "quality_review_missing"
1400
+ },
1401
+ source: "review",
1402
+ reason: "quality_review_missing"
1403
+ };
1404
+ }
1405
+ if (specWarn) {
1406
+ return {
1407
+ status: "in_progress",
1408
+ nextAction:
1409
+ review.spec.issues[0] ||
1410
+ review.spec.summary ||
1411
+ `resolve spec review follow-up for task group ${group.taskGroupId}`,
1412
+ resumeCursor: {
1413
+ groupIndex: fallbackCursor.groupIndex,
1414
+ nextUncheckedItem: null,
1415
+ liveFocus: "spec_review_warn"
1416
+ },
1417
+ source: "review",
1418
+ reason: "spec_review_warn"
1419
+ };
1420
+ }
1421
+ } else if (review.quality.status === STATUS.WARN) {
1422
+ return {
1423
+ status: "in_progress",
1424
+ nextAction:
1425
+ review.quality.issues[0] ||
1426
+ review.quality.summary ||
1427
+ `resolve quality review follow-up for task group ${group.taskGroupId}`,
1428
+ resumeCursor: {
1429
+ groupIndex: fallbackCursor.groupIndex,
1430
+ nextUncheckedItem: null,
1431
+ liveFocus: "quality_review_warn"
1432
+ },
1433
+ source: "review",
1434
+ reason: "quality_review_warn"
1435
+ };
1436
+ } else if (specWarn) {
1437
+ return {
1438
+ status: "in_progress",
1439
+ nextAction:
1440
+ review.spec.issues[0] ||
1441
+ review.spec.summary ||
1442
+ `resolve spec review follow-up for task group ${group.taskGroupId}`,
1443
+ resumeCursor: {
1444
+ groupIndex: fallbackCursor.groupIndex,
1445
+ nextUncheckedItem: null,
1446
+ liveFocus: "spec_review_warn"
1447
+ },
1448
+ source: "review",
1449
+ reason: "spec_review_warn"
1450
+ };
1451
+ }
1452
+ }
1453
+ }
1454
+
1455
+ if (implementer.present && implementer.signalStatus === STATUS.WARN) {
1456
+ return {
1457
+ status: "in_progress",
1458
+ nextAction:
1459
+ implementer.concerns[0] ||
1460
+ implementer.summary ||
1461
+ `resolve implementer concerns for task group ${group.taskGroupId}`,
1462
+ resumeCursor: {
1463
+ groupIndex: fallbackCursor.groupIndex,
1464
+ nextUncheckedItem: null,
1465
+ liveFocus: "implementer_warn"
1466
+ },
1467
+ source: "implementer",
1468
+ reason: "implementer_warn"
1469
+ };
1470
+ }
1471
+
1472
+ return effective;
1473
+ }
1474
+
1475
+ function deriveTaskGroupRuntimeState(plannedTaskGroups, signals, seedTaskGroups) {
1476
+ const plannedGroups = Array.isArray(plannedTaskGroups) ? plannedTaskGroups : [];
1477
+ const seedMap = normalizeTaskGroupSeedMap(seedTaskGroups);
1478
+
1479
+ return plannedGroups.map((plannedGroup, index) => {
1480
+ const taskGroupId = normalizeTaskGroupId(plannedGroup.taskGroupId || plannedGroup.id);
1481
+ const seed = seedMap.get(taskGroupId) || {};
1482
+ const planned = {
1483
+ status: plannedGroup.status,
1484
+ completion: plannedGroup.completion,
1485
+ checkpointOutcome: plannedGroup.checkpointOutcome,
1486
+ evidence: Array.isArray(plannedGroup.evidence) ? plannedGroup.evidence : [],
1487
+ nextAction: plannedGroup.nextAction,
1488
+ resumeCursor: normalizeResumeCursor(plannedGroup.resumeCursor, index)
1489
+ };
1490
+ const implementer = buildTaskGroupImplementerState(taskGroupId, signals, seed.implementer);
1491
+ const review = buildTaskGroupReviewState(plannedGroup, signals, seed.review);
1492
+ const effective = buildEffectiveTaskGroupState(plannedGroup, planned, implementer, review);
1493
+
1494
+ return {
1495
+ taskGroupId,
1496
+ title: plannedGroup.title,
1497
+ status: effective.status,
1498
+ completion: planned.completion,
1499
+ checkpointOutcome: planned.checkpointOutcome,
1500
+ evidence: planned.evidence,
1501
+ nextAction: effective.nextAction,
1502
+ targetFiles: Array.isArray(plannedGroup.targetFiles) ? plannedGroup.targetFiles : [],
1503
+ fileReferences: Array.isArray(plannedGroup.fileReferences) ? plannedGroup.fileReferences : [],
1504
+ verificationActions: Array.isArray(plannedGroup.verificationActions)
1505
+ ? plannedGroup.verificationActions
1506
+ : [],
1507
+ verificationCommands: Array.isArray(plannedGroup.verificationCommands)
1508
+ ? plannedGroup.verificationCommands
1509
+ : [],
1510
+ executionIntent: Array.isArray(plannedGroup.executionIntent) ? plannedGroup.executionIntent : [],
1511
+ reviewIntent: plannedGroup.reviewIntent === true,
1512
+ testingIntent: plannedGroup.testingIntent === true,
1513
+ codeChangeLikely: plannedGroup.codeChangeLikely === true,
1514
+ resumeCursor: effective.resumeCursor,
1515
+ planned,
1516
+ implementer,
1517
+ review,
1518
+ effective
1519
+ };
1520
+ });
1521
+ }
1522
+
1523
+ function buildTaskGroupMetadataPayload(changeId, checkpointStatuses, taskGroups) {
1524
+ return {
1525
+ version: TASK_GROUP_METADATA_VERSION,
1526
+ changeId,
1527
+ checkpointOutcome: checkpointStatuses[CHECKPOINT_LABELS.TASK] || STATUS.WARN,
1528
+ taskGroups: Array.isArray(taskGroups) ? taskGroups : [],
1529
+ updatedAt: new Date().toISOString()
1530
+ };
1531
+ }
1532
+
1533
+ function loadTaskGroupMetadataFromPath(targetPath) {
1534
+ if (!targetPath || !pathExists(targetPath)) {
1535
+ return null;
1536
+ }
1537
+ try {
1538
+ return JSON.parse(readTextIfExists(targetPath));
1539
+ } catch (_error) {
1540
+ return null;
1541
+ }
1542
+ }
1543
+
1544
+ function resolvePersistedTaskGroupSeed(projectRoot, changeId, persistedRecord, plannedTaskGroups) {
1545
+ const metadataRefs =
1546
+ persistedRecord && persistedRecord.metadataRefs && typeof persistedRecord.metadataRefs === "object"
1547
+ ? persistedRecord.metadataRefs
1548
+ : {};
1549
+ const canonicalPath =
1550
+ metadataRefs.taskGroupsPath || resolveTaskGroupMetadataPath(projectRoot, changeId);
1551
+ const notes = [];
1552
+
1553
+ if (canonicalPath && pathExists(canonicalPath)) {
1554
+ const actualDigest = digestForPath(canonicalPath);
1555
+ const expectedDigest = metadataRefs.taskGroupsDigest || null;
1556
+ if (expectedDigest && actualDigest && expectedDigest !== actualDigest) {
1557
+ notes.push("Canonical task-group runtime state digest mismatch; rebuilding task-group state from artifacts.");
1558
+ return {
1559
+ taskGroups: plannedTaskGroups,
1560
+ notes
1561
+ };
1562
+ }
1563
+
1564
+ const loaded = canonicalPath === resolveTaskGroupMetadataPath(projectRoot, changeId)
1565
+ ? readTaskGroupMetadata(projectRoot, changeId)
1566
+ : loadTaskGroupMetadataFromPath(canonicalPath);
1567
+ if (loaded && Array.isArray(loaded.taskGroups)) {
1568
+ return {
1569
+ taskGroups: loaded.taskGroups,
1570
+ notes
1571
+ };
1572
+ }
1573
+ notes.push("Canonical task-group runtime state is unreadable; rebuilding task-group state from artifacts.");
1574
+ return {
1575
+ taskGroups: plannedTaskGroups,
1576
+ notes
1577
+ };
1578
+ }
1579
+
1580
+ if (Array.isArray(persistedRecord && persistedRecord.taskGroups) && persistedRecord.taskGroups.length > 0) {
1581
+ notes.push("Using legacy embedded task-group state as migration fallback.");
1582
+ return {
1583
+ taskGroups: persistedRecord.taskGroups,
1584
+ notes
1585
+ };
1586
+ }
1587
+
1588
+ notes.push("Canonical task-group runtime state is missing; rebuilding task-group state from artifacts.");
1589
+ return {
1590
+ taskGroups: plannedTaskGroups,
1591
+ notes
1592
+ };
1593
+ }
1594
+
1595
+ function buildGatesWithLiveOverlays(baseGates, completionAudit, disciplineState, verificationFreshness) {
1596
+ const gates = baseGates && typeof baseGates === "object" ? { ...baseGates } : {};
1597
+ if (completionAudit) {
1598
+ gates[HANDOFF_GATES.VERIFY_TO_COMPLETE] =
1599
+ completionAudit.status === "PASS" ? STATUS.PASS : STATUS.WARN;
1600
+ }
1601
+ if (disciplineState && disciplineState.blockers.length > 0) {
1602
+ gates[HANDOFF_GATES.TASKS_TO_BUILD] = STATUS.BLOCK;
1603
+ }
1604
+ if (verificationFreshness && !verificationFreshness.fresh) {
1605
+ gates[HANDOFF_GATES.VERIFY_TO_COMPLETE] = STATUS.BLOCK;
1606
+ }
1607
+ return gates;
1608
+ }
1609
+
1610
+ function finalizeWorkflowView(options = {}) {
1611
+ const findings = {
1612
+ blockers: Array.isArray(options.findings && options.findings.blockers)
1613
+ ? options.findings.blockers.slice()
1614
+ : [],
1615
+ warnings: Array.isArray(options.findings && options.findings.warnings)
1616
+ ? options.findings.warnings.slice()
1617
+ : [],
1618
+ notes: Array.isArray(options.findings && options.findings.notes)
1619
+ ? options.findings.notes.slice()
1620
+ : []
1621
+ };
1622
+ let stageId = options.stageId;
1623
+ const integrityAudit = options.integrityAudit || null;
1624
+ const completionAudit = options.completionAudit || null;
1625
+ const disciplineState = options.disciplineState || null;
1626
+ const verificationFreshness = options.verificationFreshness || null;
1627
+ const planningSignalFreshness =
1628
+ options.planningSignalFreshness && typeof options.planningSignalFreshness === "object"
1629
+ ? options.planningSignalFreshness
1630
+ : {
1631
+ effectiveSignalSummary: options.signalSummary || {},
1632
+ stalePlanningSignals: {},
1633
+ needsRerunSurfaces: []
1634
+ };
1635
+ const taskGroups = deriveTaskGroupRuntimeState(
1636
+ options.plannedTaskGroups,
1637
+ options.changeSignals,
1638
+ options.taskGroupSeed
1639
+ );
1640
+
1641
+ stageId = applyAuditFindings(stageId, findings, integrityAudit, completionAudit);
1642
+ stageId = applyPlanningSignalFreshnessFindings(stageId, findings, planningSignalFreshness);
1643
+ stageId = applyExecutionSignalFindings(stageId, findings, planningSignalFreshness.effectiveSignalSummary || {});
1644
+ applyTaskExecutionAndReviewFindings(findings, options.changeSignals || []);
1645
+
1646
+ if (disciplineState) {
1647
+ findings.blockers.push(...disciplineState.blockers);
1648
+ findings.warnings.push(...disciplineState.warnings);
1649
+ findings.notes.push(...disciplineState.notes);
1650
+ if (disciplineState.blockers.length > 0 && ["build", "verify", "complete"].includes(stageId)) {
1651
+ stageId = options.hasTasksArtifact ? "tasks" : "design";
1652
+ }
1653
+ }
1654
+
1655
+ if (verificationFreshness && !verificationFreshness.fresh && (stageId === "verify" || stageId === "complete")) {
1656
+ findings.blockers.push(
1657
+ "Completion-facing routing requires fresh verification evidence; stale evidence keeps the route in verify."
1658
+ );
1659
+ stageId = "verify";
1660
+ }
1661
+
1662
+ const gates = buildGatesWithLiveOverlays(
1663
+ options.baseGates,
1664
+ completionAudit,
1665
+ disciplineState,
1666
+ verificationFreshness
1667
+ );
1668
+ const executionProfile = deriveExecutionProfile({
1669
+ stage: stageId,
1670
+ taskGroups
1671
+ });
1672
+ let worktreePreflight = null;
1673
+ if (options.changeId && (stageId === "build" || stageId === "verify")) {
1674
+ worktreePreflight = runWorktreePreflight(options.projectRoot, {
1675
+ parallelPreferred: executionProfile.mode === "bounded_parallel"
1676
+ });
1677
+ if (
1678
+ executionProfile.mode === "bounded_parallel" &&
1679
+ worktreePreflight.summary &&
1680
+ worktreePreflight.summary.recommendedIsolation
1681
+ ) {
1682
+ executionProfile.effectiveMode = "serial";
1683
+ executionProfile.rationale = dedupeMessages([
1684
+ ...(executionProfile.rationale || []),
1685
+ "worktree preflight recommends isolation; effective mode downgraded to serial"
1686
+ ]);
1687
+ findings.warnings.push(
1688
+ "Bounded-parallel profile downgraded to serial until worktree isolation is ready or explicitly accepted."
1689
+ );
1690
+ }
1691
+ }
1692
+
1693
+ dedupeFindings(findings);
1694
+ const blockingGate = selectBlockingGateIdentity(findings);
1695
+
1696
+ return buildWorkflowResult({
1697
+ projectRoot: options.projectRoot,
1698
+ changeId: options.changeId,
1699
+ stageId,
1700
+ findings,
1701
+ checkpoints: options.checkpoints || {},
1702
+ gates,
1703
+ audits: {
1704
+ integrity: integrityAudit,
1705
+ completion: completionAudit
1706
+ },
1707
+ routeContext: options.routeContext,
1708
+ source: options.source || "derived",
1709
+ taskGroups,
1710
+ discipline: disciplineState,
1711
+ executionProfile,
1712
+ worktreePreflight,
1713
+ verificationFreshness,
1714
+ blockingGate,
1715
+ needsRerunSurfaces: planningSignalFreshness.needsRerunSurfaces,
1716
+ stalePlanningSignals: planningSignalFreshness.stalePlanningSignals
1717
+ });
1718
+ }
1719
+
1720
+ function selectFocusedTaskGroup(taskGroups) {
1721
+ const priority = {
1722
+ blocked: 0,
1723
+ review_pending: 1,
1724
+ in_progress: 2,
1725
+ pending: 3,
1726
+ completed: 4
1727
+ };
1728
+ const groups = Array.isArray(taskGroups) ? taskGroups : [];
1729
+ return groups
1730
+ .slice()
1731
+ .sort((left, right) => {
1732
+ const leftRank = Object.prototype.hasOwnProperty.call(priority, left.status) ? priority[left.status] : 5;
1733
+ const rightRank = Object.prototype.hasOwnProperty.call(priority, right.status) ? priority[right.status] : 5;
1734
+ if (leftRank !== rightRank) {
1735
+ return leftRank - rightRank;
1736
+ }
1737
+ const leftIndex =
1738
+ left && left.resumeCursor && Number.isInteger(left.resumeCursor.groupIndex)
1739
+ ? left.resumeCursor.groupIndex
1740
+ : Number.MAX_SAFE_INTEGER;
1741
+ const rightIndex =
1742
+ right && right.resumeCursor && Number.isInteger(right.resumeCursor.groupIndex)
1743
+ ? right.resumeCursor.groupIndex
1744
+ : Number.MAX_SAFE_INTEGER;
1745
+ return leftIndex - rightIndex;
1746
+ })[0] || null;
1747
+ }
1748
+
672
1749
  function statusFromFindings(findings) {
673
1750
  if (findings.blockers.length > 0) {
674
1751
  return STATUS.BLOCK;
@@ -690,11 +1767,10 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
690
1767
 
691
1768
  if (!pathExists(projectRoot)) {
692
1769
  findings.blockers.push(`Project path does not exist: ${projectRoot}`);
693
- const stageId = "bootstrap";
694
1770
  return buildWorkflowResult({
695
1771
  projectRoot,
696
1772
  changeId: null,
697
- stageId,
1773
+ stageId: "bootstrap",
698
1774
  findings,
699
1775
  checkpoints: {},
700
1776
  gates: {},
@@ -737,9 +1813,7 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
737
1813
  findings.notes.push(`Available change ids: ${changeIds.join(", ")}`);
738
1814
  activeChangeDir = pickLatestChange(changeDirs);
739
1815
  if (activeChangeDir) {
740
- findings.notes.push(
741
- `Latest inferred change for context only: ${path.basename(activeChangeDir)}`
742
- );
1816
+ findings.notes.push(`Latest inferred change for context only: ${path.basename(activeChangeDir)}`);
743
1817
  }
744
1818
  } else {
745
1819
  findings.blockers.push("No non-empty change directory found under `.da-vinci/changes/`.");
@@ -760,12 +1834,19 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
760
1834
 
761
1835
  const tasksArtifactText = activeChangeDir ? readTextIfExists(path.join(activeChangeDir, "tasks.md")) : "";
762
1836
  const checkpointStatuses = activeChangeDir ? readCheckpointStatuses(activeChangeDir) : {};
1837
+ const plannedTaskGroups = deriveTaskGroupMetadata(tasksArtifactText, checkpointStatuses);
763
1838
  const changeSignals = activeChangeId
764
1839
  ? readExecutionSignals(projectRoot, {
765
1840
  changeId: activeChangeId
766
1841
  })
767
1842
  : [];
768
1843
  const signalSummary = summarizeSignalsBySurface(changeSignals);
1844
+ const planningSignalFreshness = collectPlanningSignalFreshnessState(
1845
+ projectRoot,
1846
+ activeChangeId,
1847
+ signalSummary
1848
+ );
1849
+ const routingSignalSummary = planningSignalFreshness.effectiveSignalSummary;
769
1850
  const disciplineState = activeChangeDir ? inspectDisciplineState(activeChangeDir) : null;
770
1851
  const freshnessArtifactPaths = activeChangeDir
771
1852
  ? {
@@ -776,7 +1857,7 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
776
1857
  verificationPath: path.join(activeChangeDir, "verification.md")
777
1858
  }
778
1859
  : null;
779
- const integrityAudit = collectIntegrityAudit(projectRoot, workflowRoot, signalSummary);
1860
+ const integrityAudit = collectIntegrityAudit(projectRoot, workflowRoot, routingSignalSummary);
780
1861
  const designCheckpointStatus = normalizeCheckpointStatus(checkpointStatuses[CHECKPOINT_LABELS.DESIGN]);
781
1862
  const designSourceCheckpointStatus = normalizeCheckpointStatus(
782
1863
  checkpointStatuses[CHECKPOINT_LABELS.DESIGN_SOURCE]
@@ -786,6 +1867,18 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
786
1867
  );
787
1868
  const mappingCheckpointStatus = normalizeCheckpointStatus(checkpointStatuses[CHECKPOINT_LABELS.MAPPING]);
788
1869
  const taskCheckpointStatus = normalizeCheckpointStatus(checkpointStatuses[CHECKPOINT_LABELS.TASK]);
1870
+ const verificationFreshness = activeChangeId
1871
+ ? collectVerificationFreshness(projectRoot, {
1872
+ changeId: activeChangeId,
1873
+ resolved: { changeDir: activeChangeDir },
1874
+ artifactPaths: freshnessArtifactPaths
1875
+ })
1876
+ : null;
1877
+ const routeContext = {
1878
+ projectRoot,
1879
+ changeId: activeChangeId || requestedChangeId || "change-001",
1880
+ ambiguousChangeSelection
1881
+ };
789
1882
 
790
1883
  if (activeChangeId) {
791
1884
  const persistedSelection = selectPersistedStateForChange(projectRoot, activeChangeId, {
@@ -794,123 +1887,47 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
794
1887
  if (persistedSelection.usable && persistedSelection.changeRecord) {
795
1888
  const persistedRecord = persistedSelection.changeRecord;
796
1889
  const stageRecord = getStageById(persistedRecord.stage) || getStageById("bootstrap");
797
- const persistedTaskMetadata = readTaskGroupMetadata(projectRoot, activeChangeId);
798
- const persistedFindings = {
799
- blockers: Array.isArray(persistedRecord.failures) ? persistedRecord.failures.slice() : [],
800
- warnings: Array.isArray(persistedRecord.warnings) ? persistedRecord.warnings.slice() : [],
801
- notes: sanitizePersistedNotes(persistedRecord.notes)
802
- };
803
- let persistedStageId = stageRecord.id;
804
- const persistedNeedsCompletionAudit =
805
- persistedStageId === "verify" || persistedStageId === "complete";
806
- const completionAudit = persistedNeedsCompletionAudit
807
- ? collectCompletionAudit(projectRoot, workflowRoot, activeChangeId, signalSummary)
808
- : null;
809
- persistedStageId = applyAuditFindings(
810
- persistedStageId,
811
- persistedFindings,
812
- integrityAudit,
813
- completionAudit
814
- );
815
- persistedStageId = applyExecutionSignalFindings(
816
- persistedStageId,
817
- persistedFindings,
818
- signalSummary
1890
+ const completionAudit =
1891
+ stageRecord.id === "verify" || stageRecord.id === "complete"
1892
+ ? collectCompletionAudit(projectRoot, workflowRoot, activeChangeId, routingSignalSummary)
1893
+ : null;
1894
+ const persistedSeed = resolvePersistedTaskGroupSeed(
1895
+ projectRoot,
1896
+ activeChangeId,
1897
+ persistedRecord,
1898
+ plannedTaskGroups
819
1899
  );
820
- applyTaskExecutionAndReviewFindings(persistedFindings, changeSignals);
821
- if (disciplineState) {
822
- persistedFindings.blockers.push(...disciplineState.blockers);
823
- persistedFindings.warnings.push(...disciplineState.warnings);
824
- persistedFindings.notes.push(...disciplineState.notes);
825
- if (disciplineState.blockers.length > 0 && ["build", "verify", "complete"].includes(persistedStageId)) {
826
- persistedStageId = artifactState.tasks ? "tasks" : "design";
827
- }
828
- }
829
-
830
- const verificationFreshness = activeChangeId
831
- ? collectVerificationFreshness(projectRoot, {
832
- changeId: activeChangeId,
833
- resolved: { changeDir: activeChangeDir },
834
- artifactPaths: freshnessArtifactPaths
835
- })
836
- : null;
837
- if (
838
- verificationFreshness &&
839
- !verificationFreshness.fresh &&
840
- (persistedStageId === "verify" || persistedStageId === "complete")
841
- ) {
842
- persistedFindings.blockers.push(
843
- "Completion-facing routing requires fresh verification evidence; stale evidence keeps the route in verify."
844
- );
845
- persistedStageId = "verify";
846
- }
847
- persistedFindings.notes.push("workflow-status is using trusted persisted workflow state.");
848
- dedupeFindings(persistedFindings);
849
-
850
- const persistedGates =
851
- persistedRecord && persistedRecord.gates && typeof persistedRecord.gates === "object"
852
- ? { ...persistedRecord.gates }
853
- : {};
854
- if (completionAudit) {
855
- persistedGates[HANDOFF_GATES.VERIFY_TO_COMPLETE] =
856
- completionAudit.status === "PASS" ? STATUS.PASS : STATUS.WARN;
857
- }
858
- if (disciplineState && disciplineState.blockers.length > 0) {
859
- persistedGates[HANDOFF_GATES.TASKS_TO_BUILD] = STATUS.BLOCK;
860
- }
861
- if (verificationFreshness && !verificationFreshness.fresh) {
862
- persistedGates[HANDOFF_GATES.VERIFY_TO_COMPLETE] = STATUS.BLOCK;
863
- }
864
- const persistedTaskGroups =
865
- persistedTaskMetadata && Array.isArray(persistedTaskMetadata.taskGroups)
866
- ? persistedTaskMetadata.taskGroups
867
- : Array.isArray(persistedRecord.taskGroups)
868
- ? persistedRecord.taskGroups
869
- : [];
870
- const executionProfile = deriveExecutionProfile({
871
- stage: persistedStageId,
872
- taskGroups: persistedTaskGroups
873
- });
874
- let worktreePreflight = null;
875
- if (activeChangeId && (persistedStageId === "build" || persistedStageId === "verify")) {
876
- worktreePreflight = runWorktreePreflight(projectRoot, {
877
- parallelPreferred: executionProfile.mode === "bounded_parallel"
878
- });
879
- if (
880
- executionProfile.mode === "bounded_parallel" &&
881
- worktreePreflight.summary &&
882
- worktreePreflight.summary.recommendedIsolation
883
- ) {
884
- executionProfile.effectiveMode = "serial";
885
- executionProfile.rationale = dedupeMessages([
886
- ...(executionProfile.rationale || []),
887
- "worktree preflight recommends isolation; effective mode downgraded to serial"
888
- ]);
889
- }
890
- }
891
-
892
- return buildWorkflowResult({
1900
+ return finalizeWorkflowView({
893
1901
  projectRoot,
894
1902
  changeId: activeChangeId,
895
- stageId: persistedStageId,
896
- findings: persistedFindings,
897
- checkpoints: checkpointStatuses,
898
- gates: persistedGates,
899
- audits: {
900
- integrity: integrityAudit,
901
- completion: completionAudit
902
- },
903
- routeContext: {
904
- projectRoot,
905
- changeId: activeChangeId,
906
- ambiguousChangeSelection
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
+ ]
907
1913
  },
1914
+ baseGates:
1915
+ persistedRecord && persistedRecord.gates && typeof persistedRecord.gates === "object"
1916
+ ? { ...persistedRecord.gates }
1917
+ : {},
1918
+ checkpoints: checkpointStatuses,
1919
+ routeContext,
908
1920
  source: "persisted",
909
- taskGroups: persistedTaskGroups,
910
- discipline: disciplineState,
911
- executionProfile,
912
- worktreePreflight,
913
- verificationFreshness
1921
+ taskGroupSeed: persistedSeed.taskGroups,
1922
+ plannedTaskGroups,
1923
+ changeSignals,
1924
+ signalSummary,
1925
+ planningSignalFreshness,
1926
+ integrityAudit,
1927
+ completionAudit,
1928
+ disciplineState,
1929
+ verificationFreshness,
1930
+ hasTasksArtifact: artifactState.tasks
914
1931
  });
915
1932
  }
916
1933
 
@@ -920,8 +1937,6 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
920
1937
  "parse-error": "Persisted workflow state is unreadable; deriving from artifacts.",
921
1938
  "version-mismatch": "Persisted workflow state version mismatch; deriving from artifacts.",
922
1939
  "change-missing": "Persisted workflow state has no entry for this change; deriving from artifacts.",
923
- "invalid-timestamp": "Persisted workflow state timestamp invalid; deriving from artifacts.",
924
- "time-stale": "Persisted workflow state is stale by time; deriving from artifacts.",
925
1940
  "fingerprint-mismatch": "Persisted workflow state conflicts with artifact truth; deriving from artifacts."
926
1941
  };
927
1942
  const message = reasonMessage[persistedSelection.reason];
@@ -931,38 +1946,17 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
931
1946
  }
932
1947
  }
933
1948
 
934
- let stageId = deriveStageFromArtifacts(artifactState, checkpointStatuses, findings);
935
- const needsCompletionAudit = stageId === "verify" || stageId === "complete";
936
- const completionAudit = needsCompletionAudit
937
- ? collectCompletionAudit(projectRoot, workflowRoot, activeChangeId, signalSummary)
938
- : null;
939
- stageId = applyAuditFindings(stageId, findings, integrityAudit, completionAudit);
940
- stageId = applyExecutionSignalFindings(stageId, findings, signalSummary);
941
- applyTaskExecutionAndReviewFindings(findings, changeSignals);
942
- if (disciplineState) {
943
- findings.blockers.push(...disciplineState.blockers);
944
- findings.warnings.push(...disciplineState.warnings);
945
- findings.notes.push(...disciplineState.notes);
946
- if (disciplineState.blockers.length > 0 && ["build", "verify", "complete"].includes(stageId)) {
947
- stageId = artifactState.tasks ? "tasks" : "design";
948
- }
949
- }
950
-
951
- const verificationFreshness = activeChangeId
952
- ? collectVerificationFreshness(projectRoot, {
953
- changeId: activeChangeId,
954
- resolved: { changeDir: activeChangeDir },
955
- artifactPaths: freshnessArtifactPaths
956
- })
957
- : null;
958
- if (verificationFreshness && !verificationFreshness.fresh && (stageId === "verify" || stageId === "complete")) {
959
- findings.blockers.push(
960
- "Completion-facing routing requires fresh verification evidence; stale evidence keeps the route in verify."
961
- );
962
- stageId = "verify";
963
- }
964
-
965
- const gates = {
1949
+ const baseFindings = {
1950
+ blockers: findings.blockers.slice(),
1951
+ warnings: findings.warnings.slice(),
1952
+ notes: findings.notes.slice()
1953
+ };
1954
+ const stageId = deriveStageFromArtifacts(artifactState, checkpointStatuses, baseFindings);
1955
+ const completionAudit =
1956
+ stageId === "verify" || stageId === "complete"
1957
+ ? collectCompletionAudit(projectRoot, workflowRoot, activeChangeId, routingSignalSummary)
1958
+ : null;
1959
+ const baseGates = {
966
1960
  [HANDOFF_GATES.BREAKDOWN_TO_DESIGN]:
967
1961
  artifactState.proposal && artifactState.specFiles.length > 0 ? STATUS.PASS : STATUS.BLOCK,
968
1962
  [HANDOFF_GATES.DESIGN_TO_TASKS]: mergeStatuses([
@@ -986,90 +1980,72 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
986
1980
  [HANDOFF_GATES.VERIFY_TO_COMPLETE]:
987
1981
  completionAudit && completionAudit.status === "PASS" ? STATUS.PASS : STATUS.WARN
988
1982
  };
989
- if (disciplineState && disciplineState.blockers.length > 0) {
990
- gates[HANDOFF_GATES.TASKS_TO_BUILD] = STATUS.BLOCK;
991
- }
992
- if (verificationFreshness && !verificationFreshness.fresh) {
993
- gates[HANDOFF_GATES.VERIFY_TO_COMPLETE] = STATUS.BLOCK;
994
- }
995
-
996
- const derivedTaskGroups = deriveTaskGroupMetadata(tasksArtifactText, checkpointStatuses);
997
- const executionProfile = deriveExecutionProfile({
998
- stage: stageId,
999
- taskGroups: derivedTaskGroups
1000
- });
1001
- let worktreePreflight = null;
1002
- if (activeChangeId && (stageId === "build" || stageId === "verify")) {
1003
- worktreePreflight = runWorktreePreflight(projectRoot, {
1004
- parallelPreferred: executionProfile.mode === "bounded_parallel"
1005
- });
1006
- if (
1007
- executionProfile.mode === "bounded_parallel" &&
1008
- worktreePreflight.summary &&
1009
- worktreePreflight.summary.recommendedIsolation
1010
- ) {
1011
- executionProfile.effectiveMode = "serial";
1012
- executionProfile.rationale = dedupeMessages([
1013
- ...(executionProfile.rationale || []),
1014
- "worktree preflight recommends isolation; effective mode downgraded to serial"
1015
- ]);
1016
- findings.warnings.push(
1017
- "Bounded-parallel profile downgraded to serial until worktree isolation is ready or explicitly accepted."
1018
- );
1019
- }
1020
- }
1021
- if (activeChangeId && !ambiguousChangeSelection && derivedTaskGroups.length > 0) {
1022
- const metadataPayload = {
1023
- version: 1,
1024
- changeId: activeChangeId,
1025
- checkpointOutcome: checkpointStatuses[CHECKPOINT_LABELS.TASK] || STATUS.WARN,
1026
- taskGroups: derivedTaskGroups,
1027
- updatedAt: new Date().toISOString()
1028
- };
1029
- const metadataPath = writeTaskGroupMetadata(projectRoot, activeChangeId, metadataPayload);
1030
- if (metadataPath) {
1031
- findings.notes.push(`Task-group metadata refreshed: ${metadataPath}`);
1032
- }
1033
- } else if (activeChangeId && ambiguousChangeSelection && derivedTaskGroups.length > 0) {
1034
- findings.notes.push(
1035
- "Skipped task-group metadata persistence because change selection is ambiguous."
1036
- );
1037
- }
1038
- dedupeFindings(findings);
1039
1983
 
1040
- const derivedResult = buildWorkflowResult({
1984
+ const derivedResult = finalizeWorkflowView({
1041
1985
  projectRoot,
1042
1986
  changeId: activeChangeId,
1043
1987
  stageId,
1044
- findings,
1988
+ findings: baseFindings,
1989
+ baseGates,
1045
1990
  checkpoints: checkpointStatuses,
1046
- gates,
1047
- audits: {
1048
- integrity: integrityAudit,
1049
- completion: completionAudit
1050
- },
1051
- routeContext: {
1052
- projectRoot,
1053
- changeId: activeChangeId || requestedChangeId || "change-001",
1054
- ambiguousChangeSelection
1055
- },
1056
- taskGroups: derivedTaskGroups,
1057
- discipline: disciplineState,
1058
- executionProfile,
1059
- worktreePreflight,
1060
- verificationFreshness
1991
+ routeContext,
1992
+ source: "derived",
1993
+ taskGroupSeed: plannedTaskGroups,
1994
+ plannedTaskGroups,
1995
+ changeSignals,
1996
+ signalSummary,
1997
+ planningSignalFreshness,
1998
+ integrityAudit,
1999
+ completionAudit,
2000
+ disciplineState,
2001
+ verificationFreshness,
2002
+ hasTasksArtifact: artifactState.tasks
1061
2003
  });
1062
2004
 
1063
2005
  if (activeChangeId && !ambiguousChangeSelection) {
2006
+ let metadataRef = {
2007
+ taskGroupsPath: null,
2008
+ taskGroupsDigest: null
2009
+ };
2010
+ try {
2011
+ const metadataRecord = writeTaskGroupMetadata(
2012
+ projectRoot,
2013
+ activeChangeId,
2014
+ buildTaskGroupMetadataPayload(activeChangeId, checkpointStatuses, derivedResult.taskGroups)
2015
+ );
2016
+ if (metadataRecord && metadataRecord.path) {
2017
+ metadataRef = {
2018
+ taskGroupsPath: metadataRecord.path,
2019
+ taskGroupsDigest: metadataRecord.digest || null
2020
+ };
2021
+ derivedResult.notes = dedupeMessages([
2022
+ ...derivedResult.notes,
2023
+ `Task-group metadata refreshed: ${metadataRecord.path}`
2024
+ ]);
2025
+ }
2026
+ } catch (error) {
2027
+ derivedResult.warnings = dedupeMessages([
2028
+ ...derivedResult.warnings,
2029
+ `Failed to persist canonical task-group metadata: ${
2030
+ error && error.message ? error.message : "unknown write error"
2031
+ }`
2032
+ ]);
2033
+ if (derivedResult.checkpointState === STATUS.PASS) {
2034
+ derivedResult.checkpointState = STATUS.WARN;
2035
+ }
2036
+ if (derivedResult.status === STATUS.PASS) {
2037
+ derivedResult.status = STATUS.WARN;
2038
+ }
2039
+ }
2040
+
1064
2041
  try {
1065
2042
  persistDerivedWorkflowResult(projectRoot, activeChangeId, derivedResult, {
1066
- metadataRefs: {
1067
- taskGroupsPath: activeChangeId
1068
- ? path.join(projectRoot, ".da-vinci", "state", "task-groups", `${activeChangeId}.json`)
1069
- : null
1070
- }
2043
+ metadataRefs: metadataRef
1071
2044
  });
1072
- derivedResult.notes.push("workflow-status persisted a fresh derived workflow snapshot.");
2045
+ derivedResult.notes = dedupeMessages([
2046
+ ...derivedResult.notes,
2047
+ "workflow-status persisted a fresh derived workflow snapshot."
2048
+ ]);
1073
2049
  } catch (error) {
1074
2050
  const message =
1075
2051
  error && error.message
@@ -1121,7 +2097,13 @@ function buildWorkflowResult(params) {
1121
2097
  discipline: params.discipline || null,
1122
2098
  executionProfile: params.executionProfile || null,
1123
2099
  worktreePreflight: params.worktreePreflight || null,
1124
- verificationFreshness: params.verificationFreshness || null
2100
+ verificationFreshness: params.verificationFreshness || null,
2101
+ blockingGate: params.blockingGate || null,
2102
+ needsRerunSurfaces: Array.isArray(params.needsRerunSurfaces) ? params.needsRerunSurfaces : [],
2103
+ stalePlanningSignals:
2104
+ params.stalePlanningSignals && typeof params.stalePlanningSignals === "object"
2105
+ ? params.stalePlanningSignals
2106
+ : {}
1125
2107
  };
1126
2108
  }
1127
2109
 
@@ -1142,6 +2124,12 @@ function formatWorkflowStatusReport(result) {
1142
2124
  } else {
1143
2125
  lines.push("Next route: none");
1144
2126
  }
2127
+ if (result.blockingGate && result.blockingGate.id) {
2128
+ lines.push(`Blocking gate: ${result.blockingGate.id} (${result.blockingGate.surface || "unknown"})`);
2129
+ }
2130
+ if (Array.isArray(result.needsRerunSurfaces) && result.needsRerunSurfaces.length > 0) {
2131
+ lines.push(`Needs rerun surfaces: ${result.needsRerunSurfaces.join(", ")}`);
2132
+ }
1145
2133
 
1146
2134
  if (result.discipline && result.discipline.designApproval) {
1147
2135
  lines.push(`Discipline design approval: ${result.discipline.designApproval.state}`);
@@ -1196,7 +2184,7 @@ function formatWorkflowStatusReport(result) {
1196
2184
  lines.push("Task-group metadata:");
1197
2185
  for (const group of result.taskGroups) {
1198
2186
  lines.push(
1199
- `- ${group.taskGroupId || group.id || "group"}: ${group.status || "unknown"} (${group.completion || 0}%)`
2187
+ `- ${group.taskGroupId || group.id || "group"}: ${group.status || "unknown"} (${group.completion || 0}%) -> ${group.nextAction || "continue"}`
1200
2188
  );
1201
2189
  }
1202
2190
  }
@@ -1222,6 +2210,12 @@ function formatNextStepReport(result) {
1222
2210
  if (result.nextStep.reason) {
1223
2211
  lines.push(`Reason: ${result.nextStep.reason}`);
1224
2212
  }
2213
+ if (result.blockingGate && result.blockingGate.id) {
2214
+ lines.push(`Blocking gate: ${result.blockingGate.id} (${result.blockingGate.surface || "unknown"})`);
2215
+ }
2216
+ if (Array.isArray(result.needsRerunSurfaces) && result.needsRerunSurfaces.length > 0) {
2217
+ lines.push(`Needs rerun surfaces: ${result.needsRerunSurfaces.join(", ")}`);
2218
+ }
1225
2219
  if (result.executionProfile) {
1226
2220
  lines.push(
1227
2221
  `Execution profile: ${result.executionProfile.mode}${
@@ -1238,9 +2232,7 @@ function formatNextStepReport(result) {
1238
2232
  lines.push("Completion evidence is stale; stay in verify until fresh evidence is recorded.");
1239
2233
  }
1240
2234
  if (Array.isArray(result.taskGroups) && result.taskGroups.length > 0) {
1241
- const active =
1242
- result.taskGroups.find((group) => group.status === "in_progress") ||
1243
- result.taskGroups.find((group) => group.status === "pending");
2235
+ const active = selectFocusedTaskGroup(result.taskGroups);
1244
2236
  if (active) {
1245
2237
  lines.push(`Task-group focus: ${active.taskGroupId || active.id} -> ${active.nextAction || "continue"}`);
1246
2238
  }