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