@xenonbyte/da-vinci-workflow 0.2.7 → 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.
@@ -0,0 +1,1033 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const {
4
+ parseDisciplineMarkers,
5
+ DISCIPLINE_MARKER_NAMES
6
+ } = require("./audit-parsers");
7
+ const { readTextIfExists, dedupeMessages } = require("./utils");
8
+ const { listSignalsBySurfacePrefix } = require("./execution-signals");
9
+ const { evaluatePlanningSignalFreshness } = require("./planning-signal-freshness");
10
+ const { deriveExecutionProfile } = require("./execution-profile");
11
+ const { runWorktreePreflight } = require("./worktree-preflight");
12
+ const { STATUS, HANDOFF_GATES, getStageById } = require("./workflow-contract");
13
+
14
+ const MAX_REPORTED_MESSAGES = 3;
15
+ const BLOCKING_GATE_PRIORITY = Object.freeze([
16
+ "clarify",
17
+ "scenarioQuality",
18
+ "analyze",
19
+ "taskCheckpoint",
20
+ "stalePlanningSignal",
21
+ "principleInheritance",
22
+ "lint-tasks",
23
+ "lint-spec",
24
+ "scope-check"
25
+ ]);
26
+ const PLANNING_SIGNAL_PROMOTION_FALLBACKS = Object.freeze({
27
+ "lint-spec": "breakdown",
28
+ "scope-check": "tasks",
29
+ "lint-tasks": "tasks"
30
+ });
31
+
32
+ /**
33
+ * @typedef {Object} WorkflowFindings
34
+ * @property {string[]} blockers
35
+ * @property {string[]} warnings
36
+ * @property {string[]} notes
37
+ * @property {Array<{ id: string, surface: string, reason: string, evidence: string[] }>} [blockingGates]
38
+ */
39
+
40
+ /**
41
+ * @typedef {Object} PlanningSignalFreshnessState
42
+ * @property {Object.<string, object>} effectiveSignalSummary
43
+ * @property {Object.<string, object>} stalePlanningSignals
44
+ * @property {string[]} needsRerunSurfaces
45
+ */
46
+
47
+ /**
48
+ * @typedef {Object} WorkflowDisciplineState
49
+ * @property {boolean} strictMode
50
+ * @property {boolean} hasAnyMarker
51
+ * @property {{ state: string, stale: boolean, marker: object | null }} designApproval
52
+ * @property {{ marker: object | null }} planSelfReview
53
+ * @property {{ marker: object | null }} operatorReviewAck
54
+ * @property {Array<object>} malformed
55
+ * @property {string[]} blockers
56
+ * @property {string[]} warnings
57
+ * @property {string[]} notes
58
+ */
59
+
60
+ /**
61
+ * @typedef {Object} WorkflowOverlayResult
62
+ * @property {string} stageId
63
+ * @property {WorkflowFindings} findings
64
+ * @property {Object.<string, string>} gates
65
+ * @property {Array<object>} taskGroups
66
+ * @property {object} executionProfile
67
+ * @property {object | null} worktreePreflight
68
+ * @property {object | null} blockingGate
69
+ * @property {string[]} needsRerunSurfaces
70
+ * @property {Object.<string, object>} stalePlanningSignals
71
+ */
72
+
73
+ function recordDecision(callback, records, record) {
74
+ if (typeof callback === "function") {
75
+ callback(records, record);
76
+ }
77
+ }
78
+
79
+ function dedupeFindings(findings) {
80
+ findings.blockers = dedupeMessages(findings.blockers);
81
+ findings.warnings = dedupeMessages(findings.warnings);
82
+ findings.notes = dedupeMessages(findings.notes);
83
+ }
84
+
85
+ function resolveDisciplineGateMode() {
86
+ const strictEnv = String(process.env.DA_VINCI_DISCIPLINE_REQUIRE_APPROVAL || "").trim();
87
+ const strict = strictEnv === "1" || /^true$/i.test(strictEnv);
88
+ return {
89
+ strict
90
+ };
91
+ }
92
+
93
+ function isStrictPromotionEnabled() {
94
+ const raw = String(process.env.DA_VINCI_DISCIPLINE_STRICT_PROMOTION || "").trim();
95
+ return raw === "1" || /^true$/i.test(raw);
96
+ }
97
+
98
+ function fallbackStageIfBeyond(currentStageId, targetStageId) {
99
+ const current = getStageById(currentStageId);
100
+ const target = getStageById(targetStageId);
101
+ if (!current || !target) {
102
+ return currentStageId;
103
+ }
104
+ if (current.order > target.order) {
105
+ return target.id;
106
+ }
107
+ return current.id;
108
+ }
109
+
110
+ function ensureGateTracking(findings) {
111
+ if (!Array.isArray(findings.blockingGates)) {
112
+ findings.blockingGates = [];
113
+ }
114
+ }
115
+
116
+ function collectGateEvidenceRefs(gate) {
117
+ return Array.isArray(gate && gate.evidence) ? gate.evidence.slice(0, MAX_REPORTED_MESSAGES) : [];
118
+ }
119
+
120
+ function addBlockingGateRecord(findings, gateId, surface, gate, reason) {
121
+ ensureGateTracking(findings);
122
+ findings.blockingGates.push({
123
+ id: gateId,
124
+ surface,
125
+ reason: String(reason || "").trim(),
126
+ evidence: collectGateEvidenceRefs(gate)
127
+ });
128
+ }
129
+
130
+ function normalizeSignalStatus(status) {
131
+ const normalized = String(status || "").trim().toUpperCase();
132
+ if (normalized === STATUS.BLOCK || normalized === STATUS.WARN || normalized === STATUS.PASS) {
133
+ return normalized;
134
+ }
135
+ return "";
136
+ }
137
+
138
+ function statusSeverity(status) {
139
+ if (status === STATUS.BLOCK) {
140
+ return 2;
141
+ }
142
+ if (status === STATUS.WARN) {
143
+ return 1;
144
+ }
145
+ if (status === STATUS.PASS) {
146
+ return 0;
147
+ }
148
+ return -1;
149
+ }
150
+
151
+ function getSignalGate(signal, gateKey) {
152
+ if (!signal || !signal.details || !signal.details.gates) {
153
+ return null;
154
+ }
155
+ const gates = signal.details.gates;
156
+ if (!gates || typeof gates !== "object" || !gateKey) {
157
+ return null;
158
+ }
159
+ return gates[gateKey] && typeof gates[gateKey] === "object" ? gates[gateKey] : null;
160
+ }
161
+
162
+ function resolveEffectiveGateStatus(gate, signal) {
163
+ const gateStatus = normalizeSignalStatus(gate && gate.status);
164
+ if (gateStatus) {
165
+ return gateStatus;
166
+ }
167
+ const signalStatus = normalizeSignalStatus(signal && signal.status);
168
+ return signalStatus || STATUS.PASS;
169
+ }
170
+
171
+ function statusTokenMatches(status, acceptedTokens) {
172
+ const normalized = String(status || "").trim().toUpperCase();
173
+ return acceptedTokens.includes(normalized);
174
+ }
175
+
176
+ function collectVerificationFreshnessEvidenceRefs(verificationFreshness) {
177
+ const surfaces =
178
+ verificationFreshness && verificationFreshness.surfaces && typeof verificationFreshness.surfaces === "object"
179
+ ? verificationFreshness.surfaces
180
+ : {};
181
+ return Object.keys(surfaces)
182
+ .filter((surface) => surfaces[surface] && surfaces[surface].stale && surfaces[surface].present)
183
+ .sort()
184
+ .map((surface) => `signal:${surface}`);
185
+ }
186
+
187
+ /**
188
+ * Inspect planning-signal freshness and remove stale planning signals from the effective routing summary.
189
+ *
190
+ * @param {string} projectRoot
191
+ * @param {string | null} changeId
192
+ * @param {Object.<string, object>} signalSummary
193
+ * @returns {PlanningSignalFreshnessState}
194
+ */
195
+ function collectPlanningSignalFreshnessState(projectRoot, changeId, signalSummary) {
196
+ const effectiveSignalSummary =
197
+ signalSummary && typeof signalSummary === "object"
198
+ ? { ...signalSummary }
199
+ : {};
200
+ const stalePlanningSignals = {};
201
+
202
+ if (!changeId) {
203
+ return {
204
+ effectiveSignalSummary,
205
+ stalePlanningSignals,
206
+ needsRerunSurfaces: []
207
+ };
208
+ }
209
+
210
+ for (const surface of Object.keys(PLANNING_SIGNAL_PROMOTION_FALLBACKS)) {
211
+ const signal = effectiveSignalSummary[surface];
212
+ if (!signal) {
213
+ continue;
214
+ }
215
+ const freshness = evaluatePlanningSignalFreshness(projectRoot, {
216
+ changeId,
217
+ surface,
218
+ signal
219
+ });
220
+ if (!freshness.applicable || freshness.fresh) {
221
+ continue;
222
+ }
223
+ stalePlanningSignals[surface] = {
224
+ surface,
225
+ reasons: Array.isArray(freshness.reasons) ? freshness.reasons.slice() : [],
226
+ signalStatus: normalizeSignalStatus(signal.status),
227
+ signalTimestamp: signal && signal.timestamp ? String(signal.timestamp) : null,
228
+ signalTimestampMs: freshness.signalTimestampMs,
229
+ latestArtifactMtimeMs: freshness.latestArtifactMtimeMs,
230
+ latestArtifactTimestamp:
231
+ freshness.latestArtifactMtimeMs > 0 ? new Date(freshness.latestArtifactMtimeMs).toISOString() : null,
232
+ staleByMs: freshness.staleByMs
233
+ };
234
+ delete effectiveSignalSummary[surface];
235
+ }
236
+
237
+ return {
238
+ effectiveSignalSummary,
239
+ stalePlanningSignals,
240
+ needsRerunSurfaces: Object.keys(stalePlanningSignals).sort()
241
+ };
242
+ }
243
+
244
+ function applyPlanningSignalFreshnessFindings(
245
+ stageId,
246
+ findings,
247
+ planningSignalFreshness,
248
+ decisionTraceRecords,
249
+ recordWorkflowDecision
250
+ ) {
251
+ let nextStageId = stageId;
252
+ const strictPromotion = isStrictPromotionEnabled();
253
+ const stalePlanningSignals =
254
+ planningSignalFreshness && planningSignalFreshness.stalePlanningSignals
255
+ ? planningSignalFreshness.stalePlanningSignals
256
+ : {};
257
+
258
+ for (const surface of Object.keys(stalePlanningSignals).sort()) {
259
+ const freshness = stalePlanningSignals[surface];
260
+ const reasonText =
261
+ Array.isArray(freshness.reasons) && freshness.reasons.length > 0
262
+ ? freshness.reasons.join(", ")
263
+ : "unknown_staleness_reason";
264
+ findings.warnings.push(
265
+ `Stale ${surface} planning signal requires rerun before routing can rely on it (${reasonText}).`
266
+ );
267
+ if (!strictPromotion) {
268
+ recordDecision(recordWorkflowDecision, decisionTraceRecords, {
269
+ decisionFamily: "planning_signal_freshness",
270
+ decisionKey: "stale_signal_rerun_required",
271
+ outcome: "rerun_required",
272
+ reasonSummary: `Stale ${surface} planning signal requires rerun before routing can rely on it.`,
273
+ context: {
274
+ planningSurface: surface,
275
+ strictPromotion: false,
276
+ signalStatus: freshness.signalStatus || null,
277
+ staleByMs: Number.isFinite(freshness.staleByMs) ? freshness.staleByMs : null,
278
+ reasons: Array.isArray(freshness.reasons) ? freshness.reasons : []
279
+ },
280
+ evidenceRefs: [`signal:${surface}`]
281
+ });
282
+ continue;
283
+ }
284
+ findings.blockers.push(
285
+ `DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; stale ${surface} planning signal blocks promotion until ${surface} is rerun.`
286
+ );
287
+ addBlockingGateRecord(
288
+ findings,
289
+ "stalePlanningSignal",
290
+ surface,
291
+ null,
292
+ `strict promotion requires rerun for stale ${surface} planning signal`
293
+ );
294
+ const fallbackStageId = PLANNING_SIGNAL_PROMOTION_FALLBACKS[surface] || nextStageId;
295
+ recordDecision(recordWorkflowDecision, decisionTraceRecords, {
296
+ decisionFamily: "planning_signal_freshness",
297
+ decisionKey: "stale_signal_strict_fallback",
298
+ outcome: "downgraded",
299
+ reasonSummary: `Strict promotion forces routing fallback because ${surface} planning signal is stale.`,
300
+ context: {
301
+ planningSurface: surface,
302
+ strictPromotion: true,
303
+ signalStatus: freshness.signalStatus || null,
304
+ fallbackStage: fallbackStageId,
305
+ staleByMs: Number.isFinite(freshness.staleByMs) ? freshness.staleByMs : null,
306
+ reasons: Array.isArray(freshness.reasons) ? freshness.reasons : []
307
+ },
308
+ evidenceRefs: [`signal:${surface}`]
309
+ });
310
+ nextStageId = fallbackStageIfBeyond(nextStageId, fallbackStageId);
311
+ }
312
+
313
+ return nextStageId;
314
+ }
315
+
316
+ function selectBlockingGateIdentity(findings) {
317
+ const candidates = Array.isArray(findings && findings.blockingGates) ? findings.blockingGates : [];
318
+ if (candidates.length === 0) {
319
+ return null;
320
+ }
321
+ const sorted = candidates
322
+ .slice()
323
+ .sort((left, right) => {
324
+ const leftPriority = BLOCKING_GATE_PRIORITY.indexOf(left.id);
325
+ const rightPriority = BLOCKING_GATE_PRIORITY.indexOf(right.id);
326
+ const normalizedLeft = leftPriority === -1 ? Number.MAX_SAFE_INTEGER : leftPriority;
327
+ const normalizedRight = rightPriority === -1 ? Number.MAX_SAFE_INTEGER : rightPriority;
328
+ if (normalizedLeft !== normalizedRight) {
329
+ return normalizedLeft - normalizedRight;
330
+ }
331
+ return String(left.surface || "").localeCompare(String(right.surface || ""));
332
+ });
333
+ return sorted[0];
334
+ }
335
+
336
+ /**
337
+ * Inspect discipline markers and staleness on design/tasks artifacts.
338
+ *
339
+ * @param {string} changeDir
340
+ * @returns {WorkflowDisciplineState}
341
+ */
342
+ function inspectDisciplineState(changeDir) {
343
+ const designPath = path.join(changeDir, "design.md");
344
+ const pencilDesignPath = path.join(changeDir, "pencil-design.md");
345
+ const pencilBindingsPath = path.join(changeDir, "pencil-bindings.md");
346
+ const tasksPath = path.join(changeDir, "tasks.md");
347
+ const designText = readTextIfExists(designPath);
348
+ const tasksText = readTextIfExists(tasksPath);
349
+ const designMarkers = parseDisciplineMarkers(designText);
350
+ const taskMarkers = parseDisciplineMarkers(tasksText);
351
+ const gateMode = resolveDisciplineGateMode();
352
+ const hasAnyMarker =
353
+ designMarkers.ordered.length > 0 ||
354
+ taskMarkers.ordered.length > 0 ||
355
+ designMarkers.malformed.length > 0 ||
356
+ taskMarkers.malformed.length > 0;
357
+
358
+ const designApproval =
359
+ designMarkers.markers[DISCIPLINE_MARKER_NAMES.designApproval] ||
360
+ taskMarkers.markers[DISCIPLINE_MARKER_NAMES.designApproval] ||
361
+ null;
362
+ const planSelfReview = taskMarkers.markers[DISCIPLINE_MARKER_NAMES.planSelfReview] || null;
363
+ const operatorReviewAck = taskMarkers.markers[DISCIPLINE_MARKER_NAMES.operatorReviewAck] || null;
364
+
365
+ const blockers = [];
366
+ const warnings = [];
367
+ const notes = [];
368
+ const designApprovalArtifacts = [
369
+ { label: "design.md", path: designPath },
370
+ { label: "pencil-design.md", path: pencilDesignPath },
371
+ { label: "pencil-bindings.md", path: pencilBindingsPath }
372
+ ];
373
+
374
+ for (const malformed of [...designMarkers.malformed, ...taskMarkers.malformed]) {
375
+ warnings.push(`Malformed discipline marker at line ${malformed.line}: ${malformed.reason}`);
376
+ }
377
+
378
+ let designApprovalState = "missing";
379
+ let designApprovalStale = false;
380
+ if (designApproval) {
381
+ if (!statusTokenMatches(designApproval.status, ["APPROVED", "PASS", "ACCEPTED"])) {
382
+ designApprovalState = "rejected";
383
+ blockers.push(
384
+ `Design approval marker is not approved (${designApproval.status}); keep workflow in tasks/design.`
385
+ );
386
+ } else {
387
+ designApprovalState = "approved";
388
+ if (designApproval.time) {
389
+ const approvalMs = Date.parse(designApproval.time);
390
+ const staleArtifacts = [];
391
+ if (Number.isFinite(approvalMs)) {
392
+ for (const artifact of designApprovalArtifacts) {
393
+ let artifactMtimeMs = 0;
394
+ try {
395
+ artifactMtimeMs = fs.statSync(artifact.path).mtimeMs;
396
+ } catch (_error) {
397
+ continue;
398
+ }
399
+ if (artifactMtimeMs > approvalMs) {
400
+ staleArtifacts.push(artifact.label);
401
+ }
402
+ }
403
+ }
404
+ if (staleArtifacts.length > 0) {
405
+ designApprovalState = "stale";
406
+ designApprovalStale = true;
407
+ blockers.push(
408
+ `Design approval marker is stale because design artifacts changed after approval timestamp: ${staleArtifacts.join(", ")}.`
409
+ );
410
+ }
411
+ } else {
412
+ warnings.push("Design approval marker is missing timestamp; staleness checks are limited.");
413
+ }
414
+ }
415
+ } else if (hasAnyMarker || gateMode.strict) {
416
+ blockers.push(
417
+ "Missing required design approval marker (`- design approval: APPROVED @ <ISO8601>`) for disciplined handoff."
418
+ );
419
+ } else {
420
+ warnings.push(
421
+ "Design approval marker is missing; legacy compatibility mode keeps this advisory. Set DA_VINCI_DISCIPLINE_REQUIRE_APPROVAL=1 to enforce."
422
+ );
423
+ notes.push("Legacy compatibility mode: missing discipline markers are advisory.");
424
+ }
425
+
426
+ if (!planSelfReview) {
427
+ warnings.push("Missing plan self-review marker in tasks.md (`- plan self review: PASS @ <ISO8601>`).");
428
+ } else if (!statusTokenMatches(planSelfReview.status, ["PASS", "APPROVED", "DONE"])) {
429
+ warnings.push(`Plan self-review marker is not PASS (${planSelfReview.status}).`);
430
+ }
431
+
432
+ if (!operatorReviewAck) {
433
+ warnings.push("Missing operator review acknowledgment marker in tasks.md (`- operator review ack: ACKNOWLEDGED @ <ISO8601>`).");
434
+ } else if (!statusTokenMatches(operatorReviewAck.status, ["ACKNOWLEDGED", "ACK", "CONFIRMED", "APPROVED"])) {
435
+ warnings.push(`Operator review acknowledgment marker is not acknowledged (${operatorReviewAck.status}).`);
436
+ }
437
+
438
+ return {
439
+ strictMode: gateMode.strict,
440
+ hasAnyMarker,
441
+ designApproval: {
442
+ state: designApprovalState,
443
+ stale: designApprovalStale,
444
+ marker: designApproval
445
+ },
446
+ planSelfReview: {
447
+ marker: planSelfReview
448
+ },
449
+ operatorReviewAck: {
450
+ marker: operatorReviewAck
451
+ },
452
+ malformed: [...designMarkers.malformed, ...taskMarkers.malformed],
453
+ blockers,
454
+ warnings,
455
+ notes
456
+ };
457
+ }
458
+
459
+ function mergeSignalBySurface(signals) {
460
+ const summary = {};
461
+ for (const signal of signals || []) {
462
+ const key = String(signal.surface || "").trim();
463
+ if (!key || summary[key]) {
464
+ continue;
465
+ }
466
+ summary[key] = signal;
467
+ }
468
+ return summary;
469
+ }
470
+
471
+ function applyTaskExecutionAndReviewFindings(findings, signals) {
472
+ const taskExecutionSignals = listSignalsBySurfacePrefix(signals, "task-execution.");
473
+ const taskReviewSignals = listSignalsBySurfacePrefix(signals, "task-review.");
474
+ const latestTaskExecution = mergeSignalBySurface(taskExecutionSignals);
475
+ const latestTaskReview = mergeSignalBySurface(taskReviewSignals);
476
+
477
+ for (const signal of Object.values(latestTaskExecution)) {
478
+ const envelope = signal.details && signal.details.envelope ? signal.details.envelope : null;
479
+ const outOfScopeWrites =
480
+ signal.details && Array.isArray(signal.details.outOfScopeWrites)
481
+ ? dedupeMessages(signal.details.outOfScopeWrites.map((item) => String(item || "").trim()).filter(Boolean))
482
+ : [];
483
+ const taskGroupId =
484
+ (envelope && envelope.taskGroupId) ||
485
+ String(signal.surface || "").replace(/^task-execution\./, "") ||
486
+ "unknown";
487
+ if (signal.status === STATUS.BLOCK) {
488
+ findings.blockers.push(`Task group ${taskGroupId} is BLOCKED in implementer status envelope.`);
489
+ } else if (signal.status === STATUS.WARN) {
490
+ findings.warnings.push(`Task group ${taskGroupId} has unresolved implementer concerns/context needs.`);
491
+ }
492
+ if (outOfScopeWrites.length > 0) {
493
+ findings.blockers.push(
494
+ `Task group ${taskGroupId} reported out-of-scope writes: ${outOfScopeWrites.join(", ")}.`
495
+ );
496
+ }
497
+ if (envelope && envelope.summary) {
498
+ findings.notes.push(`Implementer summary ${taskGroupId}: ${envelope.summary}`);
499
+ }
500
+ }
501
+
502
+ const reviewStateByGroup = {};
503
+ for (const signal of Object.values(latestTaskReview)) {
504
+ const envelope = signal.details && signal.details.envelope ? signal.details.envelope : null;
505
+ const taskGroupId =
506
+ (envelope && envelope.taskGroupId) ||
507
+ String(signal.surface || "").replace(/^task-review\./, "").split(".")[0] ||
508
+ "unknown";
509
+ const stage = envelope && envelope.stage ? envelope.stage : String(signal.surface || "").split(".").pop();
510
+ if (!reviewStateByGroup[taskGroupId]) {
511
+ reviewStateByGroup[taskGroupId] = {};
512
+ }
513
+ reviewStateByGroup[taskGroupId][stage] = signal.status;
514
+ if (signal.status === STATUS.BLOCK) {
515
+ findings.blockers.push(`Task review ${taskGroupId}/${stage} is BLOCK.`);
516
+ } else if (signal.status === STATUS.WARN) {
517
+ findings.warnings.push(`Task review ${taskGroupId}/${stage} is WARN and requires follow-up.`);
518
+ }
519
+ }
520
+
521
+ for (const [taskGroupId, state] of Object.entries(reviewStateByGroup)) {
522
+ if (state.quality && !state.spec) {
523
+ findings.blockers.push(
524
+ `Task review ordering violation for ${taskGroupId}: quality review exists without a prior spec review result.`
525
+ );
526
+ continue;
527
+ }
528
+ if (state.quality && state.spec === STATUS.WARN) {
529
+ findings.blockers.push(
530
+ `Task review ordering violation for ${taskGroupId}: quality review was recorded before spec review reached PASS.`
531
+ );
532
+ continue;
533
+ }
534
+ if (state.quality && state.spec === STATUS.BLOCK) {
535
+ findings.blockers.push(
536
+ `Task review ordering violation for ${taskGroupId}: quality review was recorded while spec review is BLOCK.`
537
+ );
538
+ }
539
+ }
540
+ }
541
+
542
+ function applyAuditFindings(stageId, findings, integrityAudit, completionAudit) {
543
+ let nextStageId = stageId;
544
+
545
+ if (integrityAudit && integrityAudit.status === "FAIL") {
546
+ findings.warnings.push("Integrity audit currently reports FAIL.");
547
+ } else if (integrityAudit && integrityAudit.status === "WARN") {
548
+ findings.warnings.push("Integrity audit currently reports WARN.");
549
+ }
550
+
551
+ if ((nextStageId === "verify" || nextStageId === "complete") && completionAudit) {
552
+ if (completionAudit.status === "PASS") {
553
+ if (nextStageId === "verify") {
554
+ nextStageId = "complete";
555
+ }
556
+ findings.notes.push("Completion audit reports PASS for the active change.");
557
+ } else if (completionAudit.status === "WARN") {
558
+ findings.warnings.push("Completion audit currently reports WARN.");
559
+ if (nextStageId === "complete") {
560
+ nextStageId = "verify";
561
+ }
562
+ } else if (completionAudit.status === "FAIL") {
563
+ findings.blockers.push("Completion audit currently reports FAIL.");
564
+ if (nextStageId === "complete") {
565
+ nextStageId = "verify";
566
+ }
567
+ }
568
+ }
569
+
570
+ return nextStageId;
571
+ }
572
+
573
+ function applyExecutionSignalFindings(stageId, findings, signalSummary) {
574
+ let nextStageId = stageId;
575
+ const strictPromotion = isStrictPromotionEnabled();
576
+ const signalParseIssue = signalSummary["signal-file-parse"];
577
+ if (signalParseIssue) {
578
+ const warningText =
579
+ Array.isArray(signalParseIssue.warnings) && signalParseIssue.warnings[0]
580
+ ? String(signalParseIssue.warnings[0])
581
+ : "Malformed execution signal files were detected.";
582
+ findings.warnings.push(warningText);
583
+ }
584
+
585
+ const diffSignal = signalSummary["diff-spec"];
586
+ if (diffSignal && diffSignal.status && diffSignal.status !== STATUS.PASS) {
587
+ findings.warnings.push(`Planning diff signal diff-spec reports ${diffSignal.status}.`);
588
+ }
589
+
590
+ const lintSpecSignal = signalSummary["lint-spec"];
591
+ const lintSpecGateConfigs = [
592
+ { id: "principleInheritance", label: "principleInheritance", fallbackStage: "breakdown" },
593
+ { id: "clarify", label: "clarify", fallbackStage: "breakdown" },
594
+ { id: "scenarioQuality", label: "scenarioQuality", fallbackStage: "breakdown" }
595
+ ];
596
+ let lintSpecGateObserved = false;
597
+ let strongestLintSpecGateStatus = "";
598
+ for (const config of lintSpecGateConfigs) {
599
+ const gate = getSignalGate(lintSpecSignal, config.id);
600
+ if (!gate) {
601
+ continue;
602
+ }
603
+ lintSpecGateObserved = true;
604
+ const gateStatus = resolveEffectiveGateStatus(gate, lintSpecSignal);
605
+ if (statusSeverity(gateStatus) > statusSeverity(strongestLintSpecGateStatus)) {
606
+ strongestLintSpecGateStatus = gateStatus;
607
+ }
608
+ const evidenceRefs = collectGateEvidenceRefs(gate);
609
+ const evidenceSuffix =
610
+ evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
611
+ if (gateStatus === STATUS.BLOCK) {
612
+ findings.blockers.push(
613
+ `lint-spec gate ${config.label} is BLOCK and prevents planning promotion.${evidenceSuffix}`
614
+ );
615
+ addBlockingGateRecord(
616
+ findings,
617
+ config.id,
618
+ "lint-spec",
619
+ gate,
620
+ `lint-spec gate ${config.label} is BLOCK`
621
+ );
622
+ nextStageId = fallbackStageIfBeyond(nextStageId, config.fallbackStage);
623
+ } else if (gateStatus === STATUS.WARN) {
624
+ findings.warnings.push(`lint-spec gate ${config.label} is WARN.${evidenceSuffix}`);
625
+ if (strictPromotion) {
626
+ findings.blockers.push(
627
+ `DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-spec gate ${config.label} WARN blocks promotion.`
628
+ );
629
+ addBlockingGateRecord(
630
+ findings,
631
+ config.id,
632
+ "lint-spec",
633
+ gate,
634
+ `strict promotion escalated lint-spec gate ${config.label} WARN`
635
+ );
636
+ nextStageId = fallbackStageIfBeyond(nextStageId, config.fallbackStage);
637
+ }
638
+ }
639
+ for (const message of Array.isArray(gate.compatibility) ? gate.compatibility : []) {
640
+ findings.notes.push(`lint-spec gate ${config.label} compatibility: ${message}`);
641
+ }
642
+ if (config.id === "clarify") {
643
+ for (const bounded of Array.isArray(gate.bounded) ? gate.bounded : []) {
644
+ findings.notes.push(`lint-spec gate clarify bounded ambiguity: ${bounded}`);
645
+ }
646
+ }
647
+ }
648
+ const lintSpecSignalStatus = normalizeSignalStatus(lintSpecSignal && lintSpecSignal.status);
649
+ if (
650
+ lintSpecSignal &&
651
+ (!lintSpecGateObserved || statusSeverity(lintSpecSignalStatus) > statusSeverity(strongestLintSpecGateStatus))
652
+ ) {
653
+ if (lintSpecSignalStatus === STATUS.BLOCK) {
654
+ findings.blockers.push("lint-spec signal is BLOCK.");
655
+ addBlockingGateRecord(
656
+ findings,
657
+ "lint-spec",
658
+ "lint-spec",
659
+ lintSpecSignal.details && lintSpecSignal.details.gates ? lintSpecSignal.details.gates : null,
660
+ "lint-spec signal is BLOCK"
661
+ );
662
+ nextStageId = fallbackStageIfBeyond(nextStageId, "breakdown");
663
+ } else if (lintSpecSignalStatus === STATUS.WARN) {
664
+ findings.warnings.push("lint-spec signal is WARN.");
665
+ if (strictPromotion) {
666
+ findings.blockers.push(
667
+ "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-spec WARN blocks promotion."
668
+ );
669
+ addBlockingGateRecord(
670
+ findings,
671
+ "lint-spec",
672
+ "lint-spec",
673
+ lintSpecSignal.details && lintSpecSignal.details.gates ? lintSpecSignal.details.gates : null,
674
+ "strict promotion escalated lint-spec WARN"
675
+ );
676
+ nextStageId = fallbackStageIfBeyond(nextStageId, "breakdown");
677
+ }
678
+ }
679
+ }
680
+
681
+ const analyzeSignal = signalSummary["scope-check"];
682
+ const analyzeGate = getSignalGate(analyzeSignal, "analyze");
683
+ let analyzeGateStatus = "";
684
+ if (analyzeGate) {
685
+ const evidenceRefs = collectGateEvidenceRefs(analyzeGate);
686
+ const evidenceSuffix =
687
+ evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
688
+ analyzeGateStatus = resolveEffectiveGateStatus(analyzeGate, analyzeSignal);
689
+ if (analyzeGateStatus === STATUS.BLOCK) {
690
+ findings.blockers.push(`scope-check gate analyze is BLOCK.${evidenceSuffix}`);
691
+ addBlockingGateRecord(
692
+ findings,
693
+ "analyze",
694
+ "scope-check",
695
+ analyzeGate,
696
+ "scope-check gate analyze is BLOCK"
697
+ );
698
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
699
+ } else if (analyzeGateStatus === STATUS.WARN) {
700
+ findings.warnings.push(`scope-check gate analyze is WARN.${evidenceSuffix}`);
701
+ if (strictPromotion) {
702
+ findings.blockers.push(
703
+ "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; scope-check gate analyze WARN blocks promotion."
704
+ );
705
+ addBlockingGateRecord(
706
+ findings,
707
+ "analyze",
708
+ "scope-check",
709
+ analyzeGate,
710
+ "strict promotion escalated scope-check gate analyze WARN"
711
+ );
712
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
713
+ }
714
+ }
715
+ for (const message of Array.isArray(analyzeGate.compatibility) ? analyzeGate.compatibility : []) {
716
+ findings.notes.push(`scope-check gate analyze compatibility: ${message}`);
717
+ }
718
+ }
719
+ const analyzeSignalStatus = normalizeSignalStatus(analyzeSignal && analyzeSignal.status);
720
+ if (
721
+ analyzeSignal &&
722
+ (!analyzeGate || statusSeverity(analyzeSignalStatus) > statusSeverity(analyzeGateStatus))
723
+ ) {
724
+ if (analyzeSignalStatus === STATUS.BLOCK) {
725
+ findings.blockers.push("scope-check signal is BLOCK.");
726
+ addBlockingGateRecord(
727
+ findings,
728
+ "scope-check",
729
+ "scope-check",
730
+ analyzeSignal.details && analyzeSignal.details.gates ? analyzeSignal.details.gates : null,
731
+ "scope-check signal is BLOCK"
732
+ );
733
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
734
+ } else if (analyzeSignalStatus === STATUS.WARN) {
735
+ findings.warnings.push("scope-check signal is WARN.");
736
+ if (strictPromotion) {
737
+ findings.blockers.push(
738
+ "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; scope-check WARN blocks promotion."
739
+ );
740
+ addBlockingGateRecord(
741
+ findings,
742
+ "scope-check",
743
+ "scope-check",
744
+ analyzeSignal.details && analyzeSignal.details.gates ? analyzeSignal.details.gates : null,
745
+ "strict promotion escalated scope-check WARN"
746
+ );
747
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
748
+ }
749
+ }
750
+ }
751
+
752
+ const lintTasksSignal = signalSummary["lint-tasks"];
753
+ const taskCheckpointGate = getSignalGate(lintTasksSignal, "taskCheckpoint");
754
+ const taskCheckpointGateStatus = taskCheckpointGate
755
+ ? resolveEffectiveGateStatus(taskCheckpointGate, lintTasksSignal)
756
+ : "";
757
+ const lintTasksSignalStatus = normalizeSignalStatus(lintTasksSignal && lintTasksSignal.status);
758
+ if (taskCheckpointGate && taskCheckpointGateStatus === STATUS.BLOCK) {
759
+ const evidenceRefs = collectGateEvidenceRefs(taskCheckpointGate);
760
+ const evidenceSuffix =
761
+ evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
762
+ findings.blockers.push(`lint-tasks task-checkpoint is BLOCK and prevents promotion into build.${evidenceSuffix}`);
763
+ addBlockingGateRecord(
764
+ findings,
765
+ "taskCheckpoint",
766
+ "lint-tasks",
767
+ taskCheckpointGate,
768
+ "lint-tasks task-checkpoint is BLOCK"
769
+ );
770
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
771
+ } else if (taskCheckpointGate && taskCheckpointGateStatus === STATUS.WARN) {
772
+ const evidenceRefs = collectGateEvidenceRefs(taskCheckpointGate);
773
+ const evidenceSuffix =
774
+ evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
775
+ findings.warnings.push(`lint-tasks task-checkpoint is WARN.${evidenceSuffix}`);
776
+ if (strictPromotion) {
777
+ findings.blockers.push(
778
+ "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-tasks WARN blocks promotion into build."
779
+ );
780
+ addBlockingGateRecord(
781
+ findings,
782
+ "taskCheckpoint",
783
+ "lint-tasks",
784
+ taskCheckpointGate,
785
+ "strict promotion escalated lint-tasks task-checkpoint WARN"
786
+ );
787
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
788
+ }
789
+ }
790
+ if (
791
+ lintTasksSignal &&
792
+ (!taskCheckpointGate || statusSeverity(lintTasksSignalStatus) > statusSeverity(taskCheckpointGateStatus))
793
+ ) {
794
+ if (lintTasksSignalStatus === STATUS.BLOCK) {
795
+ findings.blockers.push("lint-tasks signal is BLOCK.");
796
+ addBlockingGateRecord(
797
+ findings,
798
+ "lint-tasks",
799
+ "lint-tasks",
800
+ lintTasksSignal.details && lintTasksSignal.details.gates ? lintTasksSignal.details.gates : null,
801
+ "lint-tasks signal is BLOCK"
802
+ );
803
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
804
+ } else if (lintTasksSignalStatus === STATUS.WARN) {
805
+ findings.warnings.push("lint-tasks signal is WARN.");
806
+ if (strictPromotion) {
807
+ findings.blockers.push(
808
+ "DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-tasks WARN blocks promotion into build."
809
+ );
810
+ addBlockingGateRecord(
811
+ findings,
812
+ "lint-tasks",
813
+ "lint-tasks",
814
+ lintTasksSignal.details && lintTasksSignal.details.gates ? lintTasksSignal.details.gates : null,
815
+ "strict promotion escalated lint-tasks WARN"
816
+ );
817
+ nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
818
+ }
819
+ }
820
+ }
821
+ if (taskCheckpointGate) {
822
+ for (const message of Array.isArray(taskCheckpointGate.compatibility)
823
+ ? taskCheckpointGate.compatibility
824
+ : []) {
825
+ findings.notes.push(`lint-tasks gate taskCheckpoint compatibility: ${message}`);
826
+ }
827
+ }
828
+
829
+ const verificationSignal = signalSummary["verify-coverage"];
830
+ if (verificationSignal && verificationSignal.status === STATUS.BLOCK) {
831
+ findings.blockers.push("verify-coverage signal is BLOCK.");
832
+ if (nextStageId === "complete") {
833
+ nextStageId = "verify";
834
+ }
835
+ } else if (verificationSignal && verificationSignal.status === STATUS.WARN) {
836
+ findings.warnings.push("verify-coverage signal is WARN.");
837
+ }
838
+
839
+ return nextStageId;
840
+ }
841
+
842
+ function buildGatesWithLiveOverlays(baseGates, completionAudit, disciplineState, verificationFreshness) {
843
+ const gates = baseGates && typeof baseGates === "object" ? { ...baseGates } : {};
844
+ if (completionAudit) {
845
+ gates[HANDOFF_GATES.VERIFY_TO_COMPLETE] =
846
+ completionAudit.status === STATUS.PASS ? STATUS.PASS : STATUS.WARN;
847
+ }
848
+ if (disciplineState && disciplineState.blockers.length > 0) {
849
+ gates[HANDOFF_GATES.TASKS_TO_BUILD] = STATUS.BLOCK;
850
+ }
851
+ if (verificationFreshness && !verificationFreshness.fresh) {
852
+ gates[HANDOFF_GATES.VERIFY_TO_COMPLETE] = STATUS.BLOCK;
853
+ }
854
+ return gates;
855
+ }
856
+
857
+ /**
858
+ * Apply runtime/live overlays on top of a base workflow view.
859
+ *
860
+ * @param {{
861
+ * projectRoot: string,
862
+ * changeId?: string | null,
863
+ * stageId: string,
864
+ * findings: WorkflowFindings,
865
+ * baseGates: Object.<string, string>,
866
+ * taskGroups?: Array<object>,
867
+ * changeSignals?: Array<object>,
868
+ * signalSummary?: Object.<string, object>,
869
+ * planningSignalFreshness?: PlanningSignalFreshnessState,
870
+ * integrityAudit?: object | null,
871
+ * completionAudit?: object | null,
872
+ * disciplineState?: object | null,
873
+ * verificationFreshness?: object | null,
874
+ * hasTasksArtifact?: boolean,
875
+ * decisionTraceRecords?: Array<object> | null,
876
+ * recordWorkflowDecision?: Function
877
+ * }} [options]
878
+ * @returns {WorkflowOverlayResult}
879
+ */
880
+ function applyWorkflowOverlays(options = {}) {
881
+ const findings = {
882
+ blockers: Array.isArray(options.findings && options.findings.blockers)
883
+ ? options.findings.blockers.slice()
884
+ : [],
885
+ warnings: Array.isArray(options.findings && options.findings.warnings)
886
+ ? options.findings.warnings.slice()
887
+ : [],
888
+ notes: Array.isArray(options.findings && options.findings.notes)
889
+ ? options.findings.notes.slice()
890
+ : []
891
+ };
892
+ let stageId = options.stageId;
893
+ const integrityAudit = options.integrityAudit || null;
894
+ const completionAudit = options.completionAudit || null;
895
+ const disciplineState = options.disciplineState || null;
896
+ const verificationFreshness = options.verificationFreshness || null;
897
+ const planningSignalFreshness =
898
+ options.planningSignalFreshness && typeof options.planningSignalFreshness === "object"
899
+ ? options.planningSignalFreshness
900
+ : {
901
+ effectiveSignalSummary: options.signalSummary || {},
902
+ stalePlanningSignals: {},
903
+ needsRerunSurfaces: []
904
+ };
905
+ const taskGroups = Array.isArray(options.taskGroups) ? options.taskGroups : [];
906
+ const decisionTraceRecords = Array.isArray(options.decisionTraceRecords)
907
+ ? options.decisionTraceRecords
908
+ : null;
909
+ const recordWorkflowDecision = options.recordWorkflowDecision;
910
+
911
+ stageId = applyAuditFindings(stageId, findings, integrityAudit, completionAudit);
912
+ stageId = applyPlanningSignalFreshnessFindings(
913
+ stageId,
914
+ findings,
915
+ planningSignalFreshness,
916
+ decisionTraceRecords,
917
+ recordWorkflowDecision
918
+ );
919
+ stageId = applyExecutionSignalFindings(
920
+ stageId,
921
+ findings,
922
+ planningSignalFreshness.effectiveSignalSummary || {}
923
+ );
924
+ applyTaskExecutionAndReviewFindings(findings, options.changeSignals || []);
925
+
926
+ if (disciplineState) {
927
+ findings.blockers.push(...disciplineState.blockers);
928
+ findings.warnings.push(...disciplineState.warnings);
929
+ findings.notes.push(...disciplineState.notes);
930
+ if (disciplineState.blockers.length > 0 && ["build", "verify", "complete"].includes(stageId)) {
931
+ stageId = options.hasTasksArtifact ? "tasks" : "design";
932
+ }
933
+ }
934
+
935
+ if (verificationFreshness && !verificationFreshness.fresh && (stageId === "verify" || stageId === "complete")) {
936
+ const stageBeforeFreshness = stageId;
937
+ findings.blockers.push(
938
+ "Completion-facing routing requires fresh verification evidence; stale evidence keeps the route in verify."
939
+ );
940
+ stageId = "verify";
941
+ if (stageBeforeFreshness === "complete") {
942
+ recordDecision(recordWorkflowDecision, decisionTraceRecords, {
943
+ decisionFamily: "verification_freshness_downgrade",
944
+ decisionKey: "verification_freshness_stale",
945
+ outcome: "downgraded",
946
+ reasonSummary: "Completion-facing routing stays in verify because verification evidence is stale.",
947
+ context: {
948
+ fromStage: "complete",
949
+ toStage: "verify",
950
+ baselineIso: verificationFreshness.baselineIso || null,
951
+ staleReasonCount: Array.isArray(verificationFreshness.staleReasons)
952
+ ? verificationFreshness.staleReasons.length
953
+ : 0,
954
+ requiredSurfaces: Array.isArray(verificationFreshness.requiredSurfaces)
955
+ ? verificationFreshness.requiredSurfaces
956
+ : []
957
+ },
958
+ evidenceRefs: collectVerificationFreshnessEvidenceRefs(verificationFreshness)
959
+ });
960
+ }
961
+ }
962
+
963
+ const gates = buildGatesWithLiveOverlays(
964
+ options.baseGates,
965
+ completionAudit,
966
+ disciplineState,
967
+ verificationFreshness
968
+ );
969
+ const executionProfile = deriveExecutionProfile({
970
+ stage: stageId,
971
+ taskGroups
972
+ });
973
+ let worktreePreflight = null;
974
+ if (options.changeId && (stageId === "build" || stageId === "verify")) {
975
+ worktreePreflight = runWorktreePreflight(options.projectRoot, {
976
+ parallelPreferred: executionProfile.mode === "bounded_parallel"
977
+ });
978
+ if (
979
+ executionProfile.mode === "bounded_parallel" &&
980
+ worktreePreflight.summary &&
981
+ worktreePreflight.summary.recommendedIsolation
982
+ ) {
983
+ executionProfile.effectiveMode = "serial";
984
+ executionProfile.rationale = dedupeMessages([
985
+ ...(executionProfile.rationale || []),
986
+ "worktree preflight recommends isolation; effective mode downgraded to serial"
987
+ ]);
988
+ findings.warnings.push(
989
+ "Bounded-parallel profile downgraded to serial until worktree isolation is ready or explicitly accepted."
990
+ );
991
+ recordDecision(recordWorkflowDecision, decisionTraceRecords, {
992
+ decisionFamily: "worktree_isolation_downgrade",
993
+ decisionKey: "effective_serial_after_preflight",
994
+ outcome: "downgraded",
995
+ reasonSummary: "Worktree preflight downgraded advisory bounded parallel execution to effective serial mode.",
996
+ context: {
997
+ advisoryMode: executionProfile.mode,
998
+ effectiveMode: executionProfile.effectiveMode || "serial",
999
+ preflightStatus: worktreePreflight.status || null,
1000
+ recommendedIsolation: Boolean(
1001
+ worktreePreflight.summary && worktreePreflight.summary.recommendedIsolation
1002
+ ),
1003
+ dirtyEntries:
1004
+ worktreePreflight.summary &&
1005
+ Number.isFinite(Number(worktreePreflight.summary.dirtyEntries))
1006
+ ? Number(worktreePreflight.summary.dirtyEntries)
1007
+ : 0
1008
+ },
1009
+ evidenceRefs: ["surface:worktree-preflight"]
1010
+ });
1011
+ }
1012
+ }
1013
+
1014
+ dedupeFindings(findings);
1015
+
1016
+ return {
1017
+ stageId,
1018
+ findings,
1019
+ gates,
1020
+ taskGroups,
1021
+ executionProfile,
1022
+ worktreePreflight,
1023
+ blockingGate: selectBlockingGateIdentity(findings),
1024
+ needsRerunSurfaces: planningSignalFreshness.needsRerunSurfaces,
1025
+ stalePlanningSignals: planningSignalFreshness.stalePlanningSignals
1026
+ };
1027
+ }
1028
+
1029
+ module.exports = {
1030
+ collectPlanningSignalFreshnessState,
1031
+ inspectDisciplineState,
1032
+ applyWorkflowOverlays
1033
+ };