@xenonbyte/da-vinci-workflow 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +7 -7
- package/README.zh-CN.md +7 -7
- package/lib/async-offload.js +39 -2
- package/lib/cli/command-handlers-core.js +132 -0
- package/lib/cli/command-handlers-design.js +129 -0
- package/lib/cli/command-handlers-pen.js +231 -0
- package/lib/cli/command-handlers-workflow.js +221 -0
- package/lib/cli/command-handlers.js +49 -0
- package/lib/cli/helpers.js +62 -0
- package/lib/cli.js +98 -533
- package/lib/execution-signals.js +33 -0
- package/lib/fs-safety.js +1 -12
- package/lib/path-inside.js +17 -0
- package/lib/utils.js +2 -7
- package/lib/workflow-base-view.js +134 -0
- package/lib/workflow-decision-trace.js +335 -0
- package/lib/workflow-overlay.js +1033 -0
- package/lib/workflow-persisted-state.js +4 -0
- package/lib/workflow-stage.js +244 -0
- package/lib/workflow-state.js +414 -1708
- package/lib/workflow-task-groups.js +881 -0
- package/lib/worktree-preflight.js +31 -11
- package/package.json +1 -1
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
const { parseTasksArtifact } = require("./planning-parsers");
|
|
2
|
+
const { STATUS, CHECKPOINT_LABELS } = require("./workflow-contract");
|
|
3
|
+
const { dedupeMessages, pathExists, readTextIfExists } = require("./utils");
|
|
4
|
+
const {
|
|
5
|
+
resolveWorkflowStatePath,
|
|
6
|
+
resolveTaskGroupMetadataPath,
|
|
7
|
+
readTaskGroupMetadata,
|
|
8
|
+
digestForPath
|
|
9
|
+
} = require("./workflow-persisted-state");
|
|
10
|
+
const { formatPathRef } = require("./workflow-decision-trace");
|
|
11
|
+
|
|
12
|
+
const TRACEABLE_TASK_GROUP_FOCUS_REASONS = new Set([
|
|
13
|
+
"implementer_block",
|
|
14
|
+
"implementer_warn",
|
|
15
|
+
"spec_review_missing",
|
|
16
|
+
"spec_review_block",
|
|
17
|
+
"spec_review_warn",
|
|
18
|
+
"quality_review_missing",
|
|
19
|
+
"quality_review_block",
|
|
20
|
+
"quality_review_warn"
|
|
21
|
+
]);
|
|
22
|
+
const TASK_GROUP_SEED_TRACE_KEYS = Object.freeze({
|
|
23
|
+
missing: "seed_missing",
|
|
24
|
+
unreadable: "seed_unreadable",
|
|
25
|
+
"digest-mismatch": "seed_digest_mismatch",
|
|
26
|
+
legacy: "seed_legacy_embedded"
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} TaskGroupPlan
|
|
31
|
+
* @property {string} taskGroupId
|
|
32
|
+
* @property {string} title
|
|
33
|
+
* @property {string} status
|
|
34
|
+
* @property {number} completion
|
|
35
|
+
* @property {string} checkpointOutcome
|
|
36
|
+
* @property {string[]} evidence
|
|
37
|
+
* @property {string} nextAction
|
|
38
|
+
* @property {string[]} [targetFiles]
|
|
39
|
+
* @property {string[]} [fileReferences]
|
|
40
|
+
* @property {string[]} [verificationActions]
|
|
41
|
+
* @property {string[]} [verificationCommands]
|
|
42
|
+
* @property {string[]} [executionIntent]
|
|
43
|
+
* @property {boolean} [reviewIntent]
|
|
44
|
+
* @property {boolean} [testingIntent]
|
|
45
|
+
* @property {boolean} [codeChangeLikely]
|
|
46
|
+
* @property {{ groupIndex: number | null, nextUncheckedItem: string | null }} [resumeCursor]
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Object} TaskGroupRuntimeEnvelope
|
|
51
|
+
* @property {boolean} present
|
|
52
|
+
* @property {string | null} [signalStatus]
|
|
53
|
+
* @property {string | null} [implementerStatus]
|
|
54
|
+
* @property {string | null} [summary]
|
|
55
|
+
* @property {string[]} [changedFiles]
|
|
56
|
+
* @property {string[]} [testEvidence]
|
|
57
|
+
* @property {string[]} [concerns]
|
|
58
|
+
* @property {string[]} [blockers]
|
|
59
|
+
* @property {string[]} [outOfScopeWrites]
|
|
60
|
+
* @property {string | null} [recordedAt]
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @typedef {Object} TaskGroupRuntimeState
|
|
65
|
+
* @property {string} taskGroupId
|
|
66
|
+
* @property {string} title
|
|
67
|
+
* @property {string} status
|
|
68
|
+
* @property {number} completion
|
|
69
|
+
* @property {string} checkpointOutcome
|
|
70
|
+
* @property {string[]} evidence
|
|
71
|
+
* @property {string} nextAction
|
|
72
|
+
* @property {string[]} targetFiles
|
|
73
|
+
* @property {string[]} fileReferences
|
|
74
|
+
* @property {string[]} verificationActions
|
|
75
|
+
* @property {string[]} verificationCommands
|
|
76
|
+
* @property {string[]} executionIntent
|
|
77
|
+
* @property {boolean} reviewIntent
|
|
78
|
+
* @property {boolean} testingIntent
|
|
79
|
+
* @property {boolean} codeChangeLikely
|
|
80
|
+
* @property {{ groupIndex: number | null, nextUncheckedItem: string | null, liveFocus?: string | null }} resumeCursor
|
|
81
|
+
* @property {object} planned
|
|
82
|
+
* @property {TaskGroupRuntimeEnvelope} implementer
|
|
83
|
+
* @property {object} review
|
|
84
|
+
* @property {object} effective
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @typedef {Object} TaskGroupSeedResolution
|
|
89
|
+
* @property {Array<object>} taskGroups
|
|
90
|
+
* @property {string[]} notes
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
function recordDecision(callback, records, record) {
|
|
94
|
+
if (typeof callback === "function") {
|
|
95
|
+
callback(records, record);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildTaskGroupFocusEvidenceRefs(taskGroupId, reason) {
|
|
100
|
+
if (reason === "implementer_block" || reason === "implementer_warn") {
|
|
101
|
+
return [`signal:task-execution.${taskGroupId}`];
|
|
102
|
+
}
|
|
103
|
+
if (reason === "spec_review_block" || reason === "spec_review_warn") {
|
|
104
|
+
return [`signal:task-review.${taskGroupId}.spec`];
|
|
105
|
+
}
|
|
106
|
+
if (reason === "quality_review_block" || reason === "quality_review_warn") {
|
|
107
|
+
return [`signal:task-review.${taskGroupId}.quality`];
|
|
108
|
+
}
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildTaskGroupFocusReasonSummary(taskGroupId, reason) {
|
|
113
|
+
switch (reason) {
|
|
114
|
+
case "implementer_block":
|
|
115
|
+
return `Implementer BLOCK overrides planned checklist focus for task group ${taskGroupId}.`;
|
|
116
|
+
case "implementer_warn":
|
|
117
|
+
return `Implementer WARN overrides planned checklist focus for task group ${taskGroupId}.`;
|
|
118
|
+
case "spec_review_missing":
|
|
119
|
+
return `Missing spec review takes focus over planned checklist work for task group ${taskGroupId}.`;
|
|
120
|
+
case "spec_review_block":
|
|
121
|
+
return `Spec review BLOCK takes focus over planned checklist work for task group ${taskGroupId}.`;
|
|
122
|
+
case "spec_review_warn":
|
|
123
|
+
return `Spec review WARN follow-up takes focus over planned checklist work for task group ${taskGroupId}.`;
|
|
124
|
+
case "quality_review_missing":
|
|
125
|
+
return `Missing quality review takes focus over planned checklist work for task group ${taskGroupId}.`;
|
|
126
|
+
case "quality_review_block":
|
|
127
|
+
return `Quality review BLOCK takes focus over planned checklist work for task group ${taskGroupId}.`;
|
|
128
|
+
case "quality_review_warn":
|
|
129
|
+
return `Quality review WARN follow-up takes focus over planned checklist work for task group ${taskGroupId}.`;
|
|
130
|
+
default:
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Parse `tasks.md` into canonical task-group plan metadata.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} tasksMarkdownText
|
|
139
|
+
* @param {Object.<string, string>} checkpointStatuses
|
|
140
|
+
* @returns {TaskGroupPlan[]}
|
|
141
|
+
*/
|
|
142
|
+
function deriveTaskGroupMetadata(tasksMarkdownText, checkpointStatuses) {
|
|
143
|
+
if (!tasksMarkdownText) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const taskArtifact = parseTasksArtifact(tasksMarkdownText);
|
|
148
|
+
const lines = String(tasksMarkdownText || "").replace(/\r\n?/g, "\n").split("\n");
|
|
149
|
+
const sections = [];
|
|
150
|
+
let current = null;
|
|
151
|
+
for (const line of lines) {
|
|
152
|
+
const headingMatch = line.match(/^\s{0,3}##\s+(\d+(?:\.\d+)*)\.\s+(.+)$/);
|
|
153
|
+
if (headingMatch) {
|
|
154
|
+
if (current) {
|
|
155
|
+
sections.push(current);
|
|
156
|
+
}
|
|
157
|
+
current = {
|
|
158
|
+
id: headingMatch[1],
|
|
159
|
+
title: String(headingMatch[2] || "").trim(),
|
|
160
|
+
checklistItems: []
|
|
161
|
+
};
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (!current) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const checklistMatch = line.match(/^\s*-\s*\[([ xX])\]\s+(.+)$/);
|
|
168
|
+
if (checklistMatch) {
|
|
169
|
+
current.checklistItems.push({
|
|
170
|
+
checked: String(checklistMatch[1] || "").toLowerCase() === "x",
|
|
171
|
+
text: String(checklistMatch[2] || "").trim()
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (current) {
|
|
176
|
+
sections.push(current);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const normalizedTaskCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.TASK] || STATUS.WARN;
|
|
180
|
+
const groupMetadataById = new Map(
|
|
181
|
+
(taskArtifact.taskGroups || []).map((group) => [
|
|
182
|
+
group.id,
|
|
183
|
+
{
|
|
184
|
+
targetFiles: Array.isArray(group.targetFiles) ? group.targetFiles : [],
|
|
185
|
+
fileReferences: Array.isArray(group.fileReferences) ? group.fileReferences : [],
|
|
186
|
+
verificationActions: Array.isArray(group.verificationActions) ? group.verificationActions : [],
|
|
187
|
+
verificationCommands: Array.isArray(group.verificationCommands) ? group.verificationCommands : [],
|
|
188
|
+
executionIntent: Array.isArray(group.executionIntent) ? group.executionIntent : [],
|
|
189
|
+
reviewIntent: group.reviewIntent === true,
|
|
190
|
+
testingIntent: group.testingIntent === true,
|
|
191
|
+
codeChangeLikely: group.codeChangeLikely === true
|
|
192
|
+
}
|
|
193
|
+
])
|
|
194
|
+
);
|
|
195
|
+
const metadata = sections.map((section, index) => {
|
|
196
|
+
const total = section.checklistItems.length;
|
|
197
|
+
const done = section.checklistItems.filter((item) => item.checked).length;
|
|
198
|
+
const completion = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
199
|
+
const sectionStatus =
|
|
200
|
+
total === 0 ? "pending" : done === 0 ? "pending" : done < total ? "in_progress" : "completed";
|
|
201
|
+
const nextAction =
|
|
202
|
+
sectionStatus === "completed"
|
|
203
|
+
? "advance to next task group"
|
|
204
|
+
: section.checklistItems.find((item) => !item.checked)?.text || "continue group work";
|
|
205
|
+
const groupMetadata = groupMetadataById.get(section.id) || {};
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
taskGroupId: section.id,
|
|
209
|
+
title: section.title,
|
|
210
|
+
status: sectionStatus,
|
|
211
|
+
completion,
|
|
212
|
+
checkpointOutcome: normalizedTaskCheckpoint,
|
|
213
|
+
evidence: section.checklistItems.filter((item) => item.checked).map((item) => item.text),
|
|
214
|
+
nextAction,
|
|
215
|
+
targetFiles: groupMetadata.targetFiles || [],
|
|
216
|
+
fileReferences: groupMetadata.fileReferences || [],
|
|
217
|
+
verificationActions: groupMetadata.verificationActions || [],
|
|
218
|
+
verificationCommands: groupMetadata.verificationCommands || [],
|
|
219
|
+
executionIntent: groupMetadata.executionIntent || [],
|
|
220
|
+
reviewIntent: groupMetadata.reviewIntent === true,
|
|
221
|
+
testingIntent: groupMetadata.testingIntent === true,
|
|
222
|
+
codeChangeLikely: groupMetadata.codeChangeLikely === true,
|
|
223
|
+
resumeCursor: {
|
|
224
|
+
groupIndex: index,
|
|
225
|
+
nextUncheckedItem:
|
|
226
|
+
section.checklistItems.find((item) => !item.checked)?.text ||
|
|
227
|
+
section.checklistItems[section.checklistItems.length - 1]?.text ||
|
|
228
|
+
null
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (metadata.length === 0 && taskArtifact.taskGroups.length > 0) {
|
|
234
|
+
return taskArtifact.taskGroups.map((group, index) => {
|
|
235
|
+
const groupMetadata = groupMetadataById.get(group.id) || {};
|
|
236
|
+
return {
|
|
237
|
+
taskGroupId: group.id,
|
|
238
|
+
title: group.title,
|
|
239
|
+
status: "pending",
|
|
240
|
+
completion: 0,
|
|
241
|
+
checkpointOutcome: normalizedTaskCheckpoint,
|
|
242
|
+
evidence: [],
|
|
243
|
+
nextAction: "start task group",
|
|
244
|
+
targetFiles: groupMetadata.targetFiles || [],
|
|
245
|
+
fileReferences: groupMetadata.fileReferences || [],
|
|
246
|
+
verificationActions: groupMetadata.verificationActions || [],
|
|
247
|
+
verificationCommands: groupMetadata.verificationCommands || [],
|
|
248
|
+
executionIntent: groupMetadata.executionIntent || [],
|
|
249
|
+
reviewIntent: groupMetadata.reviewIntent === true,
|
|
250
|
+
testingIntent: groupMetadata.testingIntent === true,
|
|
251
|
+
codeChangeLikely: groupMetadata.codeChangeLikely === true,
|
|
252
|
+
resumeCursor: {
|
|
253
|
+
groupIndex: index,
|
|
254
|
+
nextUncheckedItem: null
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return metadata;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function normalizeTaskGroupId(value) {
|
|
264
|
+
return String(value || "").trim();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function normalizeResumeCursor(cursor, fallbackGroupIndex = null) {
|
|
268
|
+
const groupIndex =
|
|
269
|
+
cursor && Number.isInteger(cursor.groupIndex)
|
|
270
|
+
? cursor.groupIndex
|
|
271
|
+
: Number.isInteger(fallbackGroupIndex)
|
|
272
|
+
? fallbackGroupIndex
|
|
273
|
+
: null;
|
|
274
|
+
return {
|
|
275
|
+
groupIndex,
|
|
276
|
+
nextUncheckedItem:
|
|
277
|
+
cursor && Object.prototype.hasOwnProperty.call(cursor, "nextUncheckedItem")
|
|
278
|
+
? cursor.nextUncheckedItem
|
|
279
|
+
: null
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function normalizeTaskGroupSeedMap(taskGroups) {
|
|
284
|
+
const byId = new Map();
|
|
285
|
+
for (const group of Array.isArray(taskGroups) ? taskGroups : []) {
|
|
286
|
+
const taskGroupId = normalizeTaskGroupId(group && (group.taskGroupId || group.id));
|
|
287
|
+
if (!taskGroupId || byId.has(taskGroupId)) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
byId.set(taskGroupId, group);
|
|
291
|
+
}
|
|
292
|
+
return byId;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function findLatestSignalBySurface(signals, surface) {
|
|
296
|
+
const normalizedSurface = String(surface || "").trim();
|
|
297
|
+
if (!normalizedSurface) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
return (signals || []).find((signal) => String(signal.surface || "").trim() === normalizedSurface) || null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function summarizeSignalIssues(signal, envelopeItems) {
|
|
304
|
+
if (!signal) {
|
|
305
|
+
return [];
|
|
306
|
+
}
|
|
307
|
+
const fromEnvelope = Array.isArray(envelopeItems) ? envelopeItems : [];
|
|
308
|
+
const fromSignal = [
|
|
309
|
+
...(Array.isArray(signal.failures) ? signal.failures : []),
|
|
310
|
+
...(Array.isArray(signal.warnings) ? signal.warnings : [])
|
|
311
|
+
];
|
|
312
|
+
return dedupeMessages([...fromEnvelope, ...fromSignal].map((item) => String(item || "").trim()).filter(Boolean));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function buildTaskGroupImplementerState(taskGroupId, signals, fallbackState) {
|
|
316
|
+
const fallback = fallbackState && typeof fallbackState === "object" ? fallbackState : {};
|
|
317
|
+
const signal = findLatestSignalBySurface(signals, `task-execution.${taskGroupId}`);
|
|
318
|
+
const envelope = signal && signal.details && signal.details.envelope ? signal.details.envelope : null;
|
|
319
|
+
if (!signal) {
|
|
320
|
+
return {
|
|
321
|
+
present: false,
|
|
322
|
+
signalStatus: null,
|
|
323
|
+
implementerStatus: fallback.implementerStatus || null,
|
|
324
|
+
summary: fallback.summary || null,
|
|
325
|
+
changedFiles: Array.isArray(fallback.changedFiles) ? fallback.changedFiles : [],
|
|
326
|
+
testEvidence: Array.isArray(fallback.testEvidence) ? fallback.testEvidence : [],
|
|
327
|
+
concerns: Array.isArray(fallback.concerns) ? fallback.concerns : [],
|
|
328
|
+
blockers: Array.isArray(fallback.blockers) ? fallback.blockers : [],
|
|
329
|
+
outOfScopeWrites: Array.isArray(fallback.outOfScopeWrites) ? fallback.outOfScopeWrites : [],
|
|
330
|
+
recordedAt: fallback.recordedAt || null
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
present: true,
|
|
336
|
+
signalStatus: signal.status || null,
|
|
337
|
+
implementerStatus: envelope && envelope.status ? envelope.status : fallback.implementerStatus || null,
|
|
338
|
+
summary: envelope && envelope.summary ? envelope.summary : fallback.summary || null,
|
|
339
|
+
changedFiles:
|
|
340
|
+
envelope && Array.isArray(envelope.changedFiles)
|
|
341
|
+
? envelope.changedFiles
|
|
342
|
+
: Array.isArray(fallback.changedFiles)
|
|
343
|
+
? fallback.changedFiles
|
|
344
|
+
: [],
|
|
345
|
+
testEvidence:
|
|
346
|
+
envelope && Array.isArray(envelope.testEvidence)
|
|
347
|
+
? envelope.testEvidence
|
|
348
|
+
: Array.isArray(fallback.testEvidence)
|
|
349
|
+
? fallback.testEvidence
|
|
350
|
+
: [],
|
|
351
|
+
concerns: summarizeSignalIssues(signal, envelope && envelope.concerns),
|
|
352
|
+
blockers: summarizeSignalIssues(signal, envelope && envelope.blockers),
|
|
353
|
+
outOfScopeWrites:
|
|
354
|
+
signal.details && Array.isArray(signal.details.outOfScopeWrites)
|
|
355
|
+
? dedupeMessages(signal.details.outOfScopeWrites.map((item) => String(item || "").trim()).filter(Boolean))
|
|
356
|
+
: Array.isArray(fallback.outOfScopeWrites)
|
|
357
|
+
? fallback.outOfScopeWrites
|
|
358
|
+
: [],
|
|
359
|
+
recordedAt: (envelope && envelope.recordedAt) || signal.timestamp || fallback.recordedAt || null
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function buildTaskGroupReviewStageState(taskGroupId, stage, signals, fallbackState) {
|
|
364
|
+
const fallback = fallbackState && typeof fallbackState === "object" ? fallbackState : {};
|
|
365
|
+
const signal = findLatestSignalBySurface(signals, `task-review.${taskGroupId}.${stage}`);
|
|
366
|
+
const envelope = signal && signal.details && signal.details.envelope ? signal.details.envelope : null;
|
|
367
|
+
if (!signal) {
|
|
368
|
+
return {
|
|
369
|
+
present: false,
|
|
370
|
+
status: fallback.status || "missing",
|
|
371
|
+
summary: fallback.summary || null,
|
|
372
|
+
reviewer: fallback.reviewer || null,
|
|
373
|
+
issues: Array.isArray(fallback.issues) ? fallback.issues : [],
|
|
374
|
+
recordedAt: fallback.recordedAt || null
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
present: true,
|
|
380
|
+
status: signal.status || "missing",
|
|
381
|
+
summary: envelope && envelope.summary ? envelope.summary : fallback.summary || null,
|
|
382
|
+
reviewer: envelope && envelope.reviewer ? envelope.reviewer : fallback.reviewer || null,
|
|
383
|
+
issues: summarizeSignalIssues(signal, envelope && envelope.issues),
|
|
384
|
+
recordedAt: (envelope && envelope.recordedAt) || signal.timestamp || fallback.recordedAt || null
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function buildTaskGroupReviewState(taskGroup, signals, fallbackState) {
|
|
389
|
+
const fallback = fallbackState && typeof fallbackState === "object" ? fallbackState : {};
|
|
390
|
+
const taskGroupId = normalizeTaskGroupId(taskGroup.taskGroupId || taskGroup.id);
|
|
391
|
+
const required = taskGroup.reviewIntent === true;
|
|
392
|
+
const spec = buildTaskGroupReviewStageState(taskGroupId, "spec", signals, fallback.spec);
|
|
393
|
+
const quality = buildTaskGroupReviewStageState(taskGroupId, "quality", signals, fallback.quality);
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
required,
|
|
397
|
+
ordering: required ? "spec_then_quality" : "none",
|
|
398
|
+
spec,
|
|
399
|
+
quality
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function buildEffectiveTaskGroupState(group, planned, implementer, review) {
|
|
404
|
+
const fallbackCursor = normalizeResumeCursor(planned.resumeCursor, null);
|
|
405
|
+
const effective = {
|
|
406
|
+
status: planned.status,
|
|
407
|
+
nextAction: planned.nextAction,
|
|
408
|
+
resumeCursor: fallbackCursor,
|
|
409
|
+
source: "planned",
|
|
410
|
+
reason: "planned_checklist"
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
if (implementer.present && implementer.outOfScopeWrites.length > 0) {
|
|
414
|
+
return {
|
|
415
|
+
status: "blocked",
|
|
416
|
+
nextAction:
|
|
417
|
+
`resolve out-of-scope writes for task group ${group.taskGroupId}: ${implementer.outOfScopeWrites.join(", ")}`,
|
|
418
|
+
resumeCursor: {
|
|
419
|
+
groupIndex: fallbackCursor.groupIndex,
|
|
420
|
+
nextUncheckedItem: null,
|
|
421
|
+
liveFocus: "out_of_scope_write"
|
|
422
|
+
},
|
|
423
|
+
source: "implementer",
|
|
424
|
+
reason: "out_of_scope_write"
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (implementer.present && implementer.signalStatus === STATUS.BLOCK) {
|
|
429
|
+
return {
|
|
430
|
+
status: "blocked",
|
|
431
|
+
nextAction:
|
|
432
|
+
implementer.blockers[0] ||
|
|
433
|
+
implementer.summary ||
|
|
434
|
+
`resolve implementer blocker for task group ${group.taskGroupId}`,
|
|
435
|
+
resumeCursor: {
|
|
436
|
+
groupIndex: fallbackCursor.groupIndex,
|
|
437
|
+
nextUncheckedItem: null,
|
|
438
|
+
liveFocus: "implementer_block"
|
|
439
|
+
},
|
|
440
|
+
source: "implementer",
|
|
441
|
+
reason: "implementer_block"
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const reviewSignalsPresent = review.spec.present || review.quality.present;
|
|
446
|
+
const plannedCompletion = Number.isFinite(Number(planned.completion))
|
|
447
|
+
? Number(planned.completion)
|
|
448
|
+
: 0;
|
|
449
|
+
const reviewNearComplete = planned.status !== "completed" && plannedCompletion >= 75;
|
|
450
|
+
const reviewHardDue = planned.status === "completed" || reviewNearComplete;
|
|
451
|
+
const reviewContextReady = review.required && (reviewSignalsPresent || reviewHardDue || implementer.present);
|
|
452
|
+
|
|
453
|
+
if (reviewContextReady) {
|
|
454
|
+
if (
|
|
455
|
+
review.quality.present &&
|
|
456
|
+
(!review.spec.present || review.spec.status === "missing" || review.spec.status === STATUS.WARN)
|
|
457
|
+
) {
|
|
458
|
+
return {
|
|
459
|
+
status: "blocked",
|
|
460
|
+
nextAction:
|
|
461
|
+
`remove or rerun out-of-order quality review for task group ${group.taskGroupId} after spec review PASS`,
|
|
462
|
+
resumeCursor: {
|
|
463
|
+
groupIndex: fallbackCursor.groupIndex,
|
|
464
|
+
nextUncheckedItem: null,
|
|
465
|
+
liveFocus: "review_ordering_violation"
|
|
466
|
+
},
|
|
467
|
+
source: "review",
|
|
468
|
+
reason: "review_ordering_violation"
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
if (review.spec.status === STATUS.BLOCK) {
|
|
472
|
+
return {
|
|
473
|
+
status: "blocked",
|
|
474
|
+
nextAction:
|
|
475
|
+
review.spec.issues[0] || review.spec.summary || `resolve spec review BLOCK for task group ${group.taskGroupId}`,
|
|
476
|
+
resumeCursor: {
|
|
477
|
+
groupIndex: fallbackCursor.groupIndex,
|
|
478
|
+
nextUncheckedItem: null,
|
|
479
|
+
liveFocus: "spec_review_block"
|
|
480
|
+
},
|
|
481
|
+
source: "review",
|
|
482
|
+
reason: "spec_review_block"
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
if (!review.spec.present || review.spec.status === "missing") {
|
|
486
|
+
if (reviewHardDue || reviewSignalsPresent) {
|
|
487
|
+
return {
|
|
488
|
+
status: "review_pending",
|
|
489
|
+
nextAction: `record spec review PASS or WARN for task group ${group.taskGroupId} before quality review`,
|
|
490
|
+
resumeCursor: {
|
|
491
|
+
groupIndex: fallbackCursor.groupIndex,
|
|
492
|
+
nextUncheckedItem: null,
|
|
493
|
+
liveFocus: "spec_review_missing"
|
|
494
|
+
},
|
|
495
|
+
source: "review",
|
|
496
|
+
reason: "spec_review_missing"
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
} else {
|
|
500
|
+
const specWarn = review.spec.status === STATUS.WARN;
|
|
501
|
+
if (review.quality.status === STATUS.BLOCK) {
|
|
502
|
+
return {
|
|
503
|
+
status: "blocked",
|
|
504
|
+
nextAction:
|
|
505
|
+
review.quality.issues[0] ||
|
|
506
|
+
review.quality.summary ||
|
|
507
|
+
`resolve quality review BLOCK for task group ${group.taskGroupId}`,
|
|
508
|
+
resumeCursor: {
|
|
509
|
+
groupIndex: fallbackCursor.groupIndex,
|
|
510
|
+
nextUncheckedItem: null,
|
|
511
|
+
liveFocus: "quality_review_block"
|
|
512
|
+
},
|
|
513
|
+
source: "review",
|
|
514
|
+
reason: "quality_review_block"
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
if (!review.quality.present || review.quality.status === "missing") {
|
|
518
|
+
if (reviewHardDue) {
|
|
519
|
+
return {
|
|
520
|
+
status: "review_pending",
|
|
521
|
+
nextAction: `record quality review PASS or WARN for task group ${group.taskGroupId}`,
|
|
522
|
+
resumeCursor: {
|
|
523
|
+
groupIndex: fallbackCursor.groupIndex,
|
|
524
|
+
nextUncheckedItem: null,
|
|
525
|
+
liveFocus: "quality_review_missing"
|
|
526
|
+
},
|
|
527
|
+
source: "review",
|
|
528
|
+
reason: "quality_review_missing"
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
if (specWarn) {
|
|
532
|
+
return {
|
|
533
|
+
status: "in_progress",
|
|
534
|
+
nextAction:
|
|
535
|
+
review.spec.issues[0] ||
|
|
536
|
+
review.spec.summary ||
|
|
537
|
+
`resolve spec review follow-up for task group ${group.taskGroupId}`,
|
|
538
|
+
resumeCursor: {
|
|
539
|
+
groupIndex: fallbackCursor.groupIndex,
|
|
540
|
+
nextUncheckedItem: null,
|
|
541
|
+
liveFocus: "spec_review_warn"
|
|
542
|
+
},
|
|
543
|
+
source: "review",
|
|
544
|
+
reason: "spec_review_warn"
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
} else if (review.quality.status === STATUS.WARN) {
|
|
548
|
+
return {
|
|
549
|
+
status: "in_progress",
|
|
550
|
+
nextAction:
|
|
551
|
+
review.quality.issues[0] ||
|
|
552
|
+
review.quality.summary ||
|
|
553
|
+
`resolve quality review follow-up for task group ${group.taskGroupId}`,
|
|
554
|
+
resumeCursor: {
|
|
555
|
+
groupIndex: fallbackCursor.groupIndex,
|
|
556
|
+
nextUncheckedItem: null,
|
|
557
|
+
liveFocus: "quality_review_warn"
|
|
558
|
+
},
|
|
559
|
+
source: "review",
|
|
560
|
+
reason: "quality_review_warn"
|
|
561
|
+
};
|
|
562
|
+
} else if (specWarn) {
|
|
563
|
+
return {
|
|
564
|
+
status: "in_progress",
|
|
565
|
+
nextAction:
|
|
566
|
+
review.spec.issues[0] ||
|
|
567
|
+
review.spec.summary ||
|
|
568
|
+
`resolve spec review follow-up for task group ${group.taskGroupId}`,
|
|
569
|
+
resumeCursor: {
|
|
570
|
+
groupIndex: fallbackCursor.groupIndex,
|
|
571
|
+
nextUncheckedItem: null,
|
|
572
|
+
liveFocus: "spec_review_warn"
|
|
573
|
+
},
|
|
574
|
+
source: "review",
|
|
575
|
+
reason: "spec_review_warn"
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (implementer.present && implementer.signalStatus === STATUS.WARN) {
|
|
582
|
+
return {
|
|
583
|
+
status: "in_progress",
|
|
584
|
+
nextAction:
|
|
585
|
+
implementer.concerns[0] ||
|
|
586
|
+
implementer.summary ||
|
|
587
|
+
`resolve implementer concerns for task group ${group.taskGroupId}`,
|
|
588
|
+
resumeCursor: {
|
|
589
|
+
groupIndex: fallbackCursor.groupIndex,
|
|
590
|
+
nextUncheckedItem: null,
|
|
591
|
+
liveFocus: "implementer_warn"
|
|
592
|
+
},
|
|
593
|
+
source: "implementer",
|
|
594
|
+
reason: "implementer_warn"
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return effective;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Overlay runtime implementer/review signals on top of planned task groups.
|
|
603
|
+
*
|
|
604
|
+
* @param {TaskGroupPlan[]} plannedTaskGroups
|
|
605
|
+
* @param {Array<object>} signals
|
|
606
|
+
* @param {Array<object>} seedTaskGroups
|
|
607
|
+
* @param {Array<object> | null} decisionTraceRecords
|
|
608
|
+
* @param {Function | null} recordWorkflowDecision
|
|
609
|
+
* @returns {TaskGroupRuntimeState[]}
|
|
610
|
+
*/
|
|
611
|
+
function deriveTaskGroupRuntimeState(
|
|
612
|
+
plannedTaskGroups,
|
|
613
|
+
signals,
|
|
614
|
+
seedTaskGroups,
|
|
615
|
+
decisionTraceRecords,
|
|
616
|
+
recordWorkflowDecision
|
|
617
|
+
) {
|
|
618
|
+
const plannedGroups = Array.isArray(plannedTaskGroups) ? plannedTaskGroups : [];
|
|
619
|
+
const seedMap = normalizeTaskGroupSeedMap(seedTaskGroups);
|
|
620
|
+
|
|
621
|
+
return plannedGroups.map((plannedGroup, index) => {
|
|
622
|
+
const taskGroupId = normalizeTaskGroupId(plannedGroup.taskGroupId || plannedGroup.id);
|
|
623
|
+
const seed = seedMap.get(taskGroupId) || {};
|
|
624
|
+
const planned = {
|
|
625
|
+
status: plannedGroup.status,
|
|
626
|
+
completion: plannedGroup.completion,
|
|
627
|
+
checkpointOutcome: plannedGroup.checkpointOutcome,
|
|
628
|
+
evidence: Array.isArray(plannedGroup.evidence) ? plannedGroup.evidence : [],
|
|
629
|
+
nextAction: plannedGroup.nextAction,
|
|
630
|
+
resumeCursor: normalizeResumeCursor(plannedGroup.resumeCursor, index)
|
|
631
|
+
};
|
|
632
|
+
const implementer = buildTaskGroupImplementerState(taskGroupId, signals, seed.implementer);
|
|
633
|
+
const review = buildTaskGroupReviewState(plannedGroup, signals, seed.review);
|
|
634
|
+
const effective = buildEffectiveTaskGroupState(plannedGroup, planned, implementer, review);
|
|
635
|
+
if (TRACEABLE_TASK_GROUP_FOCUS_REASONS.has(effective.reason)) {
|
|
636
|
+
recordDecision(recordWorkflowDecision, decisionTraceRecords, {
|
|
637
|
+
decisionFamily: "task_group_focus_resolution",
|
|
638
|
+
decisionKey: effective.reason,
|
|
639
|
+
outcome: "selected_focus",
|
|
640
|
+
reasonSummary: buildTaskGroupFocusReasonSummary(taskGroupId, effective.reason),
|
|
641
|
+
context: {
|
|
642
|
+
taskGroupId,
|
|
643
|
+
plannedStatus: planned.status || null,
|
|
644
|
+
effectiveStatus: effective.status || null,
|
|
645
|
+
liveFocus: effective.resumeCursor ? effective.resumeCursor.liveFocus || null : null,
|
|
646
|
+
nextAction: effective.nextAction || null
|
|
647
|
+
},
|
|
648
|
+
evidenceRefs: buildTaskGroupFocusEvidenceRefs(taskGroupId, effective.reason)
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return {
|
|
653
|
+
taskGroupId,
|
|
654
|
+
title: plannedGroup.title,
|
|
655
|
+
status: effective.status,
|
|
656
|
+
completion: planned.completion,
|
|
657
|
+
checkpointOutcome: planned.checkpointOutcome,
|
|
658
|
+
evidence: planned.evidence,
|
|
659
|
+
nextAction: effective.nextAction,
|
|
660
|
+
targetFiles: Array.isArray(plannedGroup.targetFiles) ? plannedGroup.targetFiles : [],
|
|
661
|
+
fileReferences: Array.isArray(plannedGroup.fileReferences) ? plannedGroup.fileReferences : [],
|
|
662
|
+
verificationActions: Array.isArray(plannedGroup.verificationActions)
|
|
663
|
+
? plannedGroup.verificationActions
|
|
664
|
+
: [],
|
|
665
|
+
verificationCommands: Array.isArray(plannedGroup.verificationCommands)
|
|
666
|
+
? plannedGroup.verificationCommands
|
|
667
|
+
: [],
|
|
668
|
+
executionIntent: Array.isArray(plannedGroup.executionIntent) ? plannedGroup.executionIntent : [],
|
|
669
|
+
reviewIntent: plannedGroup.reviewIntent === true,
|
|
670
|
+
testingIntent: plannedGroup.testingIntent === true,
|
|
671
|
+
codeChangeLikely: plannedGroup.codeChangeLikely === true,
|
|
672
|
+
resumeCursor: effective.resumeCursor,
|
|
673
|
+
planned,
|
|
674
|
+
implementer,
|
|
675
|
+
review,
|
|
676
|
+
effective
|
|
677
|
+
};
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Build the canonical persisted task-group metadata payload written next to workflow snapshots.
|
|
683
|
+
*
|
|
684
|
+
* @param {string} changeId
|
|
685
|
+
* @param {Object.<string, string>} checkpointStatuses
|
|
686
|
+
* @param {Array<object>} taskGroups
|
|
687
|
+
* @returns {{ version: number, changeId: string, checkpointOutcome: string, taskGroups: Array<object>, updatedAt: string }}
|
|
688
|
+
*/
|
|
689
|
+
function buildTaskGroupMetadataPayload(changeId, checkpointStatuses, taskGroups) {
|
|
690
|
+
return {
|
|
691
|
+
version: 2,
|
|
692
|
+
changeId,
|
|
693
|
+
checkpointOutcome: checkpointStatuses[CHECKPOINT_LABELS.TASK] || STATUS.WARN,
|
|
694
|
+
taskGroups: Array.isArray(taskGroups) ? taskGroups : [],
|
|
695
|
+
updatedAt: new Date().toISOString()
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function loadTaskGroupMetadataFromPath(targetPath) {
|
|
700
|
+
if (!targetPath || !pathExists(targetPath)) {
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
return JSON.parse(readTextIfExists(targetPath));
|
|
705
|
+
} catch (_error) {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Resolve the task-group seed used when starting from a trusted persisted workflow snapshot.
|
|
712
|
+
*
|
|
713
|
+
* @param {string} projectRoot
|
|
714
|
+
* @param {string} changeId
|
|
715
|
+
* @param {object} persistedRecord
|
|
716
|
+
* @param {TaskGroupPlan[]} plannedTaskGroups
|
|
717
|
+
* @param {Array<object> | null} decisionTraceRecords
|
|
718
|
+
* @param {Function | null} recordWorkflowDecision
|
|
719
|
+
* @returns {TaskGroupSeedResolution}
|
|
720
|
+
*/
|
|
721
|
+
function resolvePersistedTaskGroupSeed(
|
|
722
|
+
projectRoot,
|
|
723
|
+
changeId,
|
|
724
|
+
persistedRecord,
|
|
725
|
+
plannedTaskGroups,
|
|
726
|
+
decisionTraceRecords,
|
|
727
|
+
recordWorkflowDecision
|
|
728
|
+
) {
|
|
729
|
+
const metadataRefs =
|
|
730
|
+
persistedRecord && persistedRecord.metadataRefs && typeof persistedRecord.metadataRefs === "object"
|
|
731
|
+
? persistedRecord.metadataRefs
|
|
732
|
+
: {};
|
|
733
|
+
const canonicalPath =
|
|
734
|
+
metadataRefs.taskGroupsPath || resolveTaskGroupMetadataPath(projectRoot, changeId);
|
|
735
|
+
const notes = [];
|
|
736
|
+
|
|
737
|
+
if (canonicalPath && pathExists(canonicalPath)) {
|
|
738
|
+
const actualDigest = digestForPath(canonicalPath);
|
|
739
|
+
const expectedDigest = metadataRefs.taskGroupsDigest || null;
|
|
740
|
+
if (expectedDigest && actualDigest && expectedDigest !== actualDigest) {
|
|
741
|
+
const message =
|
|
742
|
+
"Canonical task-group runtime state digest mismatch; rebuilding task-group state from artifacts.";
|
|
743
|
+
notes.push(message);
|
|
744
|
+
recordDecision(recordWorkflowDecision, decisionTraceRecords, {
|
|
745
|
+
decisionFamily: "task_group_seed_fallback",
|
|
746
|
+
decisionKey: TASK_GROUP_SEED_TRACE_KEYS["digest-mismatch"],
|
|
747
|
+
outcome: "fallback",
|
|
748
|
+
reasonSummary: message,
|
|
749
|
+
context: {
|
|
750
|
+
metadataPath: formatPathRef(projectRoot, canonicalPath),
|
|
751
|
+
taskGroupCount: Array.isArray(plannedTaskGroups) ? plannedTaskGroups.length : 0,
|
|
752
|
+
expectedDigestPresent: true
|
|
753
|
+
},
|
|
754
|
+
evidenceRefs: [`state:${formatPathRef(projectRoot, canonicalPath)}`]
|
|
755
|
+
});
|
|
756
|
+
return {
|
|
757
|
+
taskGroups: plannedTaskGroups,
|
|
758
|
+
notes
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const loaded =
|
|
763
|
+
canonicalPath === resolveTaskGroupMetadataPath(projectRoot, changeId)
|
|
764
|
+
? readTaskGroupMetadata(projectRoot, changeId)
|
|
765
|
+
: loadTaskGroupMetadataFromPath(canonicalPath);
|
|
766
|
+
if (loaded && Array.isArray(loaded.taskGroups)) {
|
|
767
|
+
return {
|
|
768
|
+
taskGroups: loaded.taskGroups,
|
|
769
|
+
notes
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
{
|
|
773
|
+
const message =
|
|
774
|
+
"Canonical task-group runtime state is unreadable; rebuilding task-group state from artifacts.";
|
|
775
|
+
notes.push(message);
|
|
776
|
+
recordDecision(recordWorkflowDecision, decisionTraceRecords, {
|
|
777
|
+
decisionFamily: "task_group_seed_fallback",
|
|
778
|
+
decisionKey: TASK_GROUP_SEED_TRACE_KEYS.unreadable,
|
|
779
|
+
outcome: "fallback",
|
|
780
|
+
reasonSummary: message,
|
|
781
|
+
context: {
|
|
782
|
+
metadataPath: formatPathRef(projectRoot, canonicalPath),
|
|
783
|
+
taskGroupCount: Array.isArray(plannedTaskGroups) ? plannedTaskGroups.length : 0
|
|
784
|
+
},
|
|
785
|
+
evidenceRefs: [`state:${formatPathRef(projectRoot, canonicalPath)}`]
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
return {
|
|
789
|
+
taskGroups: plannedTaskGroups,
|
|
790
|
+
notes
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (Array.isArray(persistedRecord && persistedRecord.taskGroups) && persistedRecord.taskGroups.length > 0) {
|
|
795
|
+
{
|
|
796
|
+
const message = "Using legacy embedded task-group state as migration fallback.";
|
|
797
|
+
notes.push(message);
|
|
798
|
+
recordDecision(recordWorkflowDecision, decisionTraceRecords, {
|
|
799
|
+
decisionFamily: "task_group_seed_fallback",
|
|
800
|
+
decisionKey: TASK_GROUP_SEED_TRACE_KEYS.legacy,
|
|
801
|
+
outcome: "fallback",
|
|
802
|
+
reasonSummary: message,
|
|
803
|
+
context: {
|
|
804
|
+
metadataPath: canonicalPath ? formatPathRef(projectRoot, canonicalPath) : null,
|
|
805
|
+
taskGroupCount: persistedRecord.taskGroups.length
|
|
806
|
+
},
|
|
807
|
+
evidenceRefs: [`state:${formatPathRef(projectRoot, resolveWorkflowStatePath(projectRoot))}`]
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
taskGroups: persistedRecord.taskGroups,
|
|
812
|
+
notes
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
{
|
|
817
|
+
const message = "Canonical task-group runtime state is missing; rebuilding task-group state from artifacts.";
|
|
818
|
+
notes.push(message);
|
|
819
|
+
recordDecision(recordWorkflowDecision, decisionTraceRecords, {
|
|
820
|
+
decisionFamily: "task_group_seed_fallback",
|
|
821
|
+
decisionKey: TASK_GROUP_SEED_TRACE_KEYS.missing,
|
|
822
|
+
outcome: "fallback",
|
|
823
|
+
reasonSummary: message,
|
|
824
|
+
context: {
|
|
825
|
+
metadataPath: canonicalPath ? formatPathRef(projectRoot, canonicalPath) : null,
|
|
826
|
+
taskGroupCount: Array.isArray(plannedTaskGroups) ? plannedTaskGroups.length : 0
|
|
827
|
+
},
|
|
828
|
+
evidenceRefs:
|
|
829
|
+
canonicalPath && String(canonicalPath).trim()
|
|
830
|
+
? [`state:${formatPathRef(projectRoot, canonicalPath)}`]
|
|
831
|
+
: []
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
taskGroups: plannedTaskGroups,
|
|
836
|
+
notes
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Select the highest-priority task-group focus for next-step reporting.
|
|
842
|
+
*
|
|
843
|
+
* @param {Array<{ status?: string, resumeCursor?: { groupIndex?: number } }>} taskGroups
|
|
844
|
+
* @returns {TaskGroupRuntimeState | object | null}
|
|
845
|
+
*/
|
|
846
|
+
function selectFocusedTaskGroup(taskGroups) {
|
|
847
|
+
const priority = {
|
|
848
|
+
blocked: 0,
|
|
849
|
+
review_pending: 1,
|
|
850
|
+
in_progress: 2,
|
|
851
|
+
pending: 3,
|
|
852
|
+
completed: 4
|
|
853
|
+
};
|
|
854
|
+
const groups = Array.isArray(taskGroups) ? taskGroups : [];
|
|
855
|
+
return groups
|
|
856
|
+
.slice()
|
|
857
|
+
.sort((left, right) => {
|
|
858
|
+
const leftRank = Object.prototype.hasOwnProperty.call(priority, left.status) ? priority[left.status] : 5;
|
|
859
|
+
const rightRank = Object.prototype.hasOwnProperty.call(priority, right.status) ? priority[right.status] : 5;
|
|
860
|
+
if (leftRank !== rightRank) {
|
|
861
|
+
return leftRank - rightRank;
|
|
862
|
+
}
|
|
863
|
+
const leftIndex =
|
|
864
|
+
left && left.resumeCursor && Number.isInteger(left.resumeCursor.groupIndex)
|
|
865
|
+
? left.resumeCursor.groupIndex
|
|
866
|
+
: Number.MAX_SAFE_INTEGER;
|
|
867
|
+
const rightIndex =
|
|
868
|
+
right && right.resumeCursor && Number.isInteger(right.resumeCursor.groupIndex)
|
|
869
|
+
? right.resumeCursor.groupIndex
|
|
870
|
+
: Number.MAX_SAFE_INTEGER;
|
|
871
|
+
return leftIndex - rightIndex;
|
|
872
|
+
})[0] || null;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
module.exports = {
|
|
876
|
+
deriveTaskGroupMetadata,
|
|
877
|
+
deriveTaskGroupRuntimeState,
|
|
878
|
+
buildTaskGroupMetadataPayload,
|
|
879
|
+
resolvePersistedTaskGroupSeed,
|
|
880
|
+
selectFocusedTaskGroup
|
|
881
|
+
};
|