@xenonbyte/da-vinci-workflow 0.2.4 → 0.2.6

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +15 -9
  3. package/README.zh-CN.md +16 -9
  4. package/SKILL.md +45 -704
  5. package/docs/dv-command-reference.md +33 -5
  6. package/docs/execution-chain-migration.md +14 -3
  7. package/docs/maintainer-bootstrap.md +102 -0
  8. package/docs/pencil-rendering-workflow.md +1 -1
  9. package/docs/prompt-entrypoints.md +1 -0
  10. package/docs/skill-contract-maintenance.md +14 -0
  11. package/docs/skill-usage.md +31 -0
  12. package/docs/workflow-overview.md +40 -5
  13. package/docs/zh-CN/dv-command-reference.md +31 -5
  14. package/docs/zh-CN/maintainer-bootstrap.md +101 -0
  15. package/docs/zh-CN/pencil-rendering-workflow.md +1 -1
  16. package/docs/zh-CN/prompt-entrypoints.md +1 -0
  17. package/docs/zh-CN/skill-usage.md +30 -0
  18. package/docs/zh-CN/workflow-overview.md +38 -5
  19. package/lib/audit.js +19 -0
  20. package/lib/cli/helpers.js +104 -0
  21. package/lib/cli/lint-family.js +56 -0
  22. package/lib/cli/verify-family.js +79 -0
  23. package/lib/cli.js +143 -172
  24. package/lib/gate-utils.js +56 -0
  25. package/lib/install.js +134 -6
  26. package/lib/lint-bindings.js +41 -28
  27. package/lib/lint-spec.js +403 -109
  28. package/lib/lint-tasks.js +571 -21
  29. package/lib/maintainer-readiness.js +317 -0
  30. package/lib/planning-parsers.js +198 -2
  31. package/lib/planning-quality-utils.js +81 -0
  32. package/lib/planning-signal-freshness.js +205 -0
  33. package/lib/scaffold.js +454 -23
  34. package/lib/scope-check.js +751 -82
  35. package/lib/sidecars.js +396 -1
  36. package/lib/task-review.js +2 -1
  37. package/lib/utils.js +34 -0
  38. package/lib/verify.js +1160 -88
  39. package/lib/workflow-persisted-state.js +52 -32
  40. package/lib/workflow-state.js +1187 -249
  41. package/package.json +1 -1
  42. package/references/skill-workflow-detail.md +66 -0
@@ -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);
@@ -247,9 +470,15 @@ function applyTaskExecutionAndReviewFindings(findings, signals) {
247
470
  }
248
471
 
249
472
  for (const [taskGroupId, state] of Object.entries(reviewStateByGroup)) {
250
- if (state.quality && state.quality === STATUS.PASS && state.spec !== STATUS.PASS) {
473
+ if (state.quality && !state.spec) {
474
+ findings.blockers.push(
475
+ `Task review ordering violation for ${taskGroupId}: quality review exists without a prior spec review result.`
476
+ );
477
+ continue;
478
+ }
479
+ if (state.quality && state.spec === STATUS.BLOCK) {
251
480
  findings.blockers.push(
252
- `Task review ordering violation for ${taskGroupId}: quality review passed before spec review PASS.`
481
+ `Task review ordering violation for ${taskGroupId}: quality review was recorded while spec review is BLOCK.`
253
482
  );
254
483
  }
255
484
  }
@@ -321,6 +550,7 @@ function applyAuditFindings(stageId, findings, integrityAudit, completionAudit)
321
550
 
322
551
  function applyExecutionSignalFindings(stageId, findings, signalSummary) {
323
552
  let nextStageId = stageId;
553
+ const strictPromotion = isStrictPromotionEnabled();
324
554
  const signalParseIssue = signalSummary["signal-file-parse"];
325
555
  if (signalParseIssue) {
326
556
  const warningText =
@@ -335,22 +565,242 @@ function applyExecutionSignalFindings(stageId, findings, signalSummary) {
335
565
  findings.warnings.push(`Planning diff signal diff-spec reports ${diffSignal.status}.`);
336
566
  }
337
567
 
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";
568
+ const lintSpecSignal = signalSummary["lint-spec"];
569
+ const lintSpecGateConfigs = [
570
+ { id: "principleInheritance", label: "principleInheritance", fallbackStage: "breakdown" },
571
+ { id: "clarify", label: "clarify", fallbackStage: "breakdown" },
572
+ { id: "scenarioQuality", label: "scenarioQuality", fallbackStage: "breakdown" }
573
+ ];
574
+ let lintSpecGateObserved = false;
575
+ let strongestLintSpecGateStatus = "";
576
+ for (const config of lintSpecGateConfigs) {
577
+ const gate = getSignalGate(lintSpecSignal, config.id);
578
+ if (!gate) {
579
+ continue;
580
+ }
581
+ lintSpecGateObserved = true;
582
+ const gateStatus = resolveEffectiveGateStatus(gate, lintSpecSignal);
583
+ if (statusSeverity(gateStatus) > statusSeverity(strongestLintSpecGateStatus)) {
584
+ strongestLintSpecGateStatus = gateStatus;
585
+ }
586
+ const evidenceRefs = collectGateEvidenceRefs(gate);
587
+ const evidenceSuffix =
588
+ evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
589
+ if (gateStatus === STATUS.BLOCK) {
590
+ findings.blockers.push(
591
+ `lint-spec gate ${config.label} is BLOCK and prevents planning promotion.${evidenceSuffix}`
592
+ );
593
+ addBlockingGateRecord(
594
+ findings,
595
+ config.id,
596
+ "lint-spec",
597
+ gate,
598
+ `lint-spec gate ${config.label} is BLOCK`
599
+ );
600
+ nextStageId = fallbackStageIfBeyond(nextStageId, config.fallbackStage);
601
+ } else if (gateStatus === STATUS.WARN) {
602
+ findings.warnings.push(`lint-spec gate ${config.label} is WARN.${evidenceSuffix}`);
603
+ if (strictPromotion) {
604
+ findings.blockers.push(
605
+ `DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-spec gate ${config.label} WARN blocks promotion.`
606
+ );
607
+ addBlockingGateRecord(
608
+ findings,
609
+ config.id,
610
+ "lint-spec",
611
+ gate,
612
+ `strict promotion escalated lint-spec gate ${config.label} WARN`
613
+ );
614
+ nextStageId = fallbackStageIfBeyond(nextStageId, config.fallbackStage);
615
+ }
616
+ }
617
+ for (const message of Array.isArray(gate.compatibility) ? gate.compatibility : []) {
618
+ findings.notes.push(`lint-spec gate ${config.label} compatibility: ${message}`);
619
+ }
620
+ if (config.id === "clarify") {
621
+ for (const bounded of Array.isArray(gate.bounded) ? gate.bounded : []) {
622
+ findings.notes.push(`lint-spec gate clarify bounded ambiguity: ${bounded}`);
623
+ }
624
+ }
625
+ }
626
+ const lintSpecSignalStatus = normalizeSignalStatus(lintSpecSignal && lintSpecSignal.status);
627
+ if (
628
+ lintSpecSignal &&
629
+ (!lintSpecGateObserved || statusSeverity(lintSpecSignalStatus) > statusSeverity(strongestLintSpecGateStatus))
630
+ ) {
631
+ if (lintSpecSignalStatus === STATUS.BLOCK) {
632
+ findings.blockers.push("lint-spec signal is BLOCK.");
633
+ addBlockingGateRecord(
634
+ findings,
635
+ "lint-spec",
636
+ "lint-spec",
637
+ lintSpecSignal.details && lintSpecSignal.details.gates ? lintSpecSignal.details.gates : null,
638
+ "lint-spec signal is BLOCK"
639
+ );
640
+ nextStageId = fallbackStageIfBeyond(nextStageId, "breakdown");
641
+ } else if (lintSpecSignalStatus === STATUS.WARN) {
642
+ findings.warnings.push("lint-spec signal is WARN.");
643
+ if (strictPromotion) {
644
+ findings.blockers.push(
645
+ "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-spec WARN blocks promotion."
646
+ );
647
+ addBlockingGateRecord(
648
+ findings,
649
+ "lint-spec",
650
+ "lint-spec",
651
+ lintSpecSignal.details && lintSpecSignal.details.gates ? lintSpecSignal.details.gates : null,
652
+ "strict promotion escalated lint-spec WARN"
653
+ );
654
+ nextStageId = fallbackStageIfBeyond(nextStageId, "breakdown");
655
+ }
656
+ }
657
+ }
658
+
659
+ const analyzeSignal = signalSummary["scope-check"];
660
+ const analyzeGate = getSignalGate(analyzeSignal, "analyze");
661
+ let analyzeGateStatus = "";
662
+ if (analyzeGate) {
663
+ const evidenceRefs = collectGateEvidenceRefs(analyzeGate);
664
+ const evidenceSuffix =
665
+ evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
666
+ analyzeGateStatus = resolveEffectiveGateStatus(analyzeGate, analyzeSignal);
667
+ if (analyzeGateStatus === STATUS.BLOCK) {
668
+ findings.blockers.push(`scope-check gate analyze is BLOCK.${evidenceSuffix}`);
669
+ addBlockingGateRecord(
670
+ findings,
671
+ "analyze",
672
+ "scope-check",
673
+ analyzeGate,
674
+ "scope-check gate analyze is BLOCK"
675
+ );
676
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
677
+ } else if (analyzeGateStatus === STATUS.WARN) {
678
+ findings.warnings.push(`scope-check gate analyze is WARN.${evidenceSuffix}`);
679
+ if (strictPromotion) {
680
+ findings.blockers.push(
681
+ "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; scope-check gate analyze WARN blocks promotion."
682
+ );
683
+ addBlockingGateRecord(
684
+ findings,
685
+ "analyze",
686
+ "scope-check",
687
+ analyzeGate,
688
+ "strict promotion escalated scope-check gate analyze WARN"
689
+ );
690
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
691
+ }
692
+ }
693
+ for (const message of Array.isArray(analyzeGate.compatibility) ? analyzeGate.compatibility : []) {
694
+ findings.notes.push(`scope-check gate analyze compatibility: ${message}`);
695
+ }
696
+ }
697
+ const analyzeSignalStatus = normalizeSignalStatus(analyzeSignal && analyzeSignal.status);
698
+ if (
699
+ analyzeSignal &&
700
+ (!analyzeGate || statusSeverity(analyzeSignalStatus) > statusSeverity(analyzeGateStatus))
701
+ ) {
702
+ if (analyzeSignalStatus === STATUS.BLOCK) {
703
+ findings.blockers.push("scope-check signal is BLOCK.");
704
+ addBlockingGateRecord(
705
+ findings,
706
+ "scope-check",
707
+ "scope-check",
708
+ analyzeSignal.details && analyzeSignal.details.gates ? analyzeSignal.details.gates : null,
709
+ "scope-check signal is BLOCK"
710
+ );
711
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
712
+ } else if (analyzeSignalStatus === STATUS.WARN) {
713
+ findings.warnings.push("scope-check signal is WARN.");
714
+ if (strictPromotion) {
715
+ findings.blockers.push(
716
+ "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; scope-check WARN blocks promotion."
717
+ );
718
+ addBlockingGateRecord(
719
+ findings,
720
+ "scope-check",
721
+ "scope-check",
722
+ analyzeSignal.details && analyzeSignal.details.gates ? analyzeSignal.details.gates : null,
723
+ "strict promotion escalated scope-check WARN"
724
+ );
725
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
726
+ }
346
727
  }
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")) {
728
+ }
729
+
730
+ const lintTasksSignal = signalSummary["lint-tasks"];
731
+ const taskCheckpointGate = getSignalGate(lintTasksSignal, "taskCheckpoint");
732
+ const taskCheckpointGateStatus = taskCheckpointGate
733
+ ? resolveEffectiveGateStatus(taskCheckpointGate, lintTasksSignal)
734
+ : "";
735
+ const lintTasksSignalStatus = normalizeSignalStatus(lintTasksSignal && lintTasksSignal.status);
736
+ if (taskCheckpointGate && taskCheckpointGateStatus === STATUS.BLOCK) {
737
+ const evidenceRefs = collectGateEvidenceRefs(taskCheckpointGate);
738
+ const evidenceSuffix =
739
+ evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
740
+ findings.blockers.push(`lint-tasks task-checkpoint is BLOCK and prevents promotion into build.${evidenceSuffix}`);
741
+ addBlockingGateRecord(
742
+ findings,
743
+ "taskCheckpoint",
744
+ "lint-tasks",
745
+ taskCheckpointGate,
746
+ "lint-tasks task-checkpoint is BLOCK"
747
+ );
748
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
749
+ } else if (taskCheckpointGate && taskCheckpointGateStatus === STATUS.WARN) {
750
+ const evidenceRefs = collectGateEvidenceRefs(taskCheckpointGate);
751
+ const evidenceSuffix =
752
+ evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
753
+ findings.warnings.push(`lint-tasks task-checkpoint is WARN.${evidenceSuffix}`);
754
+ if (strictPromotion) {
350
755
  findings.blockers.push(
351
756
  "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-tasks WARN blocks promotion into build."
352
757
  );
353
- nextStageId = "tasks";
758
+ addBlockingGateRecord(
759
+ findings,
760
+ "taskCheckpoint",
761
+ "lint-tasks",
762
+ taskCheckpointGate,
763
+ "strict promotion escalated lint-tasks task-checkpoint WARN"
764
+ );
765
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
766
+ }
767
+ }
768
+ if (
769
+ lintTasksSignal &&
770
+ (!taskCheckpointGate || statusSeverity(lintTasksSignalStatus) > statusSeverity(taskCheckpointGateStatus))
771
+ ) {
772
+ if (lintTasksSignalStatus === STATUS.BLOCK) {
773
+ findings.blockers.push("lint-tasks signal is BLOCK.");
774
+ addBlockingGateRecord(
775
+ findings,
776
+ "lint-tasks",
777
+ "lint-tasks",
778
+ lintTasksSignal.details && lintTasksSignal.details.gates ? lintTasksSignal.details.gates : null,
779
+ "lint-tasks signal is BLOCK"
780
+ );
781
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
782
+ } else if (lintTasksSignalStatus === STATUS.WARN) {
783
+ findings.warnings.push("lint-tasks signal is WARN.");
784
+ if (strictPromotion) {
785
+ findings.blockers.push(
786
+ "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-tasks WARN blocks promotion into build."
787
+ );
788
+ addBlockingGateRecord(
789
+ findings,
790
+ "lint-tasks",
791
+ "lint-tasks",
792
+ lintTasksSignal.details && lintTasksSignal.details.gates ? lintTasksSignal.details.gates : null,
793
+ "strict promotion escalated lint-tasks WARN"
794
+ );
795
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
796
+ }
797
+ }
798
+ }
799
+ if (taskCheckpointGate) {
800
+ for (const message of Array.isArray(taskCheckpointGate.compatibility)
801
+ ? taskCheckpointGate.compatibility
802
+ : []) {
803
+ findings.notes.push(`lint-tasks gate taskCheckpoint compatibility: ${message}`);
354
804
  }
355
805
  }
356
806
 
@@ -669,6 +1119,579 @@ function deriveTaskGroupMetadata(tasksMarkdownText, checkpointStatuses) {
669
1119
  return metadata;
670
1120
  }
671
1121
 
1122
+ function normalizeTaskGroupId(value) {
1123
+ return String(value || "").trim();
1124
+ }
1125
+
1126
+ function normalizeResumeCursor(cursor, fallbackGroupIndex = null) {
1127
+ const groupIndex =
1128
+ cursor && Number.isInteger(cursor.groupIndex)
1129
+ ? cursor.groupIndex
1130
+ : Number.isInteger(fallbackGroupIndex)
1131
+ ? fallbackGroupIndex
1132
+ : null;
1133
+ return {
1134
+ groupIndex,
1135
+ nextUncheckedItem:
1136
+ cursor && Object.prototype.hasOwnProperty.call(cursor, "nextUncheckedItem")
1137
+ ? cursor.nextUncheckedItem
1138
+ : null
1139
+ };
1140
+ }
1141
+
1142
+ function normalizeTaskGroupSeedMap(taskGroups) {
1143
+ const byId = new Map();
1144
+ for (const group of Array.isArray(taskGroups) ? taskGroups : []) {
1145
+ const taskGroupId = normalizeTaskGroupId(group && (group.taskGroupId || group.id));
1146
+ if (!taskGroupId || byId.has(taskGroupId)) {
1147
+ continue;
1148
+ }
1149
+ byId.set(taskGroupId, group);
1150
+ }
1151
+ return byId;
1152
+ }
1153
+
1154
+ function findLatestSignalBySurface(signals, surface) {
1155
+ const normalizedSurface = String(surface || "").trim();
1156
+ if (!normalizedSurface) {
1157
+ return null;
1158
+ }
1159
+ return (signals || []).find((signal) => String(signal.surface || "").trim() === normalizedSurface) || null;
1160
+ }
1161
+
1162
+ function summarizeSignalIssues(signal, envelopeItems) {
1163
+ if (!signal) {
1164
+ return [];
1165
+ }
1166
+ const fromEnvelope = Array.isArray(envelopeItems) ? envelopeItems : [];
1167
+ const fromSignal = [
1168
+ ...(Array.isArray(signal.failures) ? signal.failures : []),
1169
+ ...(Array.isArray(signal.warnings) ? signal.warnings : [])
1170
+ ];
1171
+ return dedupeMessages([...fromEnvelope, ...fromSignal].map((item) => String(item || "").trim()).filter(Boolean));
1172
+ }
1173
+
1174
+ function buildTaskGroupImplementerState(taskGroupId, signals, fallbackState) {
1175
+ const fallback = fallbackState && typeof fallbackState === "object" ? fallbackState : {};
1176
+ const signal = findLatestSignalBySurface(signals, `task-execution.${taskGroupId}`);
1177
+ const envelope = signal && signal.details && signal.details.envelope ? signal.details.envelope : null;
1178
+ if (!signal) {
1179
+ return {
1180
+ present: false,
1181
+ signalStatus: null,
1182
+ implementerStatus: fallback.implementerStatus || null,
1183
+ summary: fallback.summary || null,
1184
+ changedFiles: Array.isArray(fallback.changedFiles) ? fallback.changedFiles : [],
1185
+ testEvidence: Array.isArray(fallback.testEvidence) ? fallback.testEvidence : [],
1186
+ concerns: Array.isArray(fallback.concerns) ? fallback.concerns : [],
1187
+ blockers: Array.isArray(fallback.blockers) ? fallback.blockers : [],
1188
+ recordedAt: fallback.recordedAt || null
1189
+ };
1190
+ }
1191
+
1192
+ return {
1193
+ present: true,
1194
+ signalStatus: signal.status || null,
1195
+ implementerStatus: envelope && envelope.status ? envelope.status : fallback.implementerStatus || null,
1196
+ summary: envelope && envelope.summary ? envelope.summary : fallback.summary || null,
1197
+ changedFiles:
1198
+ envelope && Array.isArray(envelope.changedFiles)
1199
+ ? envelope.changedFiles
1200
+ : Array.isArray(fallback.changedFiles)
1201
+ ? fallback.changedFiles
1202
+ : [],
1203
+ testEvidence:
1204
+ envelope && Array.isArray(envelope.testEvidence)
1205
+ ? envelope.testEvidence
1206
+ : Array.isArray(fallback.testEvidence)
1207
+ ? fallback.testEvidence
1208
+ : [],
1209
+ concerns: summarizeSignalIssues(signal, envelope && envelope.concerns),
1210
+ blockers: summarizeSignalIssues(signal, envelope && envelope.blockers),
1211
+ recordedAt: (envelope && envelope.recordedAt) || signal.timestamp || fallback.recordedAt || null
1212
+ };
1213
+ }
1214
+
1215
+ function buildTaskGroupReviewStageState(taskGroupId, stage, signals, fallbackState) {
1216
+ const fallback = fallbackState && typeof fallbackState === "object" ? fallbackState : {};
1217
+ const signal = findLatestSignalBySurface(signals, `task-review.${taskGroupId}.${stage}`);
1218
+ const envelope = signal && signal.details && signal.details.envelope ? signal.details.envelope : null;
1219
+ if (!signal) {
1220
+ return {
1221
+ present: false,
1222
+ status: fallback.status || "missing",
1223
+ summary: fallback.summary || null,
1224
+ reviewer: fallback.reviewer || null,
1225
+ issues: Array.isArray(fallback.issues) ? fallback.issues : [],
1226
+ recordedAt: fallback.recordedAt || null
1227
+ };
1228
+ }
1229
+
1230
+ return {
1231
+ present: true,
1232
+ status: signal.status || "missing",
1233
+ summary: envelope && envelope.summary ? envelope.summary : fallback.summary || null,
1234
+ reviewer: envelope && envelope.reviewer ? envelope.reviewer : fallback.reviewer || null,
1235
+ issues: summarizeSignalIssues(signal, envelope && envelope.issues),
1236
+ recordedAt: (envelope && envelope.recordedAt) || signal.timestamp || fallback.recordedAt || null
1237
+ };
1238
+ }
1239
+
1240
+ function buildTaskGroupReviewState(taskGroup, signals, fallbackState) {
1241
+ const fallback = fallbackState && typeof fallbackState === "object" ? fallbackState : {};
1242
+ const taskGroupId = normalizeTaskGroupId(taskGroup.taskGroupId || taskGroup.id);
1243
+ const required = taskGroup.reviewIntent === true;
1244
+ const spec = buildTaskGroupReviewStageState(taskGroupId, "spec", signals, fallback.spec);
1245
+ const quality = buildTaskGroupReviewStageState(taskGroupId, "quality", signals, fallback.quality);
1246
+
1247
+ return {
1248
+ required,
1249
+ ordering: required ? "spec_then_quality" : "none",
1250
+ spec,
1251
+ quality
1252
+ };
1253
+ }
1254
+
1255
+ function buildEffectiveTaskGroupState(group, planned, implementer, review) {
1256
+ const fallbackCursor = normalizeResumeCursor(planned.resumeCursor, null);
1257
+ const effective = {
1258
+ status: planned.status,
1259
+ nextAction: planned.nextAction,
1260
+ resumeCursor: fallbackCursor,
1261
+ source: "planned",
1262
+ reason: "planned_checklist"
1263
+ };
1264
+
1265
+ if (implementer.present && implementer.signalStatus === STATUS.BLOCK) {
1266
+ return {
1267
+ status: "blocked",
1268
+ nextAction:
1269
+ implementer.blockers[0] ||
1270
+ implementer.summary ||
1271
+ `resolve implementer blocker for task group ${group.taskGroupId}`,
1272
+ resumeCursor: {
1273
+ groupIndex: fallbackCursor.groupIndex,
1274
+ nextUncheckedItem: null,
1275
+ liveFocus: "implementer_block"
1276
+ },
1277
+ source: "implementer",
1278
+ reason: "implementer_block"
1279
+ };
1280
+ }
1281
+
1282
+ const reviewSignalsPresent = review.spec.present || review.quality.present;
1283
+ const plannedCompletion = Number.isFinite(Number(planned.completion))
1284
+ ? Number(planned.completion)
1285
+ : 0;
1286
+ const reviewNearComplete = planned.status !== "completed" && plannedCompletion >= 75;
1287
+ const reviewHardDue = planned.status === "completed" || reviewNearComplete;
1288
+ const reviewContextReady = review.required && (reviewSignalsPresent || reviewHardDue || implementer.present);
1289
+
1290
+ if (reviewContextReady) {
1291
+ if (review.spec.status === STATUS.BLOCK) {
1292
+ return {
1293
+ status: "blocked",
1294
+ nextAction:
1295
+ review.spec.issues[0] || review.spec.summary || `resolve spec review BLOCK for task group ${group.taskGroupId}`,
1296
+ resumeCursor: {
1297
+ groupIndex: fallbackCursor.groupIndex,
1298
+ nextUncheckedItem: null,
1299
+ liveFocus: "spec_review_block"
1300
+ },
1301
+ source: "review",
1302
+ reason: "spec_review_block"
1303
+ };
1304
+ }
1305
+ if (!review.spec.present || review.spec.status === "missing") {
1306
+ if (reviewHardDue || reviewSignalsPresent) {
1307
+ return {
1308
+ status: "review_pending",
1309
+ nextAction: `record spec review PASS or WARN for task group ${group.taskGroupId} before quality review`,
1310
+ resumeCursor: {
1311
+ groupIndex: fallbackCursor.groupIndex,
1312
+ nextUncheckedItem: null,
1313
+ liveFocus: "spec_review_missing"
1314
+ },
1315
+ source: "review",
1316
+ reason: "spec_review_missing"
1317
+ };
1318
+ }
1319
+ } else {
1320
+ const specWarn = review.spec.status === STATUS.WARN;
1321
+ if (review.quality.status === STATUS.BLOCK) {
1322
+ return {
1323
+ status: "blocked",
1324
+ nextAction:
1325
+ review.quality.issues[0] ||
1326
+ review.quality.summary ||
1327
+ `resolve quality review BLOCK for task group ${group.taskGroupId}`,
1328
+ resumeCursor: {
1329
+ groupIndex: fallbackCursor.groupIndex,
1330
+ nextUncheckedItem: null,
1331
+ liveFocus: "quality_review_block"
1332
+ },
1333
+ source: "review",
1334
+ reason: "quality_review_block"
1335
+ };
1336
+ }
1337
+ if (!review.quality.present || review.quality.status === "missing") {
1338
+ if (reviewHardDue) {
1339
+ return {
1340
+ status: "review_pending",
1341
+ nextAction: `record quality review PASS or WARN for task group ${group.taskGroupId}`,
1342
+ resumeCursor: {
1343
+ groupIndex: fallbackCursor.groupIndex,
1344
+ nextUncheckedItem: null,
1345
+ liveFocus: "quality_review_missing"
1346
+ },
1347
+ source: "review",
1348
+ reason: "quality_review_missing"
1349
+ };
1350
+ }
1351
+ if (specWarn) {
1352
+ return {
1353
+ status: "in_progress",
1354
+ nextAction:
1355
+ review.spec.issues[0] ||
1356
+ review.spec.summary ||
1357
+ `resolve spec review follow-up for task group ${group.taskGroupId}`,
1358
+ resumeCursor: {
1359
+ groupIndex: fallbackCursor.groupIndex,
1360
+ nextUncheckedItem: null,
1361
+ liveFocus: "spec_review_warn"
1362
+ },
1363
+ source: "review",
1364
+ reason: "spec_review_warn"
1365
+ };
1366
+ }
1367
+ } else if (review.quality.status === STATUS.WARN) {
1368
+ return {
1369
+ status: "in_progress",
1370
+ nextAction:
1371
+ review.quality.issues[0] ||
1372
+ review.quality.summary ||
1373
+ `resolve quality review follow-up for task group ${group.taskGroupId}`,
1374
+ resumeCursor: {
1375
+ groupIndex: fallbackCursor.groupIndex,
1376
+ nextUncheckedItem: null,
1377
+ liveFocus: "quality_review_warn"
1378
+ },
1379
+ source: "review",
1380
+ reason: "quality_review_warn"
1381
+ };
1382
+ } else if (specWarn) {
1383
+ return {
1384
+ status: "in_progress",
1385
+ nextAction:
1386
+ review.spec.issues[0] ||
1387
+ review.spec.summary ||
1388
+ `resolve spec review follow-up for task group ${group.taskGroupId}`,
1389
+ resumeCursor: {
1390
+ groupIndex: fallbackCursor.groupIndex,
1391
+ nextUncheckedItem: null,
1392
+ liveFocus: "spec_review_warn"
1393
+ },
1394
+ source: "review",
1395
+ reason: "spec_review_warn"
1396
+ };
1397
+ }
1398
+ }
1399
+ }
1400
+
1401
+ if (implementer.present && implementer.signalStatus === STATUS.WARN) {
1402
+ return {
1403
+ status: "in_progress",
1404
+ nextAction:
1405
+ implementer.concerns[0] ||
1406
+ implementer.summary ||
1407
+ `resolve implementer concerns for task group ${group.taskGroupId}`,
1408
+ resumeCursor: {
1409
+ groupIndex: fallbackCursor.groupIndex,
1410
+ nextUncheckedItem: null,
1411
+ liveFocus: "implementer_warn"
1412
+ },
1413
+ source: "implementer",
1414
+ reason: "implementer_warn"
1415
+ };
1416
+ }
1417
+
1418
+ return effective;
1419
+ }
1420
+
1421
+ function deriveTaskGroupRuntimeState(plannedTaskGroups, signals, seedTaskGroups) {
1422
+ const plannedGroups = Array.isArray(plannedTaskGroups) ? plannedTaskGroups : [];
1423
+ const seedMap = normalizeTaskGroupSeedMap(seedTaskGroups);
1424
+
1425
+ return plannedGroups.map((plannedGroup, index) => {
1426
+ const taskGroupId = normalizeTaskGroupId(plannedGroup.taskGroupId || plannedGroup.id);
1427
+ const seed = seedMap.get(taskGroupId) || {};
1428
+ const planned = {
1429
+ status: plannedGroup.status,
1430
+ completion: plannedGroup.completion,
1431
+ checkpointOutcome: plannedGroup.checkpointOutcome,
1432
+ evidence: Array.isArray(plannedGroup.evidence) ? plannedGroup.evidence : [],
1433
+ nextAction: plannedGroup.nextAction,
1434
+ resumeCursor: normalizeResumeCursor(plannedGroup.resumeCursor, index)
1435
+ };
1436
+ const implementer = buildTaskGroupImplementerState(taskGroupId, signals, seed.implementer);
1437
+ const review = buildTaskGroupReviewState(plannedGroup, signals, seed.review);
1438
+ const effective = buildEffectiveTaskGroupState(plannedGroup, planned, implementer, review);
1439
+
1440
+ return {
1441
+ taskGroupId,
1442
+ title: plannedGroup.title,
1443
+ status: effective.status,
1444
+ completion: planned.completion,
1445
+ checkpointOutcome: planned.checkpointOutcome,
1446
+ evidence: planned.evidence,
1447
+ nextAction: effective.nextAction,
1448
+ targetFiles: Array.isArray(plannedGroup.targetFiles) ? plannedGroup.targetFiles : [],
1449
+ fileReferences: Array.isArray(plannedGroup.fileReferences) ? plannedGroup.fileReferences : [],
1450
+ verificationActions: Array.isArray(plannedGroup.verificationActions)
1451
+ ? plannedGroup.verificationActions
1452
+ : [],
1453
+ verificationCommands: Array.isArray(plannedGroup.verificationCommands)
1454
+ ? plannedGroup.verificationCommands
1455
+ : [],
1456
+ executionIntent: Array.isArray(plannedGroup.executionIntent) ? plannedGroup.executionIntent : [],
1457
+ reviewIntent: plannedGroup.reviewIntent === true,
1458
+ testingIntent: plannedGroup.testingIntent === true,
1459
+ codeChangeLikely: plannedGroup.codeChangeLikely === true,
1460
+ resumeCursor: effective.resumeCursor,
1461
+ planned,
1462
+ implementer,
1463
+ review,
1464
+ effective
1465
+ };
1466
+ });
1467
+ }
1468
+
1469
+ function buildTaskGroupMetadataPayload(changeId, checkpointStatuses, taskGroups) {
1470
+ return {
1471
+ version: TASK_GROUP_METADATA_VERSION,
1472
+ changeId,
1473
+ checkpointOutcome: checkpointStatuses[CHECKPOINT_LABELS.TASK] || STATUS.WARN,
1474
+ taskGroups: Array.isArray(taskGroups) ? taskGroups : [],
1475
+ updatedAt: new Date().toISOString()
1476
+ };
1477
+ }
1478
+
1479
+ function loadTaskGroupMetadataFromPath(targetPath) {
1480
+ if (!targetPath || !pathExists(targetPath)) {
1481
+ return null;
1482
+ }
1483
+ try {
1484
+ return JSON.parse(readTextIfExists(targetPath));
1485
+ } catch (_error) {
1486
+ return null;
1487
+ }
1488
+ }
1489
+
1490
+ function resolvePersistedTaskGroupSeed(projectRoot, changeId, persistedRecord, plannedTaskGroups) {
1491
+ const metadataRefs =
1492
+ persistedRecord && persistedRecord.metadataRefs && typeof persistedRecord.metadataRefs === "object"
1493
+ ? persistedRecord.metadataRefs
1494
+ : {};
1495
+ const canonicalPath =
1496
+ metadataRefs.taskGroupsPath || resolveTaskGroupMetadataPath(projectRoot, changeId);
1497
+ const notes = [];
1498
+
1499
+ if (canonicalPath && pathExists(canonicalPath)) {
1500
+ const actualDigest = digestForPath(canonicalPath);
1501
+ const expectedDigest = metadataRefs.taskGroupsDigest || null;
1502
+ if (expectedDigest && actualDigest && expectedDigest !== actualDigest) {
1503
+ notes.push("Canonical task-group runtime state digest mismatch; rebuilding task-group state from artifacts.");
1504
+ return {
1505
+ taskGroups: plannedTaskGroups,
1506
+ notes
1507
+ };
1508
+ }
1509
+
1510
+ const loaded = canonicalPath === resolveTaskGroupMetadataPath(projectRoot, changeId)
1511
+ ? readTaskGroupMetadata(projectRoot, changeId)
1512
+ : loadTaskGroupMetadataFromPath(canonicalPath);
1513
+ if (loaded && Array.isArray(loaded.taskGroups)) {
1514
+ return {
1515
+ taskGroups: loaded.taskGroups,
1516
+ notes
1517
+ };
1518
+ }
1519
+ notes.push("Canonical task-group runtime state is unreadable; rebuilding task-group state from artifacts.");
1520
+ return {
1521
+ taskGroups: plannedTaskGroups,
1522
+ notes
1523
+ };
1524
+ }
1525
+
1526
+ if (Array.isArray(persistedRecord && persistedRecord.taskGroups) && persistedRecord.taskGroups.length > 0) {
1527
+ notes.push("Using legacy embedded task-group state as migration fallback.");
1528
+ return {
1529
+ taskGroups: persistedRecord.taskGroups,
1530
+ notes
1531
+ };
1532
+ }
1533
+
1534
+ notes.push("Canonical task-group runtime state is missing; rebuilding task-group state from artifacts.");
1535
+ return {
1536
+ taskGroups: plannedTaskGroups,
1537
+ notes
1538
+ };
1539
+ }
1540
+
1541
+ function buildGatesWithLiveOverlays(baseGates, completionAudit, disciplineState, verificationFreshness) {
1542
+ const gates = baseGates && typeof baseGates === "object" ? { ...baseGates } : {};
1543
+ if (completionAudit) {
1544
+ gates[HANDOFF_GATES.VERIFY_TO_COMPLETE] =
1545
+ completionAudit.status === "PASS" ? STATUS.PASS : STATUS.WARN;
1546
+ }
1547
+ if (disciplineState && disciplineState.blockers.length > 0) {
1548
+ gates[HANDOFF_GATES.TASKS_TO_BUILD] = STATUS.BLOCK;
1549
+ }
1550
+ if (verificationFreshness && !verificationFreshness.fresh) {
1551
+ gates[HANDOFF_GATES.VERIFY_TO_COMPLETE] = STATUS.BLOCK;
1552
+ }
1553
+ return gates;
1554
+ }
1555
+
1556
+ function finalizeWorkflowView(options = {}) {
1557
+ const findings = {
1558
+ blockers: Array.isArray(options.findings && options.findings.blockers)
1559
+ ? options.findings.blockers.slice()
1560
+ : [],
1561
+ warnings: Array.isArray(options.findings && options.findings.warnings)
1562
+ ? options.findings.warnings.slice()
1563
+ : [],
1564
+ notes: Array.isArray(options.findings && options.findings.notes)
1565
+ ? options.findings.notes.slice()
1566
+ : []
1567
+ };
1568
+ let stageId = options.stageId;
1569
+ const integrityAudit = options.integrityAudit || null;
1570
+ const completionAudit = options.completionAudit || null;
1571
+ const disciplineState = options.disciplineState || null;
1572
+ const verificationFreshness = options.verificationFreshness || null;
1573
+ const planningSignalFreshness =
1574
+ options.planningSignalFreshness && typeof options.planningSignalFreshness === "object"
1575
+ ? options.planningSignalFreshness
1576
+ : {
1577
+ effectiveSignalSummary: options.signalSummary || {},
1578
+ stalePlanningSignals: {},
1579
+ needsRerunSurfaces: []
1580
+ };
1581
+ const taskGroups = deriveTaskGroupRuntimeState(
1582
+ options.plannedTaskGroups,
1583
+ options.changeSignals,
1584
+ options.taskGroupSeed
1585
+ );
1586
+
1587
+ stageId = applyAuditFindings(stageId, findings, integrityAudit, completionAudit);
1588
+ stageId = applyPlanningSignalFreshnessFindings(stageId, findings, planningSignalFreshness);
1589
+ stageId = applyExecutionSignalFindings(stageId, findings, planningSignalFreshness.effectiveSignalSummary || {});
1590
+ applyTaskExecutionAndReviewFindings(findings, options.changeSignals || []);
1591
+
1592
+ if (disciplineState) {
1593
+ findings.blockers.push(...disciplineState.blockers);
1594
+ findings.warnings.push(...disciplineState.warnings);
1595
+ findings.notes.push(...disciplineState.notes);
1596
+ if (disciplineState.blockers.length > 0 && ["build", "verify", "complete"].includes(stageId)) {
1597
+ stageId = options.hasTasksArtifact ? "tasks" : "design";
1598
+ }
1599
+ }
1600
+
1601
+ if (verificationFreshness && !verificationFreshness.fresh && (stageId === "verify" || stageId === "complete")) {
1602
+ findings.blockers.push(
1603
+ "Completion-facing routing requires fresh verification evidence; stale evidence keeps the route in verify."
1604
+ );
1605
+ stageId = "verify";
1606
+ }
1607
+
1608
+ const gates = buildGatesWithLiveOverlays(
1609
+ options.baseGates,
1610
+ completionAudit,
1611
+ disciplineState,
1612
+ verificationFreshness
1613
+ );
1614
+ const executionProfile = deriveExecutionProfile({
1615
+ stage: stageId,
1616
+ taskGroups
1617
+ });
1618
+ let worktreePreflight = null;
1619
+ if (options.changeId && (stageId === "build" || stageId === "verify")) {
1620
+ worktreePreflight = runWorktreePreflight(options.projectRoot, {
1621
+ parallelPreferred: executionProfile.mode === "bounded_parallel"
1622
+ });
1623
+ if (
1624
+ executionProfile.mode === "bounded_parallel" &&
1625
+ worktreePreflight.summary &&
1626
+ worktreePreflight.summary.recommendedIsolation
1627
+ ) {
1628
+ executionProfile.effectiveMode = "serial";
1629
+ executionProfile.rationale = dedupeMessages([
1630
+ ...(executionProfile.rationale || []),
1631
+ "worktree preflight recommends isolation; effective mode downgraded to serial"
1632
+ ]);
1633
+ findings.warnings.push(
1634
+ "Bounded-parallel profile downgraded to serial until worktree isolation is ready or explicitly accepted."
1635
+ );
1636
+ }
1637
+ }
1638
+
1639
+ dedupeFindings(findings);
1640
+ const blockingGate = selectBlockingGateIdentity(findings);
1641
+
1642
+ return buildWorkflowResult({
1643
+ projectRoot: options.projectRoot,
1644
+ changeId: options.changeId,
1645
+ stageId,
1646
+ findings,
1647
+ checkpoints: options.checkpoints || {},
1648
+ gates,
1649
+ audits: {
1650
+ integrity: integrityAudit,
1651
+ completion: completionAudit
1652
+ },
1653
+ routeContext: options.routeContext,
1654
+ source: options.source || "derived",
1655
+ taskGroups,
1656
+ discipline: disciplineState,
1657
+ executionProfile,
1658
+ worktreePreflight,
1659
+ verificationFreshness,
1660
+ blockingGate,
1661
+ needsRerunSurfaces: planningSignalFreshness.needsRerunSurfaces,
1662
+ stalePlanningSignals: planningSignalFreshness.stalePlanningSignals
1663
+ });
1664
+ }
1665
+
1666
+ function selectFocusedTaskGroup(taskGroups) {
1667
+ const priority = {
1668
+ blocked: 0,
1669
+ review_pending: 1,
1670
+ in_progress: 2,
1671
+ pending: 3,
1672
+ completed: 4
1673
+ };
1674
+ const groups = Array.isArray(taskGroups) ? taskGroups : [];
1675
+ return groups
1676
+ .slice()
1677
+ .sort((left, right) => {
1678
+ const leftRank = Object.prototype.hasOwnProperty.call(priority, left.status) ? priority[left.status] : 5;
1679
+ const rightRank = Object.prototype.hasOwnProperty.call(priority, right.status) ? priority[right.status] : 5;
1680
+ if (leftRank !== rightRank) {
1681
+ return leftRank - rightRank;
1682
+ }
1683
+ const leftIndex =
1684
+ left && left.resumeCursor && Number.isInteger(left.resumeCursor.groupIndex)
1685
+ ? left.resumeCursor.groupIndex
1686
+ : Number.MAX_SAFE_INTEGER;
1687
+ const rightIndex =
1688
+ right && right.resumeCursor && Number.isInteger(right.resumeCursor.groupIndex)
1689
+ ? right.resumeCursor.groupIndex
1690
+ : Number.MAX_SAFE_INTEGER;
1691
+ return leftIndex - rightIndex;
1692
+ })[0] || null;
1693
+ }
1694
+
672
1695
  function statusFromFindings(findings) {
673
1696
  if (findings.blockers.length > 0) {
674
1697
  return STATUS.BLOCK;
@@ -690,11 +1713,10 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
690
1713
 
691
1714
  if (!pathExists(projectRoot)) {
692
1715
  findings.blockers.push(`Project path does not exist: ${projectRoot}`);
693
- const stageId = "bootstrap";
694
1716
  return buildWorkflowResult({
695
1717
  projectRoot,
696
1718
  changeId: null,
697
- stageId,
1719
+ stageId: "bootstrap",
698
1720
  findings,
699
1721
  checkpoints: {},
700
1722
  gates: {},
@@ -737,9 +1759,7 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
737
1759
  findings.notes.push(`Available change ids: ${changeIds.join(", ")}`);
738
1760
  activeChangeDir = pickLatestChange(changeDirs);
739
1761
  if (activeChangeDir) {
740
- findings.notes.push(
741
- `Latest inferred change for context only: ${path.basename(activeChangeDir)}`
742
- );
1762
+ findings.notes.push(`Latest inferred change for context only: ${path.basename(activeChangeDir)}`);
743
1763
  }
744
1764
  } else {
745
1765
  findings.blockers.push("No non-empty change directory found under `.da-vinci/changes/`.");
@@ -760,12 +1780,19 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
760
1780
 
761
1781
  const tasksArtifactText = activeChangeDir ? readTextIfExists(path.join(activeChangeDir, "tasks.md")) : "";
762
1782
  const checkpointStatuses = activeChangeDir ? readCheckpointStatuses(activeChangeDir) : {};
1783
+ const plannedTaskGroups = deriveTaskGroupMetadata(tasksArtifactText, checkpointStatuses);
763
1784
  const changeSignals = activeChangeId
764
1785
  ? readExecutionSignals(projectRoot, {
765
1786
  changeId: activeChangeId
766
1787
  })
767
1788
  : [];
768
1789
  const signalSummary = summarizeSignalsBySurface(changeSignals);
1790
+ const planningSignalFreshness = collectPlanningSignalFreshnessState(
1791
+ projectRoot,
1792
+ activeChangeId,
1793
+ signalSummary
1794
+ );
1795
+ const routingSignalSummary = planningSignalFreshness.effectiveSignalSummary;
769
1796
  const disciplineState = activeChangeDir ? inspectDisciplineState(activeChangeDir) : null;
770
1797
  const freshnessArtifactPaths = activeChangeDir
771
1798
  ? {
@@ -776,7 +1803,7 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
776
1803
  verificationPath: path.join(activeChangeDir, "verification.md")
777
1804
  }
778
1805
  : null;
779
- const integrityAudit = collectIntegrityAudit(projectRoot, workflowRoot, signalSummary);
1806
+ const integrityAudit = collectIntegrityAudit(projectRoot, workflowRoot, routingSignalSummary);
780
1807
  const designCheckpointStatus = normalizeCheckpointStatus(checkpointStatuses[CHECKPOINT_LABELS.DESIGN]);
781
1808
  const designSourceCheckpointStatus = normalizeCheckpointStatus(
782
1809
  checkpointStatuses[CHECKPOINT_LABELS.DESIGN_SOURCE]
@@ -786,6 +1813,18 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
786
1813
  );
787
1814
  const mappingCheckpointStatus = normalizeCheckpointStatus(checkpointStatuses[CHECKPOINT_LABELS.MAPPING]);
788
1815
  const taskCheckpointStatus = normalizeCheckpointStatus(checkpointStatuses[CHECKPOINT_LABELS.TASK]);
1816
+ const verificationFreshness = activeChangeId
1817
+ ? collectVerificationFreshness(projectRoot, {
1818
+ changeId: activeChangeId,
1819
+ resolved: { changeDir: activeChangeDir },
1820
+ artifactPaths: freshnessArtifactPaths
1821
+ })
1822
+ : null;
1823
+ const routeContext = {
1824
+ projectRoot,
1825
+ changeId: activeChangeId || requestedChangeId || "change-001",
1826
+ ambiguousChangeSelection
1827
+ };
789
1828
 
790
1829
  if (activeChangeId) {
791
1830
  const persistedSelection = selectPersistedStateForChange(projectRoot, activeChangeId, {
@@ -794,123 +1833,47 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
794
1833
  if (persistedSelection.usable && persistedSelection.changeRecord) {
795
1834
  const persistedRecord = persistedSelection.changeRecord;
796
1835
  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
1836
+ const completionAudit =
1837
+ stageRecord.id === "verify" || stageRecord.id === "complete"
1838
+ ? collectCompletionAudit(projectRoot, workflowRoot, activeChangeId, routingSignalSummary)
1839
+ : null;
1840
+ const persistedSeed = resolvePersistedTaskGroupSeed(
1841
+ projectRoot,
1842
+ activeChangeId,
1843
+ persistedRecord,
1844
+ plannedTaskGroups
819
1845
  );
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({
1846
+ return finalizeWorkflowView({
893
1847
  projectRoot,
894
1848
  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
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
+ ]
907
1859
  },
1860
+ baseGates:
1861
+ persistedRecord && persistedRecord.gates && typeof persistedRecord.gates === "object"
1862
+ ? { ...persistedRecord.gates }
1863
+ : {},
1864
+ checkpoints: checkpointStatuses,
1865
+ routeContext,
908
1866
  source: "persisted",
909
- taskGroups: persistedTaskGroups,
910
- discipline: disciplineState,
911
- executionProfile,
912
- worktreePreflight,
913
- verificationFreshness
1867
+ taskGroupSeed: persistedSeed.taskGroups,
1868
+ plannedTaskGroups,
1869
+ changeSignals,
1870
+ signalSummary,
1871
+ planningSignalFreshness,
1872
+ integrityAudit,
1873
+ completionAudit,
1874
+ disciplineState,
1875
+ verificationFreshness,
1876
+ hasTasksArtifact: artifactState.tasks
914
1877
  });
915
1878
  }
916
1879
 
@@ -920,8 +1883,6 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
920
1883
  "parse-error": "Persisted workflow state is unreadable; deriving from artifacts.",
921
1884
  "version-mismatch": "Persisted workflow state version mismatch; deriving from artifacts.",
922
1885
  "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
1886
  "fingerprint-mismatch": "Persisted workflow state conflicts with artifact truth; deriving from artifacts."
926
1887
  };
927
1888
  const message = reasonMessage[persistedSelection.reason];
@@ -931,38 +1892,17 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
931
1892
  }
932
1893
  }
933
1894
 
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 = {
1895
+ const baseFindings = {
1896
+ blockers: findings.blockers.slice(),
1897
+ warnings: findings.warnings.slice(),
1898
+ notes: findings.notes.slice()
1899
+ };
1900
+ const stageId = deriveStageFromArtifacts(artifactState, checkpointStatuses, baseFindings);
1901
+ const completionAudit =
1902
+ stageId === "verify" || stageId === "complete"
1903
+ ? collectCompletionAudit(projectRoot, workflowRoot, activeChangeId, routingSignalSummary)
1904
+ : null;
1905
+ const baseGates = {
966
1906
  [HANDOFF_GATES.BREAKDOWN_TO_DESIGN]:
967
1907
  artifactState.proposal && artifactState.specFiles.length > 0 ? STATUS.PASS : STATUS.BLOCK,
968
1908
  [HANDOFF_GATES.DESIGN_TO_TASKS]: mergeStatuses([
@@ -986,90 +1926,72 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
986
1926
  [HANDOFF_GATES.VERIFY_TO_COMPLETE]:
987
1927
  completionAudit && completionAudit.status === "PASS" ? STATUS.PASS : STATUS.WARN
988
1928
  };
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
1929
 
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
-
1040
- const derivedResult = buildWorkflowResult({
1930
+ const derivedResult = finalizeWorkflowView({
1041
1931
  projectRoot,
1042
1932
  changeId: activeChangeId,
1043
1933
  stageId,
1044
- findings,
1934
+ findings: baseFindings,
1935
+ baseGates,
1045
1936
  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
1937
+ routeContext,
1938
+ source: "derived",
1939
+ taskGroupSeed: plannedTaskGroups,
1940
+ plannedTaskGroups,
1941
+ changeSignals,
1942
+ signalSummary,
1943
+ planningSignalFreshness,
1944
+ integrityAudit,
1945
+ completionAudit,
1946
+ disciplineState,
1947
+ verificationFreshness,
1948
+ hasTasksArtifact: artifactState.tasks
1061
1949
  });
1062
1950
 
1063
1951
  if (activeChangeId && !ambiguousChangeSelection) {
1952
+ let metadataRef = {
1953
+ taskGroupsPath: null,
1954
+ taskGroupsDigest: null
1955
+ };
1956
+ try {
1957
+ const metadataRecord = writeTaskGroupMetadata(
1958
+ projectRoot,
1959
+ activeChangeId,
1960
+ buildTaskGroupMetadataPayload(activeChangeId, checkpointStatuses, derivedResult.taskGroups)
1961
+ );
1962
+ if (metadataRecord && metadataRecord.path) {
1963
+ metadataRef = {
1964
+ taskGroupsPath: metadataRecord.path,
1965
+ taskGroupsDigest: metadataRecord.digest || null
1966
+ };
1967
+ derivedResult.notes = dedupeMessages([
1968
+ ...derivedResult.notes,
1969
+ `Task-group metadata refreshed: ${metadataRecord.path}`
1970
+ ]);
1971
+ }
1972
+ } catch (error) {
1973
+ derivedResult.warnings = dedupeMessages([
1974
+ ...derivedResult.warnings,
1975
+ `Failed to persist canonical task-group metadata: ${
1976
+ error && error.message ? error.message : "unknown write error"
1977
+ }`
1978
+ ]);
1979
+ if (derivedResult.checkpointState === STATUS.PASS) {
1980
+ derivedResult.checkpointState = STATUS.WARN;
1981
+ }
1982
+ if (derivedResult.status === STATUS.PASS) {
1983
+ derivedResult.status = STATUS.WARN;
1984
+ }
1985
+ }
1986
+
1064
1987
  try {
1065
1988
  persistDerivedWorkflowResult(projectRoot, activeChangeId, derivedResult, {
1066
- metadataRefs: {
1067
- taskGroupsPath: activeChangeId
1068
- ? path.join(projectRoot, ".da-vinci", "state", "task-groups", `${activeChangeId}.json`)
1069
- : null
1070
- }
1989
+ metadataRefs: metadataRef
1071
1990
  });
1072
- derivedResult.notes.push("workflow-status persisted a fresh derived workflow snapshot.");
1991
+ derivedResult.notes = dedupeMessages([
1992
+ ...derivedResult.notes,
1993
+ "workflow-status persisted a fresh derived workflow snapshot."
1994
+ ]);
1073
1995
  } catch (error) {
1074
1996
  const message =
1075
1997
  error && error.message
@@ -1121,7 +2043,13 @@ function buildWorkflowResult(params) {
1121
2043
  discipline: params.discipline || null,
1122
2044
  executionProfile: params.executionProfile || null,
1123
2045
  worktreePreflight: params.worktreePreflight || null,
1124
- verificationFreshness: params.verificationFreshness || null
2046
+ verificationFreshness: params.verificationFreshness || null,
2047
+ blockingGate: params.blockingGate || null,
2048
+ needsRerunSurfaces: Array.isArray(params.needsRerunSurfaces) ? params.needsRerunSurfaces : [],
2049
+ stalePlanningSignals:
2050
+ params.stalePlanningSignals && typeof params.stalePlanningSignals === "object"
2051
+ ? params.stalePlanningSignals
2052
+ : {}
1125
2053
  };
1126
2054
  }
1127
2055
 
@@ -1142,6 +2070,12 @@ function formatWorkflowStatusReport(result) {
1142
2070
  } else {
1143
2071
  lines.push("Next route: none");
1144
2072
  }
2073
+ if (result.blockingGate && result.blockingGate.id) {
2074
+ lines.push(`Blocking gate: ${result.blockingGate.id} (${result.blockingGate.surface || "unknown"})`);
2075
+ }
2076
+ if (Array.isArray(result.needsRerunSurfaces) && result.needsRerunSurfaces.length > 0) {
2077
+ lines.push(`Needs rerun surfaces: ${result.needsRerunSurfaces.join(", ")}`);
2078
+ }
1145
2079
 
1146
2080
  if (result.discipline && result.discipline.designApproval) {
1147
2081
  lines.push(`Discipline design approval: ${result.discipline.designApproval.state}`);
@@ -1196,7 +2130,7 @@ function formatWorkflowStatusReport(result) {
1196
2130
  lines.push("Task-group metadata:");
1197
2131
  for (const group of result.taskGroups) {
1198
2132
  lines.push(
1199
- `- ${group.taskGroupId || group.id || "group"}: ${group.status || "unknown"} (${group.completion || 0}%)`
2133
+ `- ${group.taskGroupId || group.id || "group"}: ${group.status || "unknown"} (${group.completion || 0}%) -> ${group.nextAction || "continue"}`
1200
2134
  );
1201
2135
  }
1202
2136
  }
@@ -1222,6 +2156,12 @@ function formatNextStepReport(result) {
1222
2156
  if (result.nextStep.reason) {
1223
2157
  lines.push(`Reason: ${result.nextStep.reason}`);
1224
2158
  }
2159
+ if (result.blockingGate && result.blockingGate.id) {
2160
+ lines.push(`Blocking gate: ${result.blockingGate.id} (${result.blockingGate.surface || "unknown"})`);
2161
+ }
2162
+ if (Array.isArray(result.needsRerunSurfaces) && result.needsRerunSurfaces.length > 0) {
2163
+ lines.push(`Needs rerun surfaces: ${result.needsRerunSurfaces.join(", ")}`);
2164
+ }
1225
2165
  if (result.executionProfile) {
1226
2166
  lines.push(
1227
2167
  `Execution profile: ${result.executionProfile.mode}${
@@ -1238,9 +2178,7 @@ function formatNextStepReport(result) {
1238
2178
  lines.push("Completion evidence is stale; stay in verify until fresh evidence is recorded.");
1239
2179
  }
1240
2180
  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");
2181
+ const active = selectFocusedTaskGroup(result.taskGroups);
1244
2182
  if (active) {
1245
2183
  lines.push(`Task-group focus: ${active.taskGroupId || active.id} -> ${active.nextAction || "continue"}`);
1246
2184
  }