@xenonbyte/da-vinci-workflow 0.2.8 → 0.2.9

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.
@@ -1,46 +1,41 @@
1
- const fs = require("fs");
2
1
  const path = require("path");
3
2
  const { auditProject } = require("./audit");
4
- const {
5
- parseCheckpointStatusMap,
6
- normalizeCheckpointLabel,
7
- parseDisciplineMarkers,
8
- DISCIPLINE_MARKER_NAMES
9
- } = require("./audit-parsers");
10
3
  const { pathExists, readTextIfExists, dedupeMessages } = require("./utils");
11
4
  const {
12
- parseTasksArtifact,
13
5
  listImmediateDirs,
14
6
  hasAnyFile,
15
7
  pickLatestChange,
16
8
  detectSpecFiles
17
9
  } = require("./planning-parsers");
10
+ const { STATUS, getStageById } = require("./workflow-contract");
11
+ const { readCheckpointStatuses } = require("./workflow-stage");
12
+ const {
13
+ buildPersistedWorkflowBaseView,
14
+ buildDerivedWorkflowBaseView
15
+ } = require("./workflow-base-view");
18
16
  const {
19
- STATUS,
20
- CHECKPOINT_LABELS,
21
- HANDOFF_GATES,
22
- getStageById,
23
- mergeStatuses
24
- } = require("./workflow-contract");
17
+ deriveTaskGroupMetadata,
18
+ deriveTaskGroupRuntimeState,
19
+ buildTaskGroupMetadataPayload,
20
+ resolvePersistedTaskGroupSeed,
21
+ selectFocusedTaskGroup
22
+ } = require("./workflow-task-groups");
25
23
  const {
26
24
  selectPersistedStateForChange,
27
25
  persistDerivedWorkflowResult,
28
- resolveWorkflowStatePath,
29
- resolveTaskGroupMetadataPath,
30
26
  writeTaskGroupMetadata,
31
- readTaskGroupMetadata,
32
- digestForPath,
33
27
  sanitizePersistedNotes
34
28
  } = require("./workflow-persisted-state");
35
29
  const {
36
30
  readExecutionSignals,
37
- summarizeSignalsBySurface,
38
- listSignalsBySurfacePrefix
31
+ summarizeSignalsBySurface
39
32
  } = require("./execution-signals");
40
- const { evaluatePlanningSignalFreshness } = require("./planning-signal-freshness");
41
- const { deriveExecutionProfile } = require("./execution-profile");
42
33
  const { collectVerificationFreshness } = require("./verify");
43
- const { runWorktreePreflight } = require("./worktree-preflight");
34
+ const {
35
+ collectPlanningSignalFreshnessState,
36
+ inspectDisciplineState,
37
+ applyWorkflowOverlays
38
+ } = require("./workflow-overlay");
44
39
  const {
45
40
  formatPathRef,
46
41
  emitWorkflowDecisionTraces,
@@ -48,18 +43,6 @@ const {
48
43
  } = require("./workflow-decision-trace");
49
44
 
50
45
  const MAX_REPORTED_MESSAGES = 3;
51
- // Task-group metadata is versioned independently from workflow route snapshots.
52
- const TASK_GROUP_METADATA_VERSION = 2;
53
- const TRACEABLE_TASK_GROUP_FOCUS_REASONS = new Set([
54
- "implementer_block",
55
- "implementer_warn",
56
- "spec_review_missing",
57
- "spec_review_block",
58
- "spec_review_warn",
59
- "quality_review_missing",
60
- "quality_review_block",
61
- "quality_review_warn"
62
- ]);
63
46
  const PERSISTED_STATE_TRACE_KEYS = Object.freeze({
64
47
  missing: "fallback_missing",
65
48
  "parse-error": "fallback_parse_error",
@@ -67,29 +50,14 @@ const PERSISTED_STATE_TRACE_KEYS = Object.freeze({
67
50
  "change-missing": "fallback_change_missing",
68
51
  "fingerprint-mismatch": "fallback_fingerprint_mismatch"
69
52
  });
70
- const TASK_GROUP_SEED_TRACE_KEYS = Object.freeze({
71
- missing: "seed_missing",
72
- unreadable: "seed_unreadable",
73
- "digest-mismatch": "seed_digest_mismatch",
74
- legacy: "seed_legacy_embedded"
75
- });
76
- const BLOCKING_GATE_PRIORITY = Object.freeze([
77
- "clarify",
78
- "scenarioQuality",
79
- "analyze",
80
- "taskCheckpoint",
81
- "stalePlanningSignal",
82
- "principleInheritance",
83
- "lint-tasks",
84
- "lint-spec",
85
- "scope-check"
86
- ]);
87
- const PLANNING_SIGNAL_PROMOTION_FALLBACKS = Object.freeze({
88
- "lint-spec": "breakdown",
89
- "scope-check": "tasks",
90
- "lint-tasks": "tasks"
91
- });
92
53
 
54
+ /**
55
+ * Append a workflow decision-trace record when tracing is enabled for the current run.
56
+ *
57
+ * @param {Array<object> | null} records
58
+ * @param {object} record
59
+ * @returns {void}
60
+ */
93
61
  function recordWorkflowDecision(records, record) {
94
62
  if (!Array.isArray(records) || !record || typeof record !== "object") {
95
63
  return;
@@ -97,59 +65,13 @@ function recordWorkflowDecision(records, record) {
97
65
  records.push(record);
98
66
  }
99
67
 
100
- function buildTaskGroupFocusEvidenceRefs(taskGroupId, reason) {
101
- if (reason === "implementer_block" || reason === "implementer_warn") {
102
- return [`signal:task-execution.${taskGroupId}`];
103
- }
104
- if (
105
- reason === "spec_review_block" ||
106
- reason === "spec_review_warn"
107
- ) {
108
- return [`signal:task-review.${taskGroupId}.spec`];
109
- }
110
- if (
111
- reason === "quality_review_block" ||
112
- reason === "quality_review_warn"
113
- ) {
114
- return [`signal:task-review.${taskGroupId}.quality`];
115
- }
116
- return [];
117
- }
118
-
119
- function buildTaskGroupFocusReasonSummary(taskGroupId, reason) {
120
- switch (reason) {
121
- case "implementer_block":
122
- return `Implementer BLOCK overrides planned checklist focus for task group ${taskGroupId}.`;
123
- case "implementer_warn":
124
- return `Implementer WARN overrides planned checklist focus for task group ${taskGroupId}.`;
125
- case "spec_review_missing":
126
- return `Missing spec review takes focus over planned checklist work for task group ${taskGroupId}.`;
127
- case "spec_review_block":
128
- return `Spec review BLOCK takes focus over planned checklist work for task group ${taskGroupId}.`;
129
- case "spec_review_warn":
130
- return `Spec review WARN follow-up takes focus over planned checklist work for task group ${taskGroupId}.`;
131
- case "quality_review_missing":
132
- return `Missing quality review takes focus over planned checklist work for task group ${taskGroupId}.`;
133
- case "quality_review_block":
134
- return `Quality review BLOCK takes focus over planned checklist work for task group ${taskGroupId}.`;
135
- case "quality_review_warn":
136
- return `Quality review WARN follow-up takes focus over planned checklist work for task group ${taskGroupId}.`;
137
- default:
138
- return "";
139
- }
140
- }
141
-
142
- function collectVerificationFreshnessEvidenceRefs(verificationFreshness) {
143
- const surfaces =
144
- verificationFreshness && verificationFreshness.surfaces && typeof verificationFreshness.surfaces === "object"
145
- ? verificationFreshness.surfaces
146
- : {};
147
- return Object.keys(surfaces)
148
- .filter((surface) => surfaces[surface] && surfaces[surface].stale && surfaces[surface].present)
149
- .sort()
150
- .map((surface) => `signal:${surface}`);
151
- }
152
-
68
+ /**
69
+ * Flush decision-trace records and attach non-fatal trace diagnostics to the workflow result.
70
+ *
71
+ * @param {object} result
72
+ * @param {{ env?: object, surface?: string, projectRoot?: string, records?: Array<object> | null }} [options]
73
+ * @returns {object}
74
+ */
153
75
  function finalizeResultWithWorkflowDecisionTracing(result, options = {}) {
154
76
  const traceResult = emitWorkflowDecisionTraces({
155
77
  env: options.env,
@@ -177,1866 +99,298 @@ function finalizeResultWithWorkflowDecisionTracing(result, options = {}) {
177
99
  ? traceResult.error.message
178
100
  : traceResult.error
179
101
  ? String(traceResult.error)
180
- : null
181
- };
182
- }
183
- return result;
184
- }
185
-
186
- function summarizeAudit(result) {
187
- if (!result) {
188
- return null;
189
- }
190
-
191
- return {
192
- status: result.status,
193
- failures: Array.isArray(result.failures) ? result.failures.slice(0, MAX_REPORTED_MESSAGES) : [],
194
- warnings: Array.isArray(result.warnings) ? result.warnings.slice(0, MAX_REPORTED_MESSAGES) : [],
195
- notes: Array.isArray(result.notes) ? result.notes.slice(0, MAX_REPORTED_MESSAGES) : []
196
- };
197
- }
198
-
199
- function dedupeFindings(findings) {
200
- findings.blockers = dedupeMessages(findings.blockers);
201
- findings.warnings = dedupeMessages(findings.warnings);
202
- findings.notes = dedupeMessages(findings.notes);
203
- }
204
-
205
- function resolveDisciplineGateMode() {
206
- const strictEnv = String(process.env.DA_VINCI_DISCIPLINE_REQUIRE_APPROVAL || "").trim();
207
- const strict = strictEnv === "1" || /^true$/i.test(strictEnv);
208
- return {
209
- strict
210
- };
211
- }
212
-
213
- function isStrictPromotionEnabled() {
214
- const raw = String(process.env.DA_VINCI_DISCIPLINE_STRICT_PROMOTION || "").trim();
215
- return raw === "1" || /^true$/i.test(raw);
216
- }
217
-
218
- function fallbackStageIfBeyond(currentStageId, targetStageId) {
219
- const current = getStageById(currentStageId);
220
- const target = getStageById(targetStageId);
221
- if (!current || !target) {
222
- return currentStageId;
223
- }
224
- if (current.order > target.order) {
225
- return target.id;
226
- }
227
- return current.id;
228
- }
229
-
230
- function ensureGateTracking(findings) {
231
- if (!Array.isArray(findings.blockingGates)) {
232
- findings.blockingGates = [];
233
- }
234
- }
235
-
236
- function collectGateEvidenceRefs(gate) {
237
- return Array.isArray(gate && gate.evidence) ? gate.evidence.slice(0, MAX_REPORTED_MESSAGES) : [];
238
- }
239
-
240
- function addBlockingGateRecord(findings, gateId, surface, gate, reason) {
241
- ensureGateTracking(findings);
242
- findings.blockingGates.push({
243
- id: gateId,
244
- surface,
245
- reason: String(reason || "").trim(),
246
- evidence: collectGateEvidenceRefs(gate)
247
- });
248
- }
249
-
250
- function collectPlanningSignalFreshnessState(projectRoot, changeId, signalSummary) {
251
- const effectiveSignalSummary =
252
- signalSummary && typeof signalSummary === "object"
253
- ? { ...signalSummary }
254
- : {};
255
- const stalePlanningSignals = {};
256
-
257
- if (!changeId) {
258
- return {
259
- effectiveSignalSummary,
260
- stalePlanningSignals,
261
- needsRerunSurfaces: []
262
- };
263
- }
264
-
265
- for (const surface of Object.keys(PLANNING_SIGNAL_PROMOTION_FALLBACKS)) {
266
- const signal = effectiveSignalSummary[surface];
267
- if (!signal) {
268
- continue;
269
- }
270
- const freshness = evaluatePlanningSignalFreshness(projectRoot, {
271
- changeId,
272
- surface,
273
- signal
274
- });
275
- if (!freshness.applicable || freshness.fresh) {
276
- continue;
277
- }
278
- stalePlanningSignals[surface] = {
279
- surface,
280
- reasons: Array.isArray(freshness.reasons) ? freshness.reasons.slice() : [],
281
- signalStatus: normalizeSignalStatus(signal.status),
282
- signalTimestamp: signal && signal.timestamp ? String(signal.timestamp) : null,
283
- signalTimestampMs: freshness.signalTimestampMs,
284
- latestArtifactMtimeMs: freshness.latestArtifactMtimeMs,
285
- latestArtifactTimestamp:
286
- freshness.latestArtifactMtimeMs > 0 ? new Date(freshness.latestArtifactMtimeMs).toISOString() : null,
287
- staleByMs: freshness.staleByMs
288
- };
289
- delete effectiveSignalSummary[surface];
290
- }
291
-
292
- return {
293
- effectiveSignalSummary,
294
- stalePlanningSignals,
295
- needsRerunSurfaces: Object.keys(stalePlanningSignals).sort()
296
- };
297
- }
298
-
299
- function applyPlanningSignalFreshnessFindings(stageId, findings, planningSignalFreshness, decisionTraceRecords) {
300
- let nextStageId = stageId;
301
- const strictPromotion = isStrictPromotionEnabled();
302
- const stalePlanningSignals =
303
- planningSignalFreshness && planningSignalFreshness.stalePlanningSignals
304
- ? planningSignalFreshness.stalePlanningSignals
305
- : {};
306
-
307
- for (const surface of Object.keys(stalePlanningSignals).sort()) {
308
- const freshness = stalePlanningSignals[surface];
309
- const reasonText =
310
- Array.isArray(freshness.reasons) && freshness.reasons.length > 0
311
- ? freshness.reasons.join(", ")
312
- : "unknown_staleness_reason";
313
- findings.warnings.push(
314
- `Stale ${surface} planning signal requires rerun before routing can rely on it (${reasonText}).`
315
- );
316
- if (!strictPromotion) {
317
- recordWorkflowDecision(decisionTraceRecords, {
318
- decisionFamily: "planning_signal_freshness",
319
- decisionKey: "stale_signal_rerun_required",
320
- outcome: "rerun_required",
321
- reasonSummary: `Stale ${surface} planning signal requires rerun before routing can rely on it.`,
322
- context: {
323
- planningSurface: surface,
324
- strictPromotion: false,
325
- signalStatus: freshness.signalStatus || null,
326
- staleByMs: Number.isFinite(freshness.staleByMs) ? freshness.staleByMs : null,
327
- reasons: Array.isArray(freshness.reasons) ? freshness.reasons : []
328
- },
329
- evidenceRefs: [`signal:${surface}`]
330
- });
331
- continue;
332
- }
333
- findings.blockers.push(
334
- `DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; stale ${surface} planning signal blocks promotion until ${surface} is rerun.`
335
- );
336
- addBlockingGateRecord(
337
- findings,
338
- "stalePlanningSignal",
339
- surface,
340
- null,
341
- `strict promotion requires rerun for stale ${surface} planning signal`
342
- );
343
- const fallbackStageId = PLANNING_SIGNAL_PROMOTION_FALLBACKS[surface] || nextStageId;
344
- recordWorkflowDecision(decisionTraceRecords, {
345
- decisionFamily: "planning_signal_freshness",
346
- decisionKey: "stale_signal_strict_fallback",
347
- outcome: "downgraded",
348
- reasonSummary: `Strict promotion forces routing fallback because ${surface} planning signal is stale.`,
349
- context: {
350
- planningSurface: surface,
351
- strictPromotion: true,
352
- signalStatus: freshness.signalStatus || null,
353
- fallbackStage: fallbackStageId,
354
- staleByMs: Number.isFinite(freshness.staleByMs) ? freshness.staleByMs : null,
355
- reasons: Array.isArray(freshness.reasons) ? freshness.reasons : []
356
- },
357
- evidenceRefs: [`signal:${surface}`]
358
- });
359
- nextStageId = fallbackStageIfBeyond(nextStageId, fallbackStageId);
360
- }
361
-
362
- return nextStageId;
363
- }
364
-
365
- function selectBlockingGateIdentity(findings) {
366
- const candidates = Array.isArray(findings && findings.blockingGates) ? findings.blockingGates : [];
367
- if (candidates.length === 0) {
368
- return null;
369
- }
370
- const sorted = candidates
371
- .slice()
372
- .sort((left, right) => {
373
- const leftPriority = BLOCKING_GATE_PRIORITY.indexOf(left.id);
374
- const rightPriority = BLOCKING_GATE_PRIORITY.indexOf(right.id);
375
- const normalizedLeft = leftPriority === -1 ? Number.MAX_SAFE_INTEGER : leftPriority;
376
- const normalizedRight = rightPriority === -1 ? Number.MAX_SAFE_INTEGER : rightPriority;
377
- if (normalizedLeft !== normalizedRight) {
378
- return normalizedLeft - normalizedRight;
379
- }
380
- return String(left.surface || "").localeCompare(String(right.surface || ""));
381
- });
382
- return sorted[0];
383
- }
384
-
385
- function getSignalGate(signal, gateKey) {
386
- if (!signal || !signal.details || !signal.details.gates) {
387
- return null;
388
- }
389
- const gates = signal.details.gates;
390
- if (!gates || typeof gates !== "object") {
391
- return null;
392
- }
393
- if (!gateKey) {
394
- return null;
395
- }
396
- return gates[gateKey] && typeof gates[gateKey] === "object" ? gates[gateKey] : null;
397
- }
398
-
399
- function normalizeSignalStatus(status) {
400
- const normalized = String(status || "").trim().toUpperCase();
401
- if (normalized === STATUS.BLOCK || normalized === STATUS.WARN || normalized === STATUS.PASS) {
402
- return normalized;
403
- }
404
- return "";
405
- }
406
-
407
- function statusSeverity(status) {
408
- if (status === STATUS.BLOCK) {
409
- return 2;
410
- }
411
- if (status === STATUS.WARN) {
412
- return 1;
413
- }
414
- if (status === STATUS.PASS) {
415
- return 0;
416
- }
417
- return -1;
418
- }
419
-
420
- function clampGateStatusBySignal(gateStatus, signalStatus) {
421
- const normalizedGate = normalizeSignalStatus(gateStatus);
422
- const normalizedSignal = normalizeSignalStatus(signalStatus);
423
- if (!normalizedGate && normalizedSignal) {
424
- return normalizedSignal;
425
- }
426
- if (normalizedGate && !normalizedSignal) {
427
- return normalizedGate;
428
- }
429
- if (!normalizedGate && !normalizedSignal) {
430
- return STATUS.PASS;
431
- }
432
- return statusSeverity(normalizedGate) >= statusSeverity(normalizedSignal)
433
- ? normalizedGate
434
- : normalizedSignal;
435
- }
436
-
437
- function resolveEffectiveGateStatus(gate, signal) {
438
- const gateStatus = normalizeSignalStatus(gate && gate.status);
439
- if (gateStatus) {
440
- return gateStatus;
441
- }
442
- const signalStatus = normalizeSignalStatus(signal && signal.status);
443
- return signalStatus || STATUS.PASS;
444
- }
445
-
446
- function statusTokenMatches(status, acceptedTokens) {
447
- const normalized = String(status || "").trim().toUpperCase();
448
- return acceptedTokens.includes(normalized);
449
- }
450
-
451
- function inspectDisciplineState(changeDir) {
452
- const designPath = path.join(changeDir, "design.md");
453
- const pencilDesignPath = path.join(changeDir, "pencil-design.md");
454
- const pencilBindingsPath = path.join(changeDir, "pencil-bindings.md");
455
- const tasksPath = path.join(changeDir, "tasks.md");
456
- const designText = readTextIfExists(designPath);
457
- const tasksText = readTextIfExists(tasksPath);
458
- const designMarkers = parseDisciplineMarkers(designText);
459
- const taskMarkers = parseDisciplineMarkers(tasksText);
460
- const gateMode = resolveDisciplineGateMode();
461
- const hasAnyMarker =
462
- designMarkers.ordered.length > 0 ||
463
- taskMarkers.ordered.length > 0 ||
464
- designMarkers.malformed.length > 0 ||
465
- taskMarkers.malformed.length > 0;
466
-
467
- const designApproval =
468
- designMarkers.markers[DISCIPLINE_MARKER_NAMES.designApproval] ||
469
- taskMarkers.markers[DISCIPLINE_MARKER_NAMES.designApproval] ||
470
- null;
471
- const planSelfReview = taskMarkers.markers[DISCIPLINE_MARKER_NAMES.planSelfReview] || null;
472
- const operatorReviewAck = taskMarkers.markers[DISCIPLINE_MARKER_NAMES.operatorReviewAck] || null;
473
-
474
- const blockers = [];
475
- const warnings = [];
476
- const notes = [];
477
- const designApprovalArtifacts = [
478
- { label: "design.md", path: designPath },
479
- { label: "pencil-design.md", path: pencilDesignPath },
480
- { label: "pencil-bindings.md", path: pencilBindingsPath }
481
- ];
482
-
483
- for (const malformed of [...designMarkers.malformed, ...taskMarkers.malformed]) {
484
- warnings.push(`Malformed discipline marker at line ${malformed.line}: ${malformed.reason}`);
485
- }
486
-
487
- let designApprovalState = "missing";
488
- let designApprovalStale = false;
489
- if (designApproval) {
490
- if (!statusTokenMatches(designApproval.status, ["APPROVED", "PASS", "ACCEPTED"])) {
491
- designApprovalState = "rejected";
492
- blockers.push(
493
- `Design approval marker is not approved (${designApproval.status}); keep workflow in tasks/design.`
494
- );
495
- } else {
496
- designApprovalState = "approved";
497
- if (designApproval.time) {
498
- const approvalMs = Date.parse(designApproval.time);
499
- const staleArtifacts = [];
500
- if (Number.isFinite(approvalMs)) {
501
- for (const artifact of designApprovalArtifacts) {
502
- let artifactMtimeMs = 0;
503
- try {
504
- artifactMtimeMs = fs.statSync(artifact.path).mtimeMs;
505
- } catch (_error) {
506
- continue;
507
- }
508
- if (artifactMtimeMs > approvalMs) {
509
- staleArtifacts.push(artifact.label);
510
- }
511
- }
512
- }
513
- if (staleArtifacts.length > 0) {
514
- designApprovalState = "stale";
515
- designApprovalStale = true;
516
- blockers.push(
517
- `Design approval marker is stale because design artifacts changed after approval timestamp: ${staleArtifacts.join(", ")}.`
518
- );
519
- }
520
- } else {
521
- warnings.push("Design approval marker is missing timestamp; staleness checks are limited.");
522
- }
523
- }
524
- } else if (hasAnyMarker || gateMode.strict) {
525
- blockers.push(
526
- "Missing required design approval marker (`- design approval: APPROVED @ <ISO8601>`) for disciplined handoff."
527
- );
528
- } else {
529
- warnings.push(
530
- "Design approval marker is missing; legacy compatibility mode keeps this advisory. Set DA_VINCI_DISCIPLINE_REQUIRE_APPROVAL=1 to enforce."
531
- );
532
- notes.push("Legacy compatibility mode: missing discipline markers are advisory.");
533
- }
534
-
535
- if (!planSelfReview) {
536
- warnings.push("Missing plan self-review marker in tasks.md (`- plan self review: PASS @ <ISO8601>`).");
537
- } else if (!statusTokenMatches(planSelfReview.status, ["PASS", "APPROVED", "DONE"])) {
538
- warnings.push(`Plan self-review marker is not PASS (${planSelfReview.status}).`);
539
- }
540
-
541
- if (!operatorReviewAck) {
542
- warnings.push("Missing operator review acknowledgment marker in tasks.md (`- operator review ack: ACKNOWLEDGED @ <ISO8601>`).");
543
- } else if (!statusTokenMatches(operatorReviewAck.status, ["ACKNOWLEDGED", "ACK", "CONFIRMED", "APPROVED"])) {
544
- warnings.push(`Operator review acknowledgment marker is not acknowledged (${operatorReviewAck.status}).`);
545
- }
546
-
547
- return {
548
- strictMode: gateMode.strict,
549
- hasAnyMarker,
550
- designApproval: {
551
- state: designApprovalState,
552
- stale: designApprovalStale,
553
- marker: designApproval
554
- },
555
- planSelfReview: {
556
- marker: planSelfReview
557
- },
558
- operatorReviewAck: {
559
- marker: operatorReviewAck
560
- },
561
- malformed: [...designMarkers.malformed, ...taskMarkers.malformed],
562
- blockers,
563
- warnings,
564
- notes
565
- };
566
- }
567
-
568
- function mergeSignalBySurface(signals) {
569
- const summary = {};
570
- for (const signal of signals || []) {
571
- const key = String(signal.surface || "").trim();
572
- if (!key || summary[key]) {
573
- continue;
574
- }
575
- summary[key] = signal;
576
- }
577
- return summary;
578
- }
579
-
580
- function applyTaskExecutionAndReviewFindings(findings, signals) {
581
- const taskExecutionSignals = listSignalsBySurfacePrefix(signals, "task-execution.");
582
- const taskReviewSignals = listSignalsBySurfacePrefix(signals, "task-review.");
583
- const latestTaskExecution = mergeSignalBySurface(taskExecutionSignals);
584
- const latestTaskReview = mergeSignalBySurface(taskReviewSignals);
585
-
586
- for (const signal of Object.values(latestTaskExecution)) {
587
- const envelope = signal.details && signal.details.envelope ? signal.details.envelope : null;
588
- const outOfScopeWrites =
589
- signal.details && Array.isArray(signal.details.outOfScopeWrites)
590
- ? dedupeMessages(signal.details.outOfScopeWrites.map((item) => String(item || "").trim()).filter(Boolean))
591
- : [];
592
- const taskGroupId =
593
- (envelope && envelope.taskGroupId) ||
594
- String(signal.surface || "").replace(/^task-execution\./, "") ||
595
- "unknown";
596
- if (signal.status === STATUS.BLOCK) {
597
- findings.blockers.push(`Task group ${taskGroupId} is BLOCKED in implementer status envelope.`);
598
- } else if (signal.status === STATUS.WARN) {
599
- findings.warnings.push(`Task group ${taskGroupId} has unresolved implementer concerns/context needs.`);
600
- }
601
- if (outOfScopeWrites.length > 0) {
602
- findings.blockers.push(
603
- `Task group ${taskGroupId} reported out-of-scope writes: ${outOfScopeWrites.join(", ")}.`
604
- );
605
- }
606
- if (envelope && envelope.summary) {
607
- findings.notes.push(`Implementer summary ${taskGroupId}: ${envelope.summary}`);
608
- }
609
- }
610
-
611
- const reviewStateByGroup = {};
612
- for (const signal of Object.values(latestTaskReview)) {
613
- const envelope = signal.details && signal.details.envelope ? signal.details.envelope : null;
614
- const taskGroupId =
615
- (envelope && envelope.taskGroupId) ||
616
- String(signal.surface || "").replace(/^task-review\./, "").split(".")[0] ||
617
- "unknown";
618
- const stage = envelope && envelope.stage ? envelope.stage : String(signal.surface || "").split(".").pop();
619
- if (!reviewStateByGroup[taskGroupId]) {
620
- reviewStateByGroup[taskGroupId] = {};
621
- }
622
- reviewStateByGroup[taskGroupId][stage] = signal.status;
623
- if (signal.status === STATUS.BLOCK) {
624
- findings.blockers.push(`Task review ${taskGroupId}/${stage} is BLOCK.`);
625
- } else if (signal.status === STATUS.WARN) {
626
- findings.warnings.push(`Task review ${taskGroupId}/${stage} is WARN and requires follow-up.`);
627
- }
628
- }
629
-
630
- for (const [taskGroupId, state] of Object.entries(reviewStateByGroup)) {
631
- if (state.quality && !state.spec) {
632
- findings.blockers.push(
633
- `Task review ordering violation for ${taskGroupId}: quality review exists without a prior spec review result.`
634
- );
635
- continue;
636
- }
637
- if (state.quality && state.spec === STATUS.WARN) {
638
- findings.blockers.push(
639
- `Task review ordering violation for ${taskGroupId}: quality review was recorded before spec review reached PASS.`
640
- );
641
- continue;
642
- }
643
- if (state.quality && state.spec === STATUS.BLOCK) {
644
- findings.blockers.push(
645
- `Task review ordering violation for ${taskGroupId}: quality review was recorded while spec review is BLOCK.`
646
- );
647
- }
648
- }
649
- }
650
-
651
- function normalizeCheckpointStatus(status) {
652
- const normalized = String(status || "").toUpperCase();
653
- if (normalized === STATUS.PASS || normalized === STATUS.WARN || normalized === STATUS.BLOCK) {
654
- return normalized;
655
- }
656
- return STATUS.PASS;
657
- }
658
-
659
- function collectIntegrityAudit(projectRoot, workflowRoot, signalSummary) {
660
- if (!pathExists(workflowRoot)) {
661
- return null;
662
- }
663
- return summarizeAudit(
664
- auditProject(projectRoot, {
665
- mode: "integrity",
666
- preloadedSignalSummary: signalSummary
667
- })
668
- );
669
- }
670
-
671
- function collectCompletionAudit(projectRoot, workflowRoot, changeId, signalSummary) {
672
- if (!changeId || !pathExists(workflowRoot)) {
673
- return null;
674
- }
675
- return summarizeAudit(
676
- auditProject(projectRoot, {
677
- mode: "completion",
678
- changeId,
679
- preloadedSignalSummary: signalSummary
680
- })
681
- );
682
- }
683
-
684
- function applyAuditFindings(stageId, findings, integrityAudit, completionAudit) {
685
- let nextStageId = stageId;
686
-
687
- if (integrityAudit && integrityAudit.status === "FAIL") {
688
- findings.warnings.push("Integrity audit currently reports FAIL.");
689
- } else if (integrityAudit && integrityAudit.status === "WARN") {
690
- findings.warnings.push("Integrity audit currently reports WARN.");
691
- }
692
-
693
- if ((nextStageId === "verify" || nextStageId === "complete") && completionAudit) {
694
- if (completionAudit.status === "PASS") {
695
- if (nextStageId === "verify") {
696
- nextStageId = "complete";
697
- }
698
- findings.notes.push("Completion audit reports PASS for the active change.");
699
- } else if (completionAudit.status === "WARN") {
700
- findings.warnings.push("Completion audit currently reports WARN.");
701
- if (nextStageId === "complete") {
702
- nextStageId = "verify";
703
- }
704
- } else if (completionAudit.status === "FAIL") {
705
- findings.blockers.push("Completion audit currently reports FAIL.");
706
- if (nextStageId === "complete") {
707
- nextStageId = "verify";
708
- }
709
- }
710
- }
711
-
712
- return nextStageId;
713
- }
714
-
715
- function applyExecutionSignalFindings(stageId, findings, signalSummary) {
716
- let nextStageId = stageId;
717
- const strictPromotion = isStrictPromotionEnabled();
718
- const signalParseIssue = signalSummary["signal-file-parse"];
719
- if (signalParseIssue) {
720
- const warningText =
721
- Array.isArray(signalParseIssue.warnings) && signalParseIssue.warnings[0]
722
- ? String(signalParseIssue.warnings[0])
723
- : "Malformed execution signal files were detected.";
724
- findings.warnings.push(warningText);
725
- }
726
-
727
- const diffSignal = signalSummary["diff-spec"];
728
- if (diffSignal && diffSignal.status && diffSignal.status !== STATUS.PASS) {
729
- findings.warnings.push(`Planning diff signal diff-spec reports ${diffSignal.status}.`);
730
- }
731
-
732
- const lintSpecSignal = signalSummary["lint-spec"];
733
- const lintSpecGateConfigs = [
734
- { id: "principleInheritance", label: "principleInheritance", fallbackStage: "breakdown" },
735
- { id: "clarify", label: "clarify", fallbackStage: "breakdown" },
736
- { id: "scenarioQuality", label: "scenarioQuality", fallbackStage: "breakdown" }
737
- ];
738
- let lintSpecGateObserved = false;
739
- let strongestLintSpecGateStatus = "";
740
- for (const config of lintSpecGateConfigs) {
741
- const gate = getSignalGate(lintSpecSignal, config.id);
742
- if (!gate) {
743
- continue;
744
- }
745
- lintSpecGateObserved = true;
746
- const gateStatus = resolveEffectiveGateStatus(gate, lintSpecSignal);
747
- if (statusSeverity(gateStatus) > statusSeverity(strongestLintSpecGateStatus)) {
748
- strongestLintSpecGateStatus = gateStatus;
749
- }
750
- const evidenceRefs = collectGateEvidenceRefs(gate);
751
- const evidenceSuffix =
752
- evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
753
- if (gateStatus === STATUS.BLOCK) {
754
- findings.blockers.push(
755
- `lint-spec gate ${config.label} is BLOCK and prevents planning promotion.${evidenceSuffix}`
756
- );
757
- addBlockingGateRecord(
758
- findings,
759
- config.id,
760
- "lint-spec",
761
- gate,
762
- `lint-spec gate ${config.label} is BLOCK`
763
- );
764
- nextStageId = fallbackStageIfBeyond(nextStageId, config.fallbackStage);
765
- } else if (gateStatus === STATUS.WARN) {
766
- findings.warnings.push(`lint-spec gate ${config.label} is WARN.${evidenceSuffix}`);
767
- if (strictPromotion) {
768
- findings.blockers.push(
769
- `DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-spec gate ${config.label} WARN blocks promotion.`
770
- );
771
- addBlockingGateRecord(
772
- findings,
773
- config.id,
774
- "lint-spec",
775
- gate,
776
- `strict promotion escalated lint-spec gate ${config.label} WARN`
777
- );
778
- nextStageId = fallbackStageIfBeyond(nextStageId, config.fallbackStage);
779
- }
780
- }
781
- for (const message of Array.isArray(gate.compatibility) ? gate.compatibility : []) {
782
- findings.notes.push(`lint-spec gate ${config.label} compatibility: ${message}`);
783
- }
784
- if (config.id === "clarify") {
785
- for (const bounded of Array.isArray(gate.bounded) ? gate.bounded : []) {
786
- findings.notes.push(`lint-spec gate clarify bounded ambiguity: ${bounded}`);
787
- }
788
- }
789
- }
790
- const lintSpecSignalStatus = normalizeSignalStatus(lintSpecSignal && lintSpecSignal.status);
791
- if (
792
- lintSpecSignal &&
793
- (!lintSpecGateObserved || statusSeverity(lintSpecSignalStatus) > statusSeverity(strongestLintSpecGateStatus))
794
- ) {
795
- if (lintSpecSignalStatus === STATUS.BLOCK) {
796
- findings.blockers.push("lint-spec signal is BLOCK.");
797
- addBlockingGateRecord(
798
- findings,
799
- "lint-spec",
800
- "lint-spec",
801
- lintSpecSignal.details && lintSpecSignal.details.gates ? lintSpecSignal.details.gates : null,
802
- "lint-spec signal is BLOCK"
803
- );
804
- nextStageId = fallbackStageIfBeyond(nextStageId, "breakdown");
805
- } else if (lintSpecSignalStatus === STATUS.WARN) {
806
- findings.warnings.push("lint-spec signal is WARN.");
807
- if (strictPromotion) {
808
- findings.blockers.push(
809
- "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-spec WARN blocks promotion."
810
- );
811
- addBlockingGateRecord(
812
- findings,
813
- "lint-spec",
814
- "lint-spec",
815
- lintSpecSignal.details && lintSpecSignal.details.gates ? lintSpecSignal.details.gates : null,
816
- "strict promotion escalated lint-spec WARN"
817
- );
818
- nextStageId = fallbackStageIfBeyond(nextStageId, "breakdown");
819
- }
820
- }
821
- }
822
-
823
- const analyzeSignal = signalSummary["scope-check"];
824
- const analyzeGate = getSignalGate(analyzeSignal, "analyze");
825
- let analyzeGateStatus = "";
826
- if (analyzeGate) {
827
- const evidenceRefs = collectGateEvidenceRefs(analyzeGate);
828
- const evidenceSuffix =
829
- evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
830
- analyzeGateStatus = resolveEffectiveGateStatus(analyzeGate, analyzeSignal);
831
- if (analyzeGateStatus === STATUS.BLOCK) {
832
- findings.blockers.push(`scope-check gate analyze is BLOCK.${evidenceSuffix}`);
833
- addBlockingGateRecord(
834
- findings,
835
- "analyze",
836
- "scope-check",
837
- analyzeGate,
838
- "scope-check gate analyze is BLOCK"
839
- );
840
- nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
841
- } else if (analyzeGateStatus === STATUS.WARN) {
842
- findings.warnings.push(`scope-check gate analyze is WARN.${evidenceSuffix}`);
843
- if (strictPromotion) {
844
- findings.blockers.push(
845
- "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; scope-check gate analyze WARN blocks promotion."
846
- );
847
- addBlockingGateRecord(
848
- findings,
849
- "analyze",
850
- "scope-check",
851
- analyzeGate,
852
- "strict promotion escalated scope-check gate analyze WARN"
853
- );
854
- nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
855
- }
856
- }
857
- for (const message of Array.isArray(analyzeGate.compatibility) ? analyzeGate.compatibility : []) {
858
- findings.notes.push(`scope-check gate analyze compatibility: ${message}`);
859
- }
860
- }
861
- const analyzeSignalStatus = normalizeSignalStatus(analyzeSignal && analyzeSignal.status);
862
- if (
863
- analyzeSignal &&
864
- (!analyzeGate || statusSeverity(analyzeSignalStatus) > statusSeverity(analyzeGateStatus))
865
- ) {
866
- if (analyzeSignalStatus === STATUS.BLOCK) {
867
- findings.blockers.push("scope-check signal is BLOCK.");
868
- addBlockingGateRecord(
869
- findings,
870
- "scope-check",
871
- "scope-check",
872
- analyzeSignal.details && analyzeSignal.details.gates ? analyzeSignal.details.gates : null,
873
- "scope-check signal is BLOCK"
874
- );
875
- nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
876
- } else if (analyzeSignalStatus === STATUS.WARN) {
877
- findings.warnings.push("scope-check signal is WARN.");
878
- if (strictPromotion) {
879
- findings.blockers.push(
880
- "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; scope-check WARN blocks promotion."
881
- );
882
- addBlockingGateRecord(
883
- findings,
884
- "scope-check",
885
- "scope-check",
886
- analyzeSignal.details && analyzeSignal.details.gates ? analyzeSignal.details.gates : null,
887
- "strict promotion escalated scope-check WARN"
888
- );
889
- nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
890
- }
891
- }
892
- }
893
-
894
- const lintTasksSignal = signalSummary["lint-tasks"];
895
- const taskCheckpointGate = getSignalGate(lintTasksSignal, "taskCheckpoint");
896
- const taskCheckpointGateStatus = taskCheckpointGate
897
- ? resolveEffectiveGateStatus(taskCheckpointGate, lintTasksSignal)
898
- : "";
899
- const lintTasksSignalStatus = normalizeSignalStatus(lintTasksSignal && lintTasksSignal.status);
900
- if (taskCheckpointGate && taskCheckpointGateStatus === STATUS.BLOCK) {
901
- const evidenceRefs = collectGateEvidenceRefs(taskCheckpointGate);
902
- const evidenceSuffix =
903
- evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
904
- findings.blockers.push(`lint-tasks task-checkpoint is BLOCK and prevents promotion into build.${evidenceSuffix}`);
905
- addBlockingGateRecord(
906
- findings,
907
- "taskCheckpoint",
908
- "lint-tasks",
909
- taskCheckpointGate,
910
- "lint-tasks task-checkpoint is BLOCK"
911
- );
912
- nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
913
- } else if (taskCheckpointGate && taskCheckpointGateStatus === STATUS.WARN) {
914
- const evidenceRefs = collectGateEvidenceRefs(taskCheckpointGate);
915
- const evidenceSuffix =
916
- evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
917
- findings.warnings.push(`lint-tasks task-checkpoint is WARN.${evidenceSuffix}`);
918
- if (strictPromotion) {
919
- findings.blockers.push(
920
- "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-tasks WARN blocks promotion into build."
921
- );
922
- addBlockingGateRecord(
923
- findings,
924
- "taskCheckpoint",
925
- "lint-tasks",
926
- taskCheckpointGate,
927
- "strict promotion escalated lint-tasks task-checkpoint WARN"
928
- );
929
- nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
930
- }
931
- }
932
- if (
933
- lintTasksSignal &&
934
- (!taskCheckpointGate || statusSeverity(lintTasksSignalStatus) > statusSeverity(taskCheckpointGateStatus))
935
- ) {
936
- if (lintTasksSignalStatus === STATUS.BLOCK) {
937
- findings.blockers.push("lint-tasks signal is BLOCK.");
938
- addBlockingGateRecord(
939
- findings,
940
- "lint-tasks",
941
- "lint-tasks",
942
- lintTasksSignal.details && lintTasksSignal.details.gates ? lintTasksSignal.details.gates : null,
943
- "lint-tasks signal is BLOCK"
944
- );
945
- nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
946
- } else if (lintTasksSignalStatus === STATUS.WARN) {
947
- findings.warnings.push("lint-tasks signal is WARN.");
948
- if (strictPromotion) {
949
- findings.blockers.push(
950
- "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-tasks WARN blocks promotion into build."
951
- );
952
- addBlockingGateRecord(
953
- findings,
954
- "lint-tasks",
955
- "lint-tasks",
956
- lintTasksSignal.details && lintTasksSignal.details.gates ? lintTasksSignal.details.gates : null,
957
- "strict promotion escalated lint-tasks WARN"
958
- );
959
- nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
960
- }
961
- }
962
- }
963
- if (taskCheckpointGate) {
964
- for (const message of Array.isArray(taskCheckpointGate.compatibility)
965
- ? taskCheckpointGate.compatibility
966
- : []) {
967
- findings.notes.push(`lint-tasks gate taskCheckpoint compatibility: ${message}`);
968
- }
969
- }
970
-
971
- const verificationSignal = signalSummary["verify-coverage"];
972
- if (verificationSignal && verificationSignal.status === STATUS.BLOCK) {
973
- findings.blockers.push("verify-coverage signal is BLOCK.");
974
- if (nextStageId === "complete") {
975
- nextStageId = "verify";
976
- }
977
- } else if (verificationSignal && verificationSignal.status === STATUS.WARN) {
978
- findings.warnings.push("verify-coverage signal is WARN.");
979
- }
980
-
981
- return nextStageId;
982
- }
983
-
984
- function buildRouteRecommendation(stageId, context = {}) {
985
- const projectRoot = context.projectRoot || process.cwd();
986
- const changeId = context.changeId || "change-001";
987
-
988
- if (context.ambiguousChangeSelection) {
989
- return {
990
- route: "select-change",
991
- command: `da-vinci workflow-status --project ${projectRoot} --change <change-id>`,
992
- reason: "Multiple change directories were found. Choose one change id first."
993
- };
994
- }
995
-
996
- switch (stageId) {
997
- case "bootstrap":
998
- return {
999
- route: "bootstrap-project",
1000
- command: `da-vinci bootstrap-project --project ${projectRoot} --change ${changeId}`,
1001
- reason: "Workflow base artifacts are missing."
1002
- };
1003
- case "breakdown":
1004
- return {
1005
- route: "/dv:breakdown",
1006
- command: "/dv:breakdown",
1007
- reason: "Proposal or spec artifacts are incomplete."
1008
- };
1009
- case "design":
1010
- return {
1011
- route: "/dv:design",
1012
- command: "/dv:design",
1013
- reason: "Design artifacts or design checkpoints are not ready."
1014
- };
1015
- case "tasks":
1016
- return {
1017
- route: "/dv:tasks",
1018
- command: "/dv:tasks",
1019
- reason: "Task planning artifacts are missing or blocked."
1020
- };
1021
- case "build":
1022
- return {
1023
- route: "/dv:build",
1024
- command: "/dv:build",
1025
- reason: "Implementation should proceed before verification."
1026
- };
1027
- case "verify":
1028
- return {
1029
- route: "/dv:verify",
1030
- command: "/dv:verify",
1031
- reason: "Verification and completion readiness are not satisfied yet."
1032
- };
1033
- default:
1034
- return {
1035
- route: null,
1036
- command: null,
1037
- reason: "No next route. Workflow appears complete."
1038
- };
1039
- }
1040
- }
1041
-
1042
- function readCheckpointStatuses(changeDir) {
1043
- const collected = {};
1044
- const files = [
1045
- path.join(changeDir, "design.md"),
1046
- path.join(changeDir, "pencil-design.md"),
1047
- path.join(changeDir, "tasks.md"),
1048
- path.join(changeDir, "verification.md")
1049
- ];
1050
-
1051
- for (const artifactPath of files) {
1052
- const text = readTextIfExists(artifactPath);
1053
- if (!text) {
1054
- continue;
1055
- }
1056
- const statuses = parseCheckpointStatusMap(text);
1057
- for (const [label, value] of Object.entries(statuses)) {
1058
- const normalized = normalizeCheckpointLabel(label);
1059
- if (!normalized) {
1060
- continue;
1061
- }
1062
- collected[normalized] = String(value || "").toUpperCase();
1063
- }
1064
- }
1065
-
1066
- return collected;
1067
- }
1068
-
1069
- function deriveStageFromArtifacts(artifactState, checkpointStatuses, findings) {
1070
- if (!artifactState.workflowRootReady) {
1071
- findings.blockers.push("Missing `.da-vinci/` directory.");
1072
- return "bootstrap";
1073
- }
1074
-
1075
- if (!artifactState.changeSelected) {
1076
- findings.blockers.push("No active change selected under `.da-vinci/changes/`.");
1077
- return "breakdown";
1078
- }
1079
-
1080
- if (!artifactState.proposal || artifactState.specFiles.length === 0) {
1081
- if (!artifactState.proposal) {
1082
- findings.blockers.push("Missing `proposal.md` for the active change.");
1083
- }
1084
- if (artifactState.specFiles.length === 0) {
1085
- findings.blockers.push("Missing `specs/*/spec.md` for the active change.");
1086
- }
1087
- return "breakdown";
1088
- }
1089
-
1090
- if (
1091
- !artifactState.design ||
1092
- !artifactState.pencilDesign ||
1093
- !artifactState.pencilBindings
1094
- ) {
1095
- if (!artifactState.design) {
1096
- findings.blockers.push("Missing `design.md` for the active change.");
1097
- }
1098
- if (!artifactState.pencilDesign) {
1099
- findings.blockers.push("Missing `pencil-design.md` for the active change.");
1100
- }
1101
- if (!artifactState.pencilBindings) {
1102
- findings.blockers.push("Missing `pencil-bindings.md` for the active change.");
1103
- }
1104
- return "design";
1105
- }
1106
-
1107
- const designCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.DESIGN];
1108
- if (designCheckpoint === STATUS.BLOCK) {
1109
- findings.blockers.push("`design checkpoint` is BLOCK.");
1110
- return "design";
1111
- }
1112
- if (designCheckpoint === STATUS.WARN) {
1113
- findings.warnings.push("`design checkpoint` is WARN.");
1114
- }
1115
-
1116
- const designSourceCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.DESIGN_SOURCE];
1117
- if (designSourceCheckpoint === STATUS.BLOCK) {
1118
- findings.blockers.push("`design source checkpoint` is BLOCK.");
1119
- return "design";
1120
- }
1121
- if (designSourceCheckpoint === STATUS.WARN) {
1122
- findings.warnings.push("`design source checkpoint` is WARN.");
1123
- }
1124
-
1125
- const runtimeGateCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.RUNTIME_GATE];
1126
- if (runtimeGateCheckpoint === STATUS.BLOCK) {
1127
- findings.blockers.push("`mcp runtime gate` is BLOCK.");
1128
- return "design";
1129
- }
1130
- if (runtimeGateCheckpoint === STATUS.WARN) {
1131
- findings.warnings.push("`mcp runtime gate` is WARN.");
1132
- }
1133
-
1134
- const mappingCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.MAPPING];
1135
- if (mappingCheckpoint === STATUS.BLOCK) {
1136
- findings.blockers.push("`mapping checkpoint` is BLOCK.");
1137
- return "design";
1138
- }
1139
- if (mappingCheckpoint === STATUS.WARN) {
1140
- findings.warnings.push("`mapping checkpoint` is WARN.");
1141
- }
1142
-
1143
- if (!artifactState.tasks) {
1144
- findings.blockers.push("Missing `tasks.md` for the active change.");
1145
- return "tasks";
1146
- }
1147
-
1148
- const taskCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.TASK];
1149
- if (taskCheckpoint === STATUS.BLOCK) {
1150
- findings.blockers.push("`task checkpoint` is BLOCK.");
1151
- return "tasks";
1152
- }
1153
- if (taskCheckpoint === STATUS.WARN) {
1154
- findings.warnings.push("`task checkpoint` is WARN.");
1155
- }
1156
-
1157
- if (!artifactState.verification) {
1158
- findings.blockers.push("Missing `verification.md` for the active change.");
1159
- return "build";
1160
- }
1161
-
1162
- return "verify";
1163
- }
1164
-
1165
- function deriveTaskGroupMetadata(tasksMarkdownText, checkpointStatuses) {
1166
- if (!tasksMarkdownText) {
1167
- return [];
1168
- }
1169
-
1170
- const taskArtifact = parseTasksArtifact(tasksMarkdownText);
1171
- const lines = String(tasksMarkdownText || "").replace(/\r\n?/g, "\n").split("\n");
1172
- const sections = [];
1173
- let current = null;
1174
- for (const line of lines) {
1175
- const headingMatch = line.match(/^\s{0,3}##\s+(\d+(?:\.\d+)*)\.\s+(.+)$/);
1176
- if (headingMatch) {
1177
- if (current) {
1178
- sections.push(current);
1179
- }
1180
- current = {
1181
- id: headingMatch[1],
1182
- title: String(headingMatch[2] || "").trim(),
1183
- checklistItems: []
1184
- };
1185
- continue;
1186
- }
1187
- if (!current) {
1188
- continue;
1189
- }
1190
- const checklistMatch = line.match(/^\s*-\s*\[([ xX])\]\s+(.+)$/);
1191
- if (checklistMatch) {
1192
- current.checklistItems.push({
1193
- checked: String(checklistMatch[1] || "").toLowerCase() === "x",
1194
- text: String(checklistMatch[2] || "").trim()
1195
- });
1196
- }
1197
- }
1198
- if (current) {
1199
- sections.push(current);
1200
- }
1201
-
1202
- const normalizedTaskCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.TASK] || STATUS.WARN;
1203
- const groupMetadataById = new Map(
1204
- (taskArtifact.taskGroups || []).map((group) => [
1205
- group.id,
1206
- {
1207
- targetFiles: Array.isArray(group.targetFiles) ? group.targetFiles : [],
1208
- fileReferences: Array.isArray(group.fileReferences) ? group.fileReferences : [],
1209
- verificationActions: Array.isArray(group.verificationActions) ? group.verificationActions : [],
1210
- verificationCommands: Array.isArray(group.verificationCommands) ? group.verificationCommands : [],
1211
- executionIntent: Array.isArray(group.executionIntent) ? group.executionIntent : [],
1212
- reviewIntent: group.reviewIntent === true,
1213
- testingIntent: group.testingIntent === true,
1214
- codeChangeLikely: group.codeChangeLikely === true
1215
- }
1216
- ])
1217
- );
1218
- const metadata = sections.map((section, index) => {
1219
- const total = section.checklistItems.length;
1220
- const done = section.checklistItems.filter((item) => item.checked).length;
1221
- const completion = total > 0 ? Math.round((done / total) * 100) : 0;
1222
- const sectionStatus =
1223
- total === 0 ? "pending" : done === 0 ? "pending" : done < total ? "in_progress" : "completed";
1224
- const nextAction =
1225
- sectionStatus === "completed"
1226
- ? "advance to next task group"
1227
- : section.checklistItems.find((item) => !item.checked)?.text || "continue group work";
1228
- const groupMetadata = groupMetadataById.get(section.id) || {};
1229
-
1230
- return {
1231
- taskGroupId: section.id,
1232
- title: section.title,
1233
- status: sectionStatus,
1234
- completion,
1235
- checkpointOutcome: normalizedTaskCheckpoint,
1236
- evidence: section.checklistItems.filter((item) => item.checked).map((item) => item.text),
1237
- nextAction,
1238
- targetFiles: groupMetadata.targetFiles || [],
1239
- fileReferences: groupMetadata.fileReferences || [],
1240
- verificationActions: groupMetadata.verificationActions || [],
1241
- verificationCommands: groupMetadata.verificationCommands || [],
1242
- executionIntent: groupMetadata.executionIntent || [],
1243
- reviewIntent: groupMetadata.reviewIntent === true,
1244
- testingIntent: groupMetadata.testingIntent === true,
1245
- codeChangeLikely: groupMetadata.codeChangeLikely === true,
1246
- resumeCursor: {
1247
- groupIndex: index,
1248
- nextUncheckedItem:
1249
- section.checklistItems.find((item) => !item.checked)?.text ||
1250
- section.checklistItems[section.checklistItems.length - 1]?.text ||
1251
- null
1252
- }
1253
- };
1254
- });
1255
-
1256
- if (metadata.length === 0 && taskArtifact.taskGroups.length > 0) {
1257
- return taskArtifact.taskGroups.map((group, index) => {
1258
- const groupMetadata = groupMetadataById.get(group.id) || {};
1259
- return {
1260
- taskGroupId: group.id,
1261
- title: group.title,
1262
- status: "pending",
1263
- completion: 0,
1264
- checkpointOutcome: normalizedTaskCheckpoint,
1265
- evidence: [],
1266
- nextAction: "start task group",
1267
- targetFiles: groupMetadata.targetFiles || [],
1268
- fileReferences: groupMetadata.fileReferences || [],
1269
- verificationActions: groupMetadata.verificationActions || [],
1270
- verificationCommands: groupMetadata.verificationCommands || [],
1271
- executionIntent: groupMetadata.executionIntent || [],
1272
- reviewIntent: groupMetadata.reviewIntent === true,
1273
- testingIntent: groupMetadata.testingIntent === true,
1274
- codeChangeLikely: groupMetadata.codeChangeLikely === true,
1275
- resumeCursor: {
1276
- groupIndex: index,
1277
- nextUncheckedItem: null
1278
- }
1279
- };
1280
- });
1281
- }
1282
-
1283
- return metadata;
1284
- }
1285
-
1286
- function normalizeTaskGroupId(value) {
1287
- return String(value || "").trim();
1288
- }
1289
-
1290
- function normalizeResumeCursor(cursor, fallbackGroupIndex = null) {
1291
- const groupIndex =
1292
- cursor && Number.isInteger(cursor.groupIndex)
1293
- ? cursor.groupIndex
1294
- : Number.isInteger(fallbackGroupIndex)
1295
- ? fallbackGroupIndex
1296
- : null;
1297
- return {
1298
- groupIndex,
1299
- nextUncheckedItem:
1300
- cursor && Object.prototype.hasOwnProperty.call(cursor, "nextUncheckedItem")
1301
- ? cursor.nextUncheckedItem
1302
- : null
1303
- };
1304
- }
1305
-
1306
- function normalizeTaskGroupSeedMap(taskGroups) {
1307
- const byId = new Map();
1308
- for (const group of Array.isArray(taskGroups) ? taskGroups : []) {
1309
- const taskGroupId = normalizeTaskGroupId(group && (group.taskGroupId || group.id));
1310
- if (!taskGroupId || byId.has(taskGroupId)) {
1311
- continue;
1312
- }
1313
- byId.set(taskGroupId, group);
1314
- }
1315
- return byId;
1316
- }
1317
-
1318
- function findLatestSignalBySurface(signals, surface) {
1319
- const normalizedSurface = String(surface || "").trim();
1320
- if (!normalizedSurface) {
1321
- return null;
1322
- }
1323
- return (signals || []).find((signal) => String(signal.surface || "").trim() === normalizedSurface) || null;
1324
- }
1325
-
1326
- function summarizeSignalIssues(signal, envelopeItems) {
1327
- if (!signal) {
1328
- return [];
102
+ : null
103
+ };
1329
104
  }
1330
- const fromEnvelope = Array.isArray(envelopeItems) ? envelopeItems : [];
1331
- const fromSignal = [
1332
- ...(Array.isArray(signal.failures) ? signal.failures : []),
1333
- ...(Array.isArray(signal.warnings) ? signal.warnings : [])
1334
- ];
1335
- return dedupeMessages([...fromEnvelope, ...fromSignal].map((item) => String(item || "").trim()).filter(Boolean));
105
+ return result;
1336
106
  }
1337
107
 
1338
- function buildTaskGroupImplementerState(taskGroupId, signals, fallbackState) {
1339
- const fallback = fallbackState && typeof fallbackState === "object" ? fallbackState : {};
1340
- const signal = findLatestSignalBySurface(signals, `task-execution.${taskGroupId}`);
1341
- const envelope = signal && signal.details && signal.details.envelope ? signal.details.envelope : null;
1342
- if (!signal) {
1343
- return {
1344
- present: false,
1345
- signalStatus: null,
1346
- implementerStatus: fallback.implementerStatus || null,
1347
- summary: fallback.summary || null,
1348
- changedFiles: Array.isArray(fallback.changedFiles) ? fallback.changedFiles : [],
1349
- testEvidence: Array.isArray(fallback.testEvidence) ? fallback.testEvidence : [],
1350
- concerns: Array.isArray(fallback.concerns) ? fallback.concerns : [],
1351
- blockers: Array.isArray(fallback.blockers) ? fallback.blockers : [],
1352
- outOfScopeWrites: Array.isArray(fallback.outOfScopeWrites) ? fallback.outOfScopeWrites : [],
1353
- recordedAt: fallback.recordedAt || null
1354
- };
108
+ /**
109
+ * Reduce full audit payloads to the capped summary stored on workflow results.
110
+ *
111
+ * @param {object | null} result
112
+ * @returns {{ status: string, failures: string[], warnings: string[], notes: string[] } | null}
113
+ */
114
+ function summarizeAudit(result) {
115
+ if (!result) {
116
+ return null;
1355
117
  }
1356
118
 
1357
119
  return {
1358
- present: true,
1359
- signalStatus: signal.status || null,
1360
- implementerStatus: envelope && envelope.status ? envelope.status : fallback.implementerStatus || null,
1361
- summary: envelope && envelope.summary ? envelope.summary : fallback.summary || null,
1362
- changedFiles:
1363
- envelope && Array.isArray(envelope.changedFiles)
1364
- ? envelope.changedFiles
1365
- : Array.isArray(fallback.changedFiles)
1366
- ? fallback.changedFiles
1367
- : [],
1368
- testEvidence:
1369
- envelope && Array.isArray(envelope.testEvidence)
1370
- ? envelope.testEvidence
1371
- : Array.isArray(fallback.testEvidence)
1372
- ? fallback.testEvidence
1373
- : [],
1374
- concerns: summarizeSignalIssues(signal, envelope && envelope.concerns),
1375
- blockers: summarizeSignalIssues(signal, envelope && envelope.blockers),
1376
- outOfScopeWrites:
1377
- signal.details && Array.isArray(signal.details.outOfScopeWrites)
1378
- ? dedupeMessages(signal.details.outOfScopeWrites.map((item) => String(item || "").trim()).filter(Boolean))
1379
- : Array.isArray(fallback.outOfScopeWrites)
1380
- ? fallback.outOfScopeWrites
1381
- : [],
1382
- recordedAt: (envelope && envelope.recordedAt) || signal.timestamp || fallback.recordedAt || null
120
+ status: result.status,
121
+ failures: Array.isArray(result.failures) ? result.failures.slice(0, MAX_REPORTED_MESSAGES) : [],
122
+ warnings: Array.isArray(result.warnings) ? result.warnings.slice(0, MAX_REPORTED_MESSAGES) : [],
123
+ notes: Array.isArray(result.notes) ? result.notes.slice(0, MAX_REPORTED_MESSAGES) : []
1383
124
  };
1384
125
  }
1385
126
 
1386
- function buildTaskGroupReviewStageState(taskGroupId, stage, signals, fallbackState) {
1387
- const fallback = fallbackState && typeof fallbackState === "object" ? fallbackState : {};
1388
- const signal = findLatestSignalBySurface(signals, `task-review.${taskGroupId}.${stage}`);
1389
- const envelope = signal && signal.details && signal.details.envelope ? signal.details.envelope : null;
1390
- if (!signal) {
1391
- return {
1392
- present: false,
1393
- status: fallback.status || "missing",
1394
- summary: fallback.summary || null,
1395
- reviewer: fallback.reviewer || null,
1396
- issues: Array.isArray(fallback.issues) ? fallback.issues : [],
1397
- recordedAt: fallback.recordedAt || null
1398
- };
127
+ /**
128
+ * Collect the integrity audit with the already-computed routing signal summary preloaded.
129
+ *
130
+ * @param {string} projectRoot
131
+ * @param {string} workflowRoot
132
+ * @param {Object.<string, object>} signalSummary
133
+ * @returns {object | null}
134
+ */
135
+ function collectIntegrityAudit(projectRoot, workflowRoot, signalSummary) {
136
+ if (!pathExists(workflowRoot)) {
137
+ return null;
1399
138
  }
1400
-
1401
- return {
1402
- present: true,
1403
- status: signal.status || "missing",
1404
- summary: envelope && envelope.summary ? envelope.summary : fallback.summary || null,
1405
- reviewer: envelope && envelope.reviewer ? envelope.reviewer : fallback.reviewer || null,
1406
- issues: summarizeSignalIssues(signal, envelope && envelope.issues),
1407
- recordedAt: (envelope && envelope.recordedAt) || signal.timestamp || fallback.recordedAt || null
1408
- };
139
+ return summarizeAudit(
140
+ auditProject(projectRoot, {
141
+ mode: "integrity",
142
+ preloadedSignalSummary: signalSummary
143
+ })
144
+ );
1409
145
  }
1410
146
 
1411
- function buildTaskGroupReviewState(taskGroup, signals, fallbackState) {
1412
- const fallback = fallbackState && typeof fallbackState === "object" ? fallbackState : {};
1413
- const taskGroupId = normalizeTaskGroupId(taskGroup.taskGroupId || taskGroup.id);
1414
- const required = taskGroup.reviewIntent === true;
1415
- const spec = buildTaskGroupReviewStageState(taskGroupId, "spec", signals, fallback.spec);
1416
- const quality = buildTaskGroupReviewStageState(taskGroupId, "quality", signals, fallback.quality);
1417
-
1418
- return {
1419
- required,
1420
- ordering: required ? "spec_then_quality" : "none",
1421
- spec,
1422
- quality
1423
- };
147
+ /**
148
+ * Collect the completion audit for an active change with the already-computed routing signal summary.
149
+ *
150
+ * @param {string} projectRoot
151
+ * @param {string} workflowRoot
152
+ * @param {string | null} changeId
153
+ * @param {Object.<string, object>} signalSummary
154
+ * @returns {object | null}
155
+ */
156
+ function collectCompletionAudit(projectRoot, workflowRoot, changeId, signalSummary) {
157
+ if (!changeId || !pathExists(workflowRoot)) {
158
+ return null;
159
+ }
160
+ return summarizeAudit(
161
+ auditProject(projectRoot, {
162
+ mode: "completion",
163
+ changeId,
164
+ preloadedSignalSummary: signalSummary
165
+ })
166
+ );
1424
167
  }
1425
168
 
1426
- function buildEffectiveTaskGroupState(group, planned, implementer, review) {
1427
- const fallbackCursor = normalizeResumeCursor(planned.resumeCursor, null);
1428
- const effective = {
1429
- status: planned.status,
1430
- nextAction: planned.nextAction,
1431
- resumeCursor: fallbackCursor,
1432
- source: "planned",
1433
- reason: "planned_checklist"
1434
- };
1435
-
1436
- if (implementer.present && implementer.outOfScopeWrites.length > 0) {
1437
- return {
1438
- status: "blocked",
1439
- nextAction:
1440
- `resolve out-of-scope writes for task group ${group.taskGroupId}: ${implementer.outOfScopeWrites.join(", ")}`,
1441
- resumeCursor: {
1442
- groupIndex: fallbackCursor.groupIndex,
1443
- nextUncheckedItem: null,
1444
- liveFocus: "out_of_scope_write"
1445
- },
1446
- source: "implementer",
1447
- reason: "out_of_scope_write"
1448
- };
1449
- }
169
+ /**
170
+ * Map the effective workflow stage to the next operator-facing route suggestion.
171
+ *
172
+ * @param {string} stageId
173
+ * @param {{ projectRoot?: string, changeId?: string, ambiguousChangeSelection?: boolean }} [context]
174
+ * @returns {{ route: string | null, command: string | null, reason: string }}
175
+ */
176
+ function buildRouteRecommendation(stageId, context = {}) {
177
+ const projectRoot = context.projectRoot || process.cwd();
178
+ const changeId = context.changeId || "change-001";
1450
179
 
1451
- if (implementer.present && implementer.signalStatus === STATUS.BLOCK) {
180
+ if (context.ambiguousChangeSelection) {
1452
181
  return {
1453
- status: "blocked",
1454
- nextAction:
1455
- implementer.blockers[0] ||
1456
- implementer.summary ||
1457
- `resolve implementer blocker for task group ${group.taskGroupId}`,
1458
- resumeCursor: {
1459
- groupIndex: fallbackCursor.groupIndex,
1460
- nextUncheckedItem: null,
1461
- liveFocus: "implementer_block"
1462
- },
1463
- source: "implementer",
1464
- reason: "implementer_block"
182
+ route: "select-change",
183
+ command: `da-vinci workflow-status --project ${projectRoot} --change <change-id>`,
184
+ reason: "Multiple change directories were found. Choose one change id first."
1465
185
  };
1466
186
  }
1467
187
 
1468
- const reviewSignalsPresent = review.spec.present || review.quality.present;
1469
- const plannedCompletion = Number.isFinite(Number(planned.completion))
1470
- ? Number(planned.completion)
1471
- : 0;
1472
- const reviewNearComplete = planned.status !== "completed" && plannedCompletion >= 75;
1473
- const reviewHardDue = planned.status === "completed" || reviewNearComplete;
1474
- const reviewContextReady = review.required && (reviewSignalsPresent || reviewHardDue || implementer.present);
1475
-
1476
- if (reviewContextReady) {
1477
- if (
1478
- review.quality.present &&
1479
- (!review.spec.present || review.spec.status === "missing" || review.spec.status === STATUS.WARN)
1480
- ) {
188
+ switch (stageId) {
189
+ case "bootstrap":
1481
190
  return {
1482
- status: "blocked",
1483
- nextAction:
1484
- `remove or rerun out-of-order quality review for task group ${group.taskGroupId} after spec review PASS`,
1485
- resumeCursor: {
1486
- groupIndex: fallbackCursor.groupIndex,
1487
- nextUncheckedItem: null,
1488
- liveFocus: "review_ordering_violation"
1489
- },
1490
- source: "review",
1491
- reason: "review_ordering_violation"
191
+ route: "bootstrap-project",
192
+ command: `da-vinci bootstrap-project --project ${projectRoot} --change ${changeId}`,
193
+ reason: "Workflow base artifacts are missing."
1492
194
  };
1493
- }
1494
- if (review.spec.status === STATUS.BLOCK) {
195
+ case "breakdown":
1495
196
  return {
1496
- status: "blocked",
1497
- nextAction:
1498
- review.spec.issues[0] || review.spec.summary || `resolve spec review BLOCK for task group ${group.taskGroupId}`,
1499
- resumeCursor: {
1500
- groupIndex: fallbackCursor.groupIndex,
1501
- nextUncheckedItem: null,
1502
- liveFocus: "spec_review_block"
1503
- },
1504
- source: "review",
1505
- reason: "spec_review_block"
197
+ route: "/dv:breakdown",
198
+ command: "/dv:breakdown",
199
+ reason: "Proposal or spec artifacts are incomplete."
1506
200
  };
1507
- }
1508
- if (!review.spec.present || review.spec.status === "missing") {
1509
- if (reviewHardDue || reviewSignalsPresent) {
1510
- return {
1511
- status: "review_pending",
1512
- nextAction: `record spec review PASS or WARN for task group ${group.taskGroupId} before quality review`,
1513
- resumeCursor: {
1514
- groupIndex: fallbackCursor.groupIndex,
1515
- nextUncheckedItem: null,
1516
- liveFocus: "spec_review_missing"
1517
- },
1518
- source: "review",
1519
- reason: "spec_review_missing"
1520
- };
1521
- }
1522
- } else {
1523
- const specWarn = review.spec.status === STATUS.WARN;
1524
- if (review.quality.status === STATUS.BLOCK) {
1525
- return {
1526
- status: "blocked",
1527
- nextAction:
1528
- review.quality.issues[0] ||
1529
- review.quality.summary ||
1530
- `resolve quality review BLOCK for task group ${group.taskGroupId}`,
1531
- resumeCursor: {
1532
- groupIndex: fallbackCursor.groupIndex,
1533
- nextUncheckedItem: null,
1534
- liveFocus: "quality_review_block"
1535
- },
1536
- source: "review",
1537
- reason: "quality_review_block"
1538
- };
1539
- }
1540
- if (!review.quality.present || review.quality.status === "missing") {
1541
- if (reviewHardDue) {
1542
- return {
1543
- status: "review_pending",
1544
- nextAction: `record quality review PASS or WARN for task group ${group.taskGroupId}`,
1545
- resumeCursor: {
1546
- groupIndex: fallbackCursor.groupIndex,
1547
- nextUncheckedItem: null,
1548
- liveFocus: "quality_review_missing"
1549
- },
1550
- source: "review",
1551
- reason: "quality_review_missing"
1552
- };
1553
- }
1554
- if (specWarn) {
1555
- return {
1556
- status: "in_progress",
1557
- nextAction:
1558
- review.spec.issues[0] ||
1559
- review.spec.summary ||
1560
- `resolve spec review follow-up for task group ${group.taskGroupId}`,
1561
- resumeCursor: {
1562
- groupIndex: fallbackCursor.groupIndex,
1563
- nextUncheckedItem: null,
1564
- liveFocus: "spec_review_warn"
1565
- },
1566
- source: "review",
1567
- reason: "spec_review_warn"
1568
- };
1569
- }
1570
- } else if (review.quality.status === STATUS.WARN) {
1571
- return {
1572
- status: "in_progress",
1573
- nextAction:
1574
- review.quality.issues[0] ||
1575
- review.quality.summary ||
1576
- `resolve quality review follow-up for task group ${group.taskGroupId}`,
1577
- resumeCursor: {
1578
- groupIndex: fallbackCursor.groupIndex,
1579
- nextUncheckedItem: null,
1580
- liveFocus: "quality_review_warn"
1581
- },
1582
- source: "review",
1583
- reason: "quality_review_warn"
1584
- };
1585
- } else if (specWarn) {
1586
- return {
1587
- status: "in_progress",
1588
- nextAction:
1589
- review.spec.issues[0] ||
1590
- review.spec.summary ||
1591
- `resolve spec review follow-up for task group ${group.taskGroupId}`,
1592
- resumeCursor: {
1593
- groupIndex: fallbackCursor.groupIndex,
1594
- nextUncheckedItem: null,
1595
- liveFocus: "spec_review_warn"
1596
- },
1597
- source: "review",
1598
- reason: "spec_review_warn"
1599
- };
1600
- }
1601
- }
1602
- }
1603
-
1604
- if (implementer.present && implementer.signalStatus === STATUS.WARN) {
1605
- return {
1606
- status: "in_progress",
1607
- nextAction:
1608
- implementer.concerns[0] ||
1609
- implementer.summary ||
1610
- `resolve implementer concerns for task group ${group.taskGroupId}`,
1611
- resumeCursor: {
1612
- groupIndex: fallbackCursor.groupIndex,
1613
- nextUncheckedItem: null,
1614
- liveFocus: "implementer_warn"
1615
- },
1616
- source: "implementer",
1617
- reason: "implementer_warn"
1618
- };
1619
- }
1620
-
1621
- return effective;
1622
- }
1623
-
1624
- function deriveTaskGroupRuntimeState(plannedTaskGroups, signals, seedTaskGroups, decisionTraceRecords) {
1625
- const plannedGroups = Array.isArray(plannedTaskGroups) ? plannedTaskGroups : [];
1626
- const seedMap = normalizeTaskGroupSeedMap(seedTaskGroups);
1627
-
1628
- return plannedGroups.map((plannedGroup, index) => {
1629
- const taskGroupId = normalizeTaskGroupId(plannedGroup.taskGroupId || plannedGroup.id);
1630
- const seed = seedMap.get(taskGroupId) || {};
1631
- const planned = {
1632
- status: plannedGroup.status,
1633
- completion: plannedGroup.completion,
1634
- checkpointOutcome: plannedGroup.checkpointOutcome,
1635
- evidence: Array.isArray(plannedGroup.evidence) ? plannedGroup.evidence : [],
1636
- nextAction: plannedGroup.nextAction,
1637
- resumeCursor: normalizeResumeCursor(plannedGroup.resumeCursor, index)
1638
- };
1639
- const implementer = buildTaskGroupImplementerState(taskGroupId, signals, seed.implementer);
1640
- const review = buildTaskGroupReviewState(plannedGroup, signals, seed.review);
1641
- const effective = buildEffectiveTaskGroupState(plannedGroup, planned, implementer, review);
1642
- if (TRACEABLE_TASK_GROUP_FOCUS_REASONS.has(effective.reason)) {
1643
- recordWorkflowDecision(decisionTraceRecords, {
1644
- decisionFamily: "task_group_focus_resolution",
1645
- decisionKey: effective.reason,
1646
- outcome: "selected_focus",
1647
- reasonSummary: buildTaskGroupFocusReasonSummary(taskGroupId, effective.reason),
1648
- context: {
1649
- taskGroupId,
1650
- plannedStatus: planned.status || null,
1651
- effectiveStatus: effective.status || null,
1652
- liveFocus: effective.resumeCursor ? effective.resumeCursor.liveFocus || null : null,
1653
- nextAction: effective.nextAction || null
1654
- },
1655
- evidenceRefs: buildTaskGroupFocusEvidenceRefs(taskGroupId, effective.reason)
1656
- });
1657
- }
1658
-
1659
- return {
1660
- taskGroupId,
1661
- title: plannedGroup.title,
1662
- status: effective.status,
1663
- completion: planned.completion,
1664
- checkpointOutcome: planned.checkpointOutcome,
1665
- evidence: planned.evidence,
1666
- nextAction: effective.nextAction,
1667
- targetFiles: Array.isArray(plannedGroup.targetFiles) ? plannedGroup.targetFiles : [],
1668
- fileReferences: Array.isArray(plannedGroup.fileReferences) ? plannedGroup.fileReferences : [],
1669
- verificationActions: Array.isArray(plannedGroup.verificationActions)
1670
- ? plannedGroup.verificationActions
1671
- : [],
1672
- verificationCommands: Array.isArray(plannedGroup.verificationCommands)
1673
- ? plannedGroup.verificationCommands
1674
- : [],
1675
- executionIntent: Array.isArray(plannedGroup.executionIntent) ? plannedGroup.executionIntent : [],
1676
- reviewIntent: plannedGroup.reviewIntent === true,
1677
- testingIntent: plannedGroup.testingIntent === true,
1678
- codeChangeLikely: plannedGroup.codeChangeLikely === true,
1679
- resumeCursor: effective.resumeCursor,
1680
- planned,
1681
- implementer,
1682
- review,
1683
- effective
1684
- };
1685
- });
1686
- }
1687
-
1688
- function buildTaskGroupMetadataPayload(changeId, checkpointStatuses, taskGroups) {
1689
- return {
1690
- version: TASK_GROUP_METADATA_VERSION,
1691
- changeId,
1692
- checkpointOutcome: checkpointStatuses[CHECKPOINT_LABELS.TASK] || STATUS.WARN,
1693
- taskGroups: Array.isArray(taskGroups) ? taskGroups : [],
1694
- updatedAt: new Date().toISOString()
1695
- };
1696
- }
1697
-
1698
- function loadTaskGroupMetadataFromPath(targetPath) {
1699
- if (!targetPath || !pathExists(targetPath)) {
1700
- return null;
1701
- }
1702
- try {
1703
- return JSON.parse(readTextIfExists(targetPath));
1704
- } catch (_error) {
1705
- return null;
1706
- }
1707
- }
1708
-
1709
- function resolvePersistedTaskGroupSeed(
1710
- projectRoot,
1711
- changeId,
1712
- persistedRecord,
1713
- plannedTaskGroups,
1714
- decisionTraceRecords
1715
- ) {
1716
- const metadataRefs =
1717
- persistedRecord && persistedRecord.metadataRefs && typeof persistedRecord.metadataRefs === "object"
1718
- ? persistedRecord.metadataRefs
1719
- : {};
1720
- const canonicalPath =
1721
- metadataRefs.taskGroupsPath || resolveTaskGroupMetadataPath(projectRoot, changeId);
1722
- const notes = [];
1723
-
1724
- if (canonicalPath && pathExists(canonicalPath)) {
1725
- const actualDigest = digestForPath(canonicalPath);
1726
- const expectedDigest = metadataRefs.taskGroupsDigest || null;
1727
- if (expectedDigest && actualDigest && expectedDigest !== actualDigest) {
1728
- const message =
1729
- "Canonical task-group runtime state digest mismatch; rebuilding task-group state from artifacts.";
1730
- notes.push(message);
1731
- recordWorkflowDecision(decisionTraceRecords, {
1732
- decisionFamily: "task_group_seed_fallback",
1733
- decisionKey: TASK_GROUP_SEED_TRACE_KEYS["digest-mismatch"],
1734
- outcome: "fallback",
1735
- reasonSummary: message,
1736
- context: {
1737
- metadataPath: formatPathRef(projectRoot, canonicalPath),
1738
- taskGroupCount: Array.isArray(plannedTaskGroups) ? plannedTaskGroups.length : 0,
1739
- expectedDigestPresent: true
1740
- },
1741
- evidenceRefs: [`state:${formatPathRef(projectRoot, canonicalPath)}`]
1742
- });
201
+ case "design":
1743
202
  return {
1744
- taskGroups: plannedTaskGroups,
1745
- notes
203
+ route: "/dv:design",
204
+ command: "/dv:design",
205
+ reason: "Design artifacts or design checkpoints are not ready."
1746
206
  };
1747
- }
1748
-
1749
- const loaded = canonicalPath === resolveTaskGroupMetadataPath(projectRoot, changeId)
1750
- ? readTaskGroupMetadata(projectRoot, changeId)
1751
- : loadTaskGroupMetadataFromPath(canonicalPath);
1752
- if (loaded && Array.isArray(loaded.taskGroups)) {
207
+ case "tasks":
208
+ return {
209
+ route: "/dv:tasks",
210
+ command: "/dv:tasks",
211
+ reason: "Task planning artifacts are missing or blocked."
212
+ };
213
+ case "build":
1753
214
  return {
1754
- taskGroups: loaded.taskGroups,
1755
- notes
215
+ route: "/dv:build",
216
+ command: "/dv:build",
217
+ reason: "Implementation should proceed before verification."
218
+ };
219
+ case "verify":
220
+ return {
221
+ route: "/dv:verify",
222
+ command: "/dv:verify",
223
+ reason: "Verification and completion readiness are not satisfied yet."
224
+ };
225
+ default:
226
+ return {
227
+ route: null,
228
+ command: null,
229
+ reason: "No next route. Workflow appears complete."
1756
230
  };
1757
- }
1758
- {
1759
- const message =
1760
- "Canonical task-group runtime state is unreadable; rebuilding task-group state from artifacts.";
1761
- notes.push(message);
1762
- recordWorkflowDecision(decisionTraceRecords, {
1763
- decisionFamily: "task_group_seed_fallback",
1764
- decisionKey: TASK_GROUP_SEED_TRACE_KEYS.unreadable,
1765
- outcome: "fallback",
1766
- reasonSummary: message,
1767
- context: {
1768
- metadataPath: formatPathRef(projectRoot, canonicalPath),
1769
- taskGroupCount: Array.isArray(plannedTaskGroups) ? plannedTaskGroups.length : 0
1770
- },
1771
- evidenceRefs: [`state:${formatPathRef(projectRoot, canonicalPath)}`]
1772
- });
1773
- }
1774
- return {
1775
- taskGroups: plannedTaskGroups,
1776
- notes
1777
- };
1778
231
  }
232
+ }
1779
233
 
1780
- if (Array.isArray(persistedRecord && persistedRecord.taskGroups) && persistedRecord.taskGroups.length > 0) {
1781
- {
1782
- const message = "Using legacy embedded task-group state as migration fallback.";
1783
- notes.push(message);
1784
- recordWorkflowDecision(decisionTraceRecords, {
1785
- decisionFamily: "task_group_seed_fallback",
1786
- decisionKey: TASK_GROUP_SEED_TRACE_KEYS.legacy,
1787
- outcome: "fallback",
1788
- reasonSummary: message,
1789
- context: {
1790
- metadataPath: canonicalPath ? formatPathRef(projectRoot, canonicalPath) : null,
1791
- taskGroupCount: persistedRecord.taskGroups.length
1792
- },
1793
- evidenceRefs: [`state:${formatPathRef(projectRoot, resolveWorkflowStatePath(projectRoot))}`]
1794
- });
1795
- }
1796
- return {
1797
- taskGroups: persistedRecord.taskGroups,
1798
- notes
1799
- };
234
+ /**
235
+ * Collapse findings into the top-level workflow status token.
236
+ *
237
+ * @param {{ blockers: string[], warnings: string[] }} findings
238
+ * @returns {string}
239
+ */
240
+ function statusFromFindings(findings) {
241
+ if (findings.blockers.length > 0) {
242
+ return STATUS.BLOCK;
1800
243
  }
1801
-
1802
- {
1803
- const message = "Canonical task-group runtime state is missing; rebuilding task-group state from artifacts.";
1804
- notes.push(message);
1805
- recordWorkflowDecision(decisionTraceRecords, {
1806
- decisionFamily: "task_group_seed_fallback",
1807
- decisionKey: TASK_GROUP_SEED_TRACE_KEYS.missing,
1808
- outcome: "fallback",
1809
- reasonSummary: message,
1810
- context: {
1811
- metadataPath: canonicalPath ? formatPathRef(projectRoot, canonicalPath) : null,
1812
- taskGroupCount: Array.isArray(plannedTaskGroups) ? plannedTaskGroups.length : 0
1813
- },
1814
- evidenceRefs:
1815
- canonicalPath && String(canonicalPath).trim()
1816
- ? [`state:${formatPathRef(projectRoot, canonicalPath)}`]
1817
- : []
1818
- });
244
+ if (findings.warnings.length > 0) {
245
+ return STATUS.WARN;
1819
246
  }
1820
- return {
1821
- taskGroups: plannedTaskGroups,
1822
- notes
1823
- };
247
+ return STATUS.PASS;
1824
248
  }
1825
249
 
1826
- function buildGatesWithLiveOverlays(baseGates, completionAudit, disciplineState, verificationFreshness) {
1827
- const gates = baseGates && typeof baseGates === "object" ? { ...baseGates } : {};
1828
- if (completionAudit) {
1829
- gates[HANDOFF_GATES.VERIFY_TO_COMPLETE] =
1830
- completionAudit.status === "PASS" ? STATUS.PASS : STATUS.WARN;
1831
- }
1832
- if (disciplineState && disciplineState.blockers.length > 0) {
1833
- gates[HANDOFF_GATES.TASKS_TO_BUILD] = STATUS.BLOCK;
1834
- }
1835
- if (verificationFreshness && !verificationFreshness.fresh) {
1836
- gates[HANDOFF_GATES.VERIFY_TO_COMPLETE] = STATUS.BLOCK;
1837
- }
1838
- return gates;
250
+ /**
251
+ * Convert an overlay result into the stable workflow-status result schema.
252
+ *
253
+ * @param {{
254
+ * projectRoot: string,
255
+ * changeId: string | null,
256
+ * overlay: {
257
+ * stageId: string,
258
+ * findings: { blockers: string[], warnings: string[], notes: string[] },
259
+ * gates: Object.<string, string>,
260
+ * taskGroups: Array<object>,
261
+ * executionProfile?: object,
262
+ * worktreePreflight?: object | null,
263
+ * blockingGate?: object | null,
264
+ * needsRerunSurfaces?: string[],
265
+ * stalePlanningSignals?: Object.<string, object>
266
+ * },
267
+ * checkpoints?: Object.<string, string>,
268
+ * audits?: { integrity: object | null, completion: object | null },
269
+ * routeContext?: object,
270
+ * source?: string,
271
+ * discipline?: object | null,
272
+ * verificationFreshness?: object | null
273
+ * }} params
274
+ * @returns {object}
275
+ */
276
+ function buildWorkflowResultFromOverlay(params) {
277
+ return buildWorkflowResult({
278
+ projectRoot: params.projectRoot,
279
+ changeId: params.changeId,
280
+ stageId: params.overlay.stageId,
281
+ findings: params.overlay.findings,
282
+ checkpoints: params.checkpoints || {},
283
+ gates: params.overlay.gates,
284
+ audits: params.audits || {
285
+ integrity: null,
286
+ completion: null
287
+ },
288
+ routeContext: params.routeContext,
289
+ source: params.source || "derived",
290
+ taskGroups: params.overlay.taskGroups,
291
+ discipline: params.discipline || null,
292
+ executionProfile: params.overlay.executionProfile || null,
293
+ worktreePreflight: params.overlay.worktreePreflight || null,
294
+ verificationFreshness: params.verificationFreshness || null,
295
+ blockingGate: params.overlay.blockingGate || null,
296
+ needsRerunSurfaces: params.overlay.needsRerunSurfaces,
297
+ stalePlanningSignals: params.overlay.stalePlanningSignals
298
+ });
1839
299
  }
1840
300
 
1841
- function finalizeWorkflowView(options = {}) {
1842
- const findings = {
1843
- blockers: Array.isArray(options.findings && options.findings.blockers)
1844
- ? options.findings.blockers.slice()
1845
- : [],
1846
- warnings: Array.isArray(options.findings && options.findings.warnings)
1847
- ? options.findings.warnings.slice()
1848
- : [],
1849
- notes: Array.isArray(options.findings && options.findings.notes)
1850
- ? options.findings.notes.slice()
1851
- : []
1852
- };
1853
- let stageId = options.stageId;
1854
- const integrityAudit = options.integrityAudit || null;
1855
- const completionAudit = options.completionAudit || null;
1856
- const disciplineState = options.disciplineState || null;
1857
- const verificationFreshness = options.verificationFreshness || null;
1858
- const planningSignalFreshness =
1859
- options.planningSignalFreshness && typeof options.planningSignalFreshness === "object"
1860
- ? options.planningSignalFreshness
1861
- : {
1862
- effectiveSignalSummary: options.signalSummary || {},
1863
- stalePlanningSignals: {},
1864
- needsRerunSurfaces: []
1865
- };
1866
- const decisionTraceRecords = Array.isArray(options.decisionTraceRecords)
1867
- ? options.decisionTraceRecords
1868
- : null;
301
+ /**
302
+ * Shared base-view pipeline:
303
+ * 1. derive task-group runtime state from the chosen base seed,
304
+ * 2. apply workflow overlays,
305
+ * 3. normalize the final workflow result shape.
306
+ *
307
+ * @param {{
308
+ * projectRoot: string,
309
+ * changeId: string | null,
310
+ * baseView: {
311
+ * source: string,
312
+ * stageId: string,
313
+ * findings: { blockers: string[], warnings: string[], notes: string[] },
314
+ * baseGates: Object.<string, string>,
315
+ * completionAudit: object | null,
316
+ * taskGroupSeed: Array<object>
317
+ * },
318
+ * plannedTaskGroups: Array<object>,
319
+ * changeSignals: Array<object>,
320
+ * signalSummary: Object.<string, object>,
321
+ * planningSignalFreshness: object,
322
+ * integrityAudit: object | null,
323
+ * disciplineState: object | null,
324
+ * verificationFreshness: object | null,
325
+ * hasTasksArtifact: boolean,
326
+ * decisionTraceRecords: Array<object> | null,
327
+ * checkpoints: Object.<string, string>,
328
+ * routeContext: object
329
+ * }} [options]
330
+ * @returns {object}
331
+ */
332
+ function buildWorkflowResultForBaseView(options = {}) {
1869
333
  const taskGroups = deriveTaskGroupRuntimeState(
1870
334
  options.plannedTaskGroups,
1871
335
  options.changeSignals,
1872
- options.taskGroupSeed,
1873
- decisionTraceRecords
1874
- );
1875
-
1876
- stageId = applyAuditFindings(stageId, findings, integrityAudit, completionAudit);
1877
- stageId = applyPlanningSignalFreshnessFindings(
1878
- stageId,
1879
- findings,
1880
- planningSignalFreshness,
1881
- decisionTraceRecords
1882
- );
1883
- stageId = applyExecutionSignalFindings(stageId, findings, planningSignalFreshness.effectiveSignalSummary || {});
1884
- applyTaskExecutionAndReviewFindings(findings, options.changeSignals || []);
1885
-
1886
- if (disciplineState) {
1887
- findings.blockers.push(...disciplineState.blockers);
1888
- findings.warnings.push(...disciplineState.warnings);
1889
- findings.notes.push(...disciplineState.notes);
1890
- if (disciplineState.blockers.length > 0 && ["build", "verify", "complete"].includes(stageId)) {
1891
- stageId = options.hasTasksArtifact ? "tasks" : "design";
1892
- }
1893
- }
1894
-
1895
- if (verificationFreshness && !verificationFreshness.fresh && (stageId === "verify" || stageId === "complete")) {
1896
- const stageBeforeFreshness = stageId;
1897
- findings.blockers.push(
1898
- "Completion-facing routing requires fresh verification evidence; stale evidence keeps the route in verify."
1899
- );
1900
- stageId = "verify";
1901
- if (stageBeforeFreshness === "complete") {
1902
- recordWorkflowDecision(decisionTraceRecords, {
1903
- decisionFamily: "verification_freshness_downgrade",
1904
- decisionKey: "verification_freshness_stale",
1905
- outcome: "downgraded",
1906
- reasonSummary: "Completion-facing routing stays in verify because verification evidence is stale.",
1907
- context: {
1908
- fromStage: "complete",
1909
- toStage: "verify",
1910
- baselineIso: verificationFreshness.baselineIso || null,
1911
- staleReasonCount: Array.isArray(verificationFreshness.staleReasons)
1912
- ? verificationFreshness.staleReasons.length
1913
- : 0,
1914
- requiredSurfaces: Array.isArray(verificationFreshness.requiredSurfaces)
1915
- ? verificationFreshness.requiredSurfaces
1916
- : []
1917
- },
1918
- evidenceRefs: collectVerificationFreshnessEvidenceRefs(verificationFreshness)
1919
- });
1920
- }
1921
- }
1922
-
1923
- const gates = buildGatesWithLiveOverlays(
1924
- options.baseGates,
1925
- completionAudit,
1926
- disciplineState,
1927
- verificationFreshness
336
+ options.baseView.taskGroupSeed,
337
+ options.decisionTraceRecords,
338
+ recordWorkflowDecision
1928
339
  );
1929
- const executionProfile = deriveExecutionProfile({
1930
- stage: stageId,
1931
- taskGroups
340
+ const overlay = applyWorkflowOverlays({
341
+ projectRoot: options.projectRoot,
342
+ changeId: options.changeId,
343
+ stageId: options.baseView.stageId,
344
+ findings: options.baseView.findings,
345
+ baseGates: options.baseView.baseGates,
346
+ taskGroups,
347
+ changeSignals: options.changeSignals,
348
+ signalSummary: options.signalSummary,
349
+ planningSignalFreshness: options.planningSignalFreshness,
350
+ integrityAudit: options.integrityAudit,
351
+ completionAudit: options.baseView.completionAudit,
352
+ disciplineState: options.disciplineState,
353
+ verificationFreshness: options.verificationFreshness,
354
+ hasTasksArtifact: options.hasTasksArtifact,
355
+ decisionTraceRecords: options.decisionTraceRecords,
356
+ recordWorkflowDecision
1932
357
  });
1933
- let worktreePreflight = null;
1934
- if (options.changeId && (stageId === "build" || stageId === "verify")) {
1935
- worktreePreflight = runWorktreePreflight(options.projectRoot, {
1936
- parallelPreferred: executionProfile.mode === "bounded_parallel"
1937
- });
1938
- if (
1939
- executionProfile.mode === "bounded_parallel" &&
1940
- worktreePreflight.summary &&
1941
- worktreePreflight.summary.recommendedIsolation
1942
- ) {
1943
- executionProfile.effectiveMode = "serial";
1944
- executionProfile.rationale = dedupeMessages([
1945
- ...(executionProfile.rationale || []),
1946
- "worktree preflight recommends isolation; effective mode downgraded to serial"
1947
- ]);
1948
- findings.warnings.push(
1949
- "Bounded-parallel profile downgraded to serial until worktree isolation is ready or explicitly accepted."
1950
- );
1951
- recordWorkflowDecision(decisionTraceRecords, {
1952
- decisionFamily: "worktree_isolation_downgrade",
1953
- decisionKey: "effective_serial_after_preflight",
1954
- outcome: "downgraded",
1955
- reasonSummary: "Worktree preflight downgraded advisory bounded parallel execution to effective serial mode.",
1956
- context: {
1957
- advisoryMode: executionProfile.mode,
1958
- effectiveMode: executionProfile.effectiveMode || "serial",
1959
- preflightStatus: worktreePreflight.status || null,
1960
- recommendedIsolation: Boolean(
1961
- worktreePreflight.summary && worktreePreflight.summary.recommendedIsolation
1962
- ),
1963
- dirtyEntries:
1964
- worktreePreflight.summary &&
1965
- Number.isFinite(Number(worktreePreflight.summary.dirtyEntries))
1966
- ? Number(worktreePreflight.summary.dirtyEntries)
1967
- : 0
1968
- },
1969
- evidenceRefs: ["surface:worktree-preflight"]
1970
- });
1971
- }
1972
- }
1973
-
1974
- dedupeFindings(findings);
1975
- const blockingGate = selectBlockingGateIdentity(findings);
1976
358
 
1977
- return buildWorkflowResult({
359
+ return buildWorkflowResultFromOverlay({
1978
360
  projectRoot: options.projectRoot,
1979
361
  changeId: options.changeId,
1980
- stageId,
1981
- findings,
1982
- checkpoints: options.checkpoints || {},
1983
- gates,
362
+ checkpoints: options.checkpoints,
363
+ routeContext: options.routeContext,
364
+ source: options.baseView.source,
365
+ overlay,
1984
366
  audits: {
1985
- integrity: integrityAudit,
1986
- completion: completionAudit
367
+ integrity: options.integrityAudit,
368
+ completion: options.baseView.completionAudit
1987
369
  },
1988
- routeContext: options.routeContext,
1989
- source: options.source || "derived",
1990
- taskGroups,
1991
- discipline: disciplineState,
1992
- executionProfile,
1993
- worktreePreflight,
1994
- verificationFreshness,
1995
- blockingGate,
1996
- needsRerunSurfaces: planningSignalFreshness.needsRerunSurfaces,
1997
- stalePlanningSignals: planningSignalFreshness.stalePlanningSignals
370
+ discipline: options.disciplineState,
371
+ verificationFreshness: options.verificationFreshness
1998
372
  });
1999
373
  }
2000
374
 
2001
- function selectFocusedTaskGroup(taskGroups) {
2002
- const priority = {
2003
- blocked: 0,
2004
- review_pending: 1,
2005
- in_progress: 2,
2006
- pending: 3,
2007
- completed: 4
2008
- };
2009
- const groups = Array.isArray(taskGroups) ? taskGroups : [];
2010
- return groups
2011
- .slice()
2012
- .sort((left, right) => {
2013
- const leftRank = Object.prototype.hasOwnProperty.call(priority, left.status) ? priority[left.status] : 5;
2014
- const rightRank = Object.prototype.hasOwnProperty.call(priority, right.status) ? priority[right.status] : 5;
2015
- if (leftRank !== rightRank) {
2016
- return leftRank - rightRank;
2017
- }
2018
- const leftIndex =
2019
- left && left.resumeCursor && Number.isInteger(left.resumeCursor.groupIndex)
2020
- ? left.resumeCursor.groupIndex
2021
- : Number.MAX_SAFE_INTEGER;
2022
- const rightIndex =
2023
- right && right.resumeCursor && Number.isInteger(right.resumeCursor.groupIndex)
2024
- ? right.resumeCursor.groupIndex
2025
- : Number.MAX_SAFE_INTEGER;
2026
- return leftIndex - rightIndex;
2027
- })[0] || null;
2028
- }
2029
-
2030
- function statusFromFindings(findings) {
2031
- if (findings.blockers.length > 0) {
2032
- return STATUS.BLOCK;
2033
- }
2034
- if (findings.warnings.length > 0) {
2035
- return STATUS.WARN;
2036
- }
2037
- return STATUS.PASS;
2038
- }
2039
-
375
+ /**
376
+ * Main workflow-status orchestrator.
377
+ *
378
+ * Responsibilities:
379
+ * 1. resolve the active project/change context,
380
+ * 2. gather artifact, signal, audit, and freshness inputs,
381
+ * 3. choose a trusted persisted or artifact-derived base view,
382
+ * 4. apply runtime overlays,
383
+ * 5. persist refreshed derived snapshots when appropriate.
384
+ *
385
+ * @param {string} projectPathInput
386
+ * @param {{
387
+ * changeId?: string,
388
+ * staleWindowMs?: number,
389
+ * env?: object,
390
+ * traceSurface?: string
391
+ * }} [options]
392
+ * @returns {object}
393
+ */
2040
394
  function deriveWorkflowStatus(projectPathInput, options = {}) {
2041
395
  const projectRoot = path.resolve(projectPathInput || process.cwd());
2042
396
  const requestedChangeId = options.changeId ? String(options.changeId).trim() : "";
@@ -2153,15 +507,6 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
2153
507
  }
2154
508
  : null;
2155
509
  const integrityAudit = collectIntegrityAudit(projectRoot, workflowRoot, routingSignalSummary);
2156
- const designCheckpointStatus = normalizeCheckpointStatus(checkpointStatuses[CHECKPOINT_LABELS.DESIGN]);
2157
- const designSourceCheckpointStatus = normalizeCheckpointStatus(
2158
- checkpointStatuses[CHECKPOINT_LABELS.DESIGN_SOURCE]
2159
- );
2160
- const runtimeGateCheckpointStatus = normalizeCheckpointStatus(
2161
- checkpointStatuses[CHECKPOINT_LABELS.RUNTIME_GATE]
2162
- );
2163
- const mappingCheckpointStatus = normalizeCheckpointStatus(checkpointStatuses[CHECKPOINT_LABELS.MAPPING]);
2164
- const taskCheckpointStatus = normalizeCheckpointStatus(checkpointStatuses[CHECKPOINT_LABELS.TASK]);
2165
510
  const verificationFreshness = activeChangeId
2166
511
  ? collectVerificationFreshness(projectRoot, {
2167
512
  changeId: activeChangeId,
@@ -2175,6 +520,8 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
2175
520
  ambiguousChangeSelection
2176
521
  };
2177
522
 
523
+ // Persisted snapshots can be reused only when the fingerprint still matches
524
+ // current artifact/signal truth; otherwise we fall back to the derived path.
2178
525
  if (activeChangeId) {
2179
526
  const persistedSelection = selectPersistedStateForChange(projectRoot, activeChangeId, {
2180
527
  staleWindowMs: options.staleWindowMs
@@ -2201,51 +548,49 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
2201
548
  evidenceRefs: [`state:${formatPathRef(projectRoot, persistedSelection.statePath)}`]
2202
549
  });
2203
550
  const persistedRecord = persistedSelection.changeRecord;
2204
- const stageRecord = getStageById(persistedRecord.stage) || getStageById("bootstrap");
2205
- const completionAudit =
2206
- stageRecord.id === "verify" || stageRecord.id === "complete"
2207
- ? collectCompletionAudit(projectRoot, workflowRoot, activeChangeId, routingSignalSummary)
2208
- : null;
2209
551
  const persistedSeed = resolvePersistedTaskGroupSeed(
2210
552
  projectRoot,
2211
553
  activeChangeId,
2212
554
  persistedRecord,
2213
555
  plannedTaskGroups,
2214
- decisionTraceRecords
556
+ decisionTraceRecords,
557
+ recordWorkflowDecision
2215
558
  );
559
+ const persistedBaseView = buildPersistedWorkflowBaseView({
560
+ persistedRecord,
561
+ findings: {
562
+ blockers: Array.isArray(persistedRecord.failures) ? persistedRecord.failures.slice() : [],
563
+ warnings: Array.isArray(persistedRecord.warnings) ? persistedRecord.warnings.slice() : [],
564
+ notes: [
565
+ ...sanitizePersistedNotes(persistedRecord.notes),
566
+ ...(Array.isArray(persistedSelection.advisoryNotes) ? persistedSelection.advisoryNotes : []),
567
+ ...persistedSeed.notes,
568
+ "workflow-status is using trusted persisted workflow state."
569
+ ]
570
+ },
571
+ taskGroupSeed: persistedSeed.taskGroups,
572
+ resolveCompletionAudit(stageId) {
573
+ return stageId === "verify" || stageId === "complete"
574
+ ? collectCompletionAudit(projectRoot, workflowRoot, activeChangeId, routingSignalSummary)
575
+ : null;
576
+ }
577
+ });
2216
578
  return finalizeResultWithWorkflowDecisionTracing(
2217
- finalizeWorkflowView({
579
+ buildWorkflowResultForBaseView({
2218
580
  projectRoot,
2219
581
  changeId: activeChangeId,
2220
- stageId: stageRecord.id,
2221
- findings: {
2222
- blockers: Array.isArray(persistedRecord.failures) ? persistedRecord.failures.slice() : [],
2223
- warnings: Array.isArray(persistedRecord.warnings) ? persistedRecord.warnings.slice() : [],
2224
- notes: [
2225
- ...sanitizePersistedNotes(persistedRecord.notes),
2226
- ...(Array.isArray(persistedSelection.advisoryNotes) ? persistedSelection.advisoryNotes : []),
2227
- ...persistedSeed.notes,
2228
- "workflow-status is using trusted persisted workflow state."
2229
- ]
2230
- },
2231
- baseGates:
2232
- persistedRecord && persistedRecord.gates && typeof persistedRecord.gates === "object"
2233
- ? { ...persistedRecord.gates }
2234
- : {},
2235
- checkpoints: checkpointStatuses,
2236
- routeContext,
2237
- source: "persisted",
2238
- taskGroupSeed: persistedSeed.taskGroups,
582
+ baseView: persistedBaseView,
2239
583
  plannedTaskGroups,
2240
584
  changeSignals,
2241
585
  signalSummary,
2242
586
  planningSignalFreshness,
2243
587
  integrityAudit,
2244
- completionAudit,
2245
588
  disciplineState,
2246
589
  verificationFreshness,
2247
590
  hasTasksArtifact: artifactState.tasks,
2248
- decisionTraceRecords
591
+ decisionTraceRecords,
592
+ checkpoints: checkpointStatuses,
593
+ routeContext
2249
594
  }),
2250
595
  {
2251
596
  env: options.env,
@@ -2291,56 +636,36 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
2291
636
  warnings: findings.warnings.slice(),
2292
637
  notes: findings.notes.slice()
2293
638
  };
2294
- const stageId = deriveStageFromArtifacts(artifactState, checkpointStatuses, baseFindings);
2295
- const completionAudit =
2296
- stageId === "verify" || stageId === "complete"
2297
- ? collectCompletionAudit(projectRoot, workflowRoot, activeChangeId, routingSignalSummary)
2298
- : null;
2299
- const baseGates = {
2300
- [HANDOFF_GATES.BREAKDOWN_TO_DESIGN]:
2301
- artifactState.proposal && artifactState.specFiles.length > 0 ? STATUS.PASS : STATUS.BLOCK,
2302
- [HANDOFF_GATES.DESIGN_TO_TASKS]: mergeStatuses([
2303
- artifactState.design && artifactState.pencilDesign && artifactState.pencilBindings
2304
- ? STATUS.PASS
2305
- : STATUS.BLOCK,
2306
- designCheckpointStatus,
2307
- designSourceCheckpointStatus,
2308
- runtimeGateCheckpointStatus,
2309
- mappingCheckpointStatus
2310
- ]),
2311
- [HANDOFF_GATES.TASKS_TO_BUILD]: mergeStatuses([
2312
- artifactState.tasks ? STATUS.PASS : STATUS.BLOCK,
2313
- designCheckpointStatus,
2314
- designSourceCheckpointStatus,
2315
- runtimeGateCheckpointStatus,
2316
- mappingCheckpointStatus,
2317
- taskCheckpointStatus
2318
- ]),
2319
- [HANDOFF_GATES.BUILD_TO_VERIFY]: artifactState.verification ? STATUS.PASS : STATUS.BLOCK,
2320
- [HANDOFF_GATES.VERIFY_TO_COMPLETE]:
2321
- completionAudit && completionAudit.status === "PASS" ? STATUS.PASS : STATUS.WARN
2322
- };
2323
639
 
2324
- const derivedResult = finalizeWorkflowView({
2325
- projectRoot,
2326
- changeId: activeChangeId,
2327
- stageId,
640
+ // The derived path rebuilds base routing from artifacts, then re-applies
641
+ // live overlays before refreshing canonical task-group metadata and the
642
+ // persisted workflow snapshot.
643
+ const derivedBaseView = buildDerivedWorkflowBaseView({
644
+ artifactState,
645
+ checkpointStatuses,
2328
646
  findings: baseFindings,
2329
- baseGates,
2330
- checkpoints: checkpointStatuses,
2331
- routeContext,
2332
- source: "derived",
2333
647
  taskGroupSeed: plannedTaskGroups,
648
+ resolveCompletionAudit(stageId) {
649
+ return stageId === "verify" || stageId === "complete"
650
+ ? collectCompletionAudit(projectRoot, workflowRoot, activeChangeId, routingSignalSummary)
651
+ : null;
652
+ }
653
+ });
654
+ const derivedResult = buildWorkflowResultForBaseView({
655
+ projectRoot,
656
+ changeId: activeChangeId,
657
+ baseView: derivedBaseView,
2334
658
  plannedTaskGroups,
2335
659
  changeSignals,
2336
660
  signalSummary,
2337
661
  planningSignalFreshness,
2338
662
  integrityAudit,
2339
- completionAudit,
2340
663
  disciplineState,
2341
664
  verificationFreshness,
2342
665
  hasTasksArtifact: artifactState.tasks,
2343
- decisionTraceRecords
666
+ decisionTraceRecords,
667
+ checkpoints: checkpointStatuses,
668
+ routeContext
2344
669
  });
2345
670
 
2346
671
  if (activeChangeId && !ambiguousChangeSelection) {
@@ -2417,6 +742,30 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
2417
742
  });
2418
743
  }
2419
744
 
745
+ /**
746
+ * Build the public workflow-status payload.
747
+ *
748
+ * @param {{
749
+ * projectRoot: string,
750
+ * changeId: string | null,
751
+ * stageId: string,
752
+ * findings: { blockers: string[], warnings: string[], notes: string[] },
753
+ * checkpoints: Object.<string, string>,
754
+ * gates: Object.<string, string>,
755
+ * audits: { integrity: object | null, completion: object | null },
756
+ * routeContext: object,
757
+ * source?: string,
758
+ * taskGroups?: Array<object>,
759
+ * discipline?: object | null,
760
+ * executionProfile?: object | null,
761
+ * worktreePreflight?: object | null,
762
+ * verificationFreshness?: object | null,
763
+ * blockingGate?: object | null,
764
+ * needsRerunSurfaces?: string[],
765
+ * stalePlanningSignals?: Object.<string, object>
766
+ * }} params
767
+ * @returns {object}
768
+ */
2420
769
  function buildWorkflowResult(params) {
2421
770
  const stage = getStageById(params.stageId) || getStageById("bootstrap");
2422
771
  const nextStep = buildRouteRecommendation(params.stageId, params.routeContext);
@@ -2452,6 +801,12 @@ function buildWorkflowResult(params) {
2452
801
  };
2453
802
  }
2454
803
 
804
+ /**
805
+ * Render the human-readable `workflow-status` report.
806
+ *
807
+ * @param {object} result
808
+ * @returns {string}
809
+ */
2455
810
  function formatWorkflowStatusReport(result) {
2456
811
  const lines = [
2457
812
  "Da Vinci workflow-status",
@@ -2537,6 +892,12 @@ function formatWorkflowStatusReport(result) {
2537
892
  return lines.join("\n");
2538
893
  }
2539
894
 
895
+ /**
896
+ * Render the human-readable `next-step` report.
897
+ *
898
+ * @param {object} result
899
+ * @returns {string}
900
+ */
2540
901
  function formatNextStepReport(result) {
2541
902
  const lines = [
2542
903
  "Da Vinci next-step",