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