@xenonbyte/da-vinci-workflow 0.1.26 → 0.2.2
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 +28 -65
- package/README.zh-CN.md +28 -65
- package/bin/da-vinci-tui.js +8 -0
- package/commands/claude/dv/continue.md +5 -0
- package/commands/codex/prompts/dv-continue.md +6 -1
- package/commands/gemini/dv/continue.toml +5 -0
- package/commands/templates/dv-continue.shared.md +33 -0
- package/docs/dv-command-reference.md +35 -0
- package/docs/execution-chain-migration.md +46 -0
- package/docs/execution-chain-plan.md +125 -0
- package/docs/prompt-entrypoints.md +8 -0
- package/docs/skill-usage.md +217 -0
- package/docs/workflow-examples.md +10 -0
- package/docs/workflow-overview.md +26 -0
- package/docs/zh-CN/dv-command-reference.md +35 -0
- package/docs/zh-CN/execution-chain-migration.md +46 -0
- package/docs/zh-CN/prompt-entrypoints.md +8 -0
- package/docs/zh-CN/skill-usage.md +217 -0
- package/docs/zh-CN/workflow-examples.md +10 -0
- package/docs/zh-CN/workflow-overview.md +26 -0
- package/lib/artifact-parsers.js +120 -0
- package/lib/audit.js +61 -0
- package/lib/cli.js +351 -13
- package/lib/diff-spec.js +242 -0
- package/lib/execution-signals.js +136 -0
- package/lib/lint-bindings.js +143 -0
- package/lib/lint-spec.js +408 -0
- package/lib/lint-tasks.js +176 -0
- package/lib/planning-parsers.js +567 -0
- package/lib/scaffold.js +193 -0
- package/lib/scope-check.js +603 -0
- package/lib/sidecars.js +369 -0
- package/lib/supervisor-review.js +28 -3
- package/lib/utils.js +10 -2
- package/lib/verify.js +652 -0
- package/lib/workflow-contract.js +107 -0
- package/lib/workflow-persisted-state.js +297 -0
- package/lib/workflow-state.js +785 -0
- package/package.json +13 -3
- package/references/artifact-templates.md +26 -0
- package/references/checkpoints.md +14 -0
- package/references/modes.md +10 -0
- package/tui/catalog.js +1190 -0
- package/tui/index.js +727 -0
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const { auditProject } = require("./audit");
|
|
3
|
+
const { parseCheckpointStatusMap, normalizeCheckpointLabel } = require("./audit-parsers");
|
|
4
|
+
const { pathExists, readTextIfExists } = require("./utils");
|
|
5
|
+
const {
|
|
6
|
+
parseTasksArtifact,
|
|
7
|
+
listImmediateDirs,
|
|
8
|
+
hasAnyFile,
|
|
9
|
+
pickLatestChange,
|
|
10
|
+
detectSpecFiles
|
|
11
|
+
} = require("./planning-parsers");
|
|
12
|
+
const {
|
|
13
|
+
STATUS,
|
|
14
|
+
CHECKPOINT_LABELS,
|
|
15
|
+
HANDOFF_GATES,
|
|
16
|
+
getStageById
|
|
17
|
+
} = require("./workflow-contract");
|
|
18
|
+
const {
|
|
19
|
+
selectPersistedStateForChange,
|
|
20
|
+
persistDerivedWorkflowResult,
|
|
21
|
+
writeTaskGroupMetadata,
|
|
22
|
+
readTaskGroupMetadata,
|
|
23
|
+
sanitizePersistedNotes
|
|
24
|
+
} = require("./workflow-persisted-state");
|
|
25
|
+
const { readExecutionSignals, summarizeSignalsBySurface } = require("./execution-signals");
|
|
26
|
+
|
|
27
|
+
const MAX_REPORTED_MESSAGES = 3;
|
|
28
|
+
|
|
29
|
+
function summarizeAudit(result) {
|
|
30
|
+
if (!result) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
status: result.status,
|
|
36
|
+
failures: Array.isArray(result.failures) ? result.failures.slice(0, MAX_REPORTED_MESSAGES) : [],
|
|
37
|
+
warnings: Array.isArray(result.warnings) ? result.warnings.slice(0, MAX_REPORTED_MESSAGES) : [],
|
|
38
|
+
notes: Array.isArray(result.notes) ? result.notes.slice(0, MAX_REPORTED_MESSAGES) : []
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function dedupeMessages(items) {
|
|
43
|
+
return Array.from(new Set((items || []).filter(Boolean)));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function dedupeFindings(findings) {
|
|
47
|
+
findings.blockers = dedupeMessages(findings.blockers);
|
|
48
|
+
findings.warnings = dedupeMessages(findings.warnings);
|
|
49
|
+
findings.notes = dedupeMessages(findings.notes);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function collectIntegrityAudit(projectRoot, workflowRoot, signalSummary) {
|
|
53
|
+
if (!pathExists(workflowRoot)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return summarizeAudit(
|
|
57
|
+
auditProject(projectRoot, {
|
|
58
|
+
mode: "integrity",
|
|
59
|
+
preloadedSignalSummary: signalSummary
|
|
60
|
+
})
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function collectCompletionAudit(projectRoot, workflowRoot, changeId, signalSummary) {
|
|
65
|
+
if (!changeId || !pathExists(workflowRoot)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return summarizeAudit(
|
|
69
|
+
auditProject(projectRoot, {
|
|
70
|
+
mode: "completion",
|
|
71
|
+
changeId,
|
|
72
|
+
preloadedSignalSummary: signalSummary
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function applyAuditFindings(stageId, findings, integrityAudit, completionAudit) {
|
|
78
|
+
let nextStageId = stageId;
|
|
79
|
+
|
|
80
|
+
if (integrityAudit && integrityAudit.status === "FAIL") {
|
|
81
|
+
findings.warnings.push("Integrity audit currently reports FAIL.");
|
|
82
|
+
} else if (integrityAudit && integrityAudit.status === "WARN") {
|
|
83
|
+
findings.warnings.push("Integrity audit currently reports WARN.");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if ((nextStageId === "verify" || nextStageId === "complete") && completionAudit) {
|
|
87
|
+
if (completionAudit.status === "PASS") {
|
|
88
|
+
if (nextStageId === "verify") {
|
|
89
|
+
nextStageId = "complete";
|
|
90
|
+
}
|
|
91
|
+
findings.notes.push("Completion audit reports PASS for the active change.");
|
|
92
|
+
} else if (completionAudit.status === "WARN") {
|
|
93
|
+
findings.warnings.push("Completion audit currently reports WARN.");
|
|
94
|
+
if (nextStageId === "complete") {
|
|
95
|
+
nextStageId = "verify";
|
|
96
|
+
}
|
|
97
|
+
} else if (completionAudit.status === "FAIL") {
|
|
98
|
+
findings.blockers.push("Completion audit currently reports FAIL.");
|
|
99
|
+
if (nextStageId === "complete") {
|
|
100
|
+
nextStageId = "verify";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return nextStageId;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function applyExecutionSignalFindings(stageId, findings, signalSummary) {
|
|
109
|
+
let nextStageId = stageId;
|
|
110
|
+
const signalParseIssue = signalSummary["signal-file-parse"];
|
|
111
|
+
if (signalParseIssue) {
|
|
112
|
+
const warningText =
|
|
113
|
+
Array.isArray(signalParseIssue.warnings) && signalParseIssue.warnings[0]
|
|
114
|
+
? String(signalParseIssue.warnings[0])
|
|
115
|
+
: "Malformed execution signal files were detected.";
|
|
116
|
+
findings.warnings.push(warningText);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const diffSignal = signalSummary["diff-spec"];
|
|
120
|
+
if (diffSignal && diffSignal.status && diffSignal.status !== STATUS.PASS) {
|
|
121
|
+
findings.warnings.push(`Planning diff signal diff-spec reports ${diffSignal.status}.`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const verificationSignal = signalSummary["verify-coverage"];
|
|
125
|
+
if (verificationSignal && verificationSignal.status === STATUS.BLOCK) {
|
|
126
|
+
findings.blockers.push("verify-coverage signal is BLOCK.");
|
|
127
|
+
if (nextStageId === "complete") {
|
|
128
|
+
nextStageId = "verify";
|
|
129
|
+
}
|
|
130
|
+
} else if (verificationSignal && verificationSignal.status === STATUS.WARN) {
|
|
131
|
+
findings.warnings.push("verify-coverage signal is WARN.");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return nextStageId;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function buildRouteRecommendation(stageId, context = {}) {
|
|
138
|
+
const projectRoot = context.projectRoot || process.cwd();
|
|
139
|
+
const changeId = context.changeId || "change-001";
|
|
140
|
+
|
|
141
|
+
if (context.ambiguousChangeSelection) {
|
|
142
|
+
return {
|
|
143
|
+
route: "select-change",
|
|
144
|
+
command: `da-vinci workflow-status --project ${projectRoot} --change <change-id>`,
|
|
145
|
+
reason: "Multiple change directories were found. Choose one change id first."
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
switch (stageId) {
|
|
150
|
+
case "bootstrap":
|
|
151
|
+
return {
|
|
152
|
+
route: "bootstrap-project",
|
|
153
|
+
command: `da-vinci bootstrap-project --project ${projectRoot} --change ${changeId}`,
|
|
154
|
+
reason: "Workflow base artifacts are missing."
|
|
155
|
+
};
|
|
156
|
+
case "breakdown":
|
|
157
|
+
return {
|
|
158
|
+
route: "/dv:breakdown",
|
|
159
|
+
command: "/dv:breakdown",
|
|
160
|
+
reason: "Proposal or spec artifacts are incomplete."
|
|
161
|
+
};
|
|
162
|
+
case "design":
|
|
163
|
+
return {
|
|
164
|
+
route: "/dv:design",
|
|
165
|
+
command: "/dv:design",
|
|
166
|
+
reason: "Design artifacts or design checkpoints are not ready."
|
|
167
|
+
};
|
|
168
|
+
case "tasks":
|
|
169
|
+
return {
|
|
170
|
+
route: "/dv:tasks",
|
|
171
|
+
command: "/dv:tasks",
|
|
172
|
+
reason: "Task planning artifacts are missing or blocked."
|
|
173
|
+
};
|
|
174
|
+
case "build":
|
|
175
|
+
return {
|
|
176
|
+
route: "/dv:build",
|
|
177
|
+
command: "/dv:build",
|
|
178
|
+
reason: "Implementation should proceed before verification."
|
|
179
|
+
};
|
|
180
|
+
case "verify":
|
|
181
|
+
return {
|
|
182
|
+
route: "/dv:verify",
|
|
183
|
+
command: "/dv:verify",
|
|
184
|
+
reason: "Verification and completion readiness are not satisfied yet."
|
|
185
|
+
};
|
|
186
|
+
default:
|
|
187
|
+
return {
|
|
188
|
+
route: null,
|
|
189
|
+
command: null,
|
|
190
|
+
reason: "No next route. Workflow appears complete."
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function readCheckpointStatuses(changeDir) {
|
|
196
|
+
const collected = {};
|
|
197
|
+
const files = [
|
|
198
|
+
path.join(changeDir, "design.md"),
|
|
199
|
+
path.join(changeDir, "pencil-design.md"),
|
|
200
|
+
path.join(changeDir, "tasks.md"),
|
|
201
|
+
path.join(changeDir, "verification.md")
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
for (const artifactPath of files) {
|
|
205
|
+
const text = readTextIfExists(artifactPath);
|
|
206
|
+
if (!text) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const statuses = parseCheckpointStatusMap(text);
|
|
210
|
+
for (const [label, value] of Object.entries(statuses)) {
|
|
211
|
+
const normalized = normalizeCheckpointLabel(label);
|
|
212
|
+
if (!normalized) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
collected[normalized] = String(value || "").toUpperCase();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return collected;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function deriveStageFromArtifacts(artifactState, checkpointStatuses, findings) {
|
|
223
|
+
if (!artifactState.workflowRootReady) {
|
|
224
|
+
findings.blockers.push("Missing `.da-vinci/` directory.");
|
|
225
|
+
return "bootstrap";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!artifactState.changeSelected) {
|
|
229
|
+
findings.blockers.push("No active change selected under `.da-vinci/changes/`.");
|
|
230
|
+
return "breakdown";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!artifactState.proposal || artifactState.specFiles.length === 0) {
|
|
234
|
+
if (!artifactState.proposal) {
|
|
235
|
+
findings.blockers.push("Missing `proposal.md` for the active change.");
|
|
236
|
+
}
|
|
237
|
+
if (artifactState.specFiles.length === 0) {
|
|
238
|
+
findings.blockers.push("Missing `specs/*/spec.md` for the active change.");
|
|
239
|
+
}
|
|
240
|
+
return "breakdown";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (
|
|
244
|
+
!artifactState.design ||
|
|
245
|
+
!artifactState.pencilDesign ||
|
|
246
|
+
!artifactState.pencilBindings
|
|
247
|
+
) {
|
|
248
|
+
if (!artifactState.design) {
|
|
249
|
+
findings.blockers.push("Missing `design.md` for the active change.");
|
|
250
|
+
}
|
|
251
|
+
if (!artifactState.pencilDesign) {
|
|
252
|
+
findings.blockers.push("Missing `pencil-design.md` for the active change.");
|
|
253
|
+
}
|
|
254
|
+
if (!artifactState.pencilBindings) {
|
|
255
|
+
findings.blockers.push("Missing `pencil-bindings.md` for the active change.");
|
|
256
|
+
}
|
|
257
|
+
return "design";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const designCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.DESIGN];
|
|
261
|
+
if (designCheckpoint === STATUS.BLOCK) {
|
|
262
|
+
findings.blockers.push("`design checkpoint` is BLOCK.");
|
|
263
|
+
return "design";
|
|
264
|
+
}
|
|
265
|
+
if (designCheckpoint === STATUS.WARN) {
|
|
266
|
+
findings.warnings.push("`design checkpoint` is WARN.");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const mappingCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.MAPPING];
|
|
270
|
+
if (mappingCheckpoint === STATUS.BLOCK) {
|
|
271
|
+
findings.blockers.push("`mapping checkpoint` is BLOCK.");
|
|
272
|
+
return "design";
|
|
273
|
+
}
|
|
274
|
+
if (mappingCheckpoint === STATUS.WARN) {
|
|
275
|
+
findings.warnings.push("`mapping checkpoint` is WARN.");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!artifactState.tasks) {
|
|
279
|
+
findings.blockers.push("Missing `tasks.md` for the active change.");
|
|
280
|
+
return "tasks";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const taskCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.TASK];
|
|
284
|
+
if (taskCheckpoint === STATUS.BLOCK) {
|
|
285
|
+
findings.blockers.push("`task checkpoint` is BLOCK.");
|
|
286
|
+
return "tasks";
|
|
287
|
+
}
|
|
288
|
+
if (taskCheckpoint === STATUS.WARN) {
|
|
289
|
+
findings.warnings.push("`task checkpoint` is WARN.");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!artifactState.verification) {
|
|
293
|
+
findings.blockers.push("Missing `verification.md` for the active change.");
|
|
294
|
+
return "build";
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return "verify";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function deriveTaskGroupMetadata(tasksMarkdownText, checkpointStatuses) {
|
|
301
|
+
if (!tasksMarkdownText) {
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const taskArtifact = parseTasksArtifact(tasksMarkdownText);
|
|
306
|
+
const lines = String(tasksMarkdownText || "").replace(/\r\n?/g, "\n").split("\n");
|
|
307
|
+
const sections = [];
|
|
308
|
+
let current = null;
|
|
309
|
+
for (const line of lines) {
|
|
310
|
+
const headingMatch = line.match(/^\s{0,3}##\s+(\d+(?:\.\d+)*)\.\s+(.+)$/);
|
|
311
|
+
if (headingMatch) {
|
|
312
|
+
if (current) {
|
|
313
|
+
sections.push(current);
|
|
314
|
+
}
|
|
315
|
+
current = {
|
|
316
|
+
id: headingMatch[1],
|
|
317
|
+
title: String(headingMatch[2] || "").trim(),
|
|
318
|
+
checklistItems: []
|
|
319
|
+
};
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (!current) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const checklistMatch = line.match(/^\s*-\s*\[([ xX])\]\s+(.+)$/);
|
|
326
|
+
if (checklistMatch) {
|
|
327
|
+
current.checklistItems.push({
|
|
328
|
+
checked: String(checklistMatch[1] || "").toLowerCase() === "x",
|
|
329
|
+
text: String(checklistMatch[2] || "").trim()
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (current) {
|
|
334
|
+
sections.push(current);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const normalizedTaskCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.TASK] || STATUS.WARN;
|
|
338
|
+
const metadata = sections.map((section, index) => {
|
|
339
|
+
const total = section.checklistItems.length;
|
|
340
|
+
const done = section.checklistItems.filter((item) => item.checked).length;
|
|
341
|
+
const completion = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
342
|
+
const sectionStatus =
|
|
343
|
+
total === 0 ? "pending" : done === 0 ? "pending" : done < total ? "in_progress" : "completed";
|
|
344
|
+
const nextAction =
|
|
345
|
+
sectionStatus === "completed"
|
|
346
|
+
? "advance to next task group"
|
|
347
|
+
: section.checklistItems.find((item) => !item.checked)?.text || "continue group work";
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
taskGroupId: section.id,
|
|
351
|
+
title: section.title,
|
|
352
|
+
status: sectionStatus,
|
|
353
|
+
completion,
|
|
354
|
+
checkpointOutcome: normalizedTaskCheckpoint,
|
|
355
|
+
evidence: section.checklistItems.filter((item) => item.checked).map((item) => item.text),
|
|
356
|
+
nextAction,
|
|
357
|
+
resumeCursor: {
|
|
358
|
+
groupIndex: index,
|
|
359
|
+
nextUncheckedItem:
|
|
360
|
+
section.checklistItems.find((item) => !item.checked)?.text ||
|
|
361
|
+
section.checklistItems[section.checklistItems.length - 1]?.text ||
|
|
362
|
+
null
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
if (metadata.length === 0 && taskArtifact.taskGroups.length > 0) {
|
|
368
|
+
return taskArtifact.taskGroups.map((group, index) => ({
|
|
369
|
+
taskGroupId: group.id,
|
|
370
|
+
title: group.title,
|
|
371
|
+
status: "pending",
|
|
372
|
+
completion: 0,
|
|
373
|
+
checkpointOutcome: normalizedTaskCheckpoint,
|
|
374
|
+
evidence: [],
|
|
375
|
+
nextAction: "start task group",
|
|
376
|
+
resumeCursor: {
|
|
377
|
+
groupIndex: index,
|
|
378
|
+
nextUncheckedItem: null
|
|
379
|
+
}
|
|
380
|
+
}));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return metadata;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function statusFromFindings(findings) {
|
|
387
|
+
if (findings.blockers.length > 0) {
|
|
388
|
+
return STATUS.BLOCK;
|
|
389
|
+
}
|
|
390
|
+
if (findings.warnings.length > 0) {
|
|
391
|
+
return STATUS.WARN;
|
|
392
|
+
}
|
|
393
|
+
return STATUS.PASS;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function deriveWorkflowStatus(projectPathInput, options = {}) {
|
|
397
|
+
const projectRoot = path.resolve(projectPathInput || process.cwd());
|
|
398
|
+
const requestedChangeId = options.changeId ? String(options.changeId).trim() : "";
|
|
399
|
+
const findings = {
|
|
400
|
+
blockers: [],
|
|
401
|
+
warnings: [],
|
|
402
|
+
notes: []
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
if (!pathExists(projectRoot)) {
|
|
406
|
+
findings.blockers.push(`Project path does not exist: ${projectRoot}`);
|
|
407
|
+
const stageId = "bootstrap";
|
|
408
|
+
return buildWorkflowResult({
|
|
409
|
+
projectRoot,
|
|
410
|
+
changeId: null,
|
|
411
|
+
stageId,
|
|
412
|
+
findings,
|
|
413
|
+
checkpoints: {},
|
|
414
|
+
gates: {},
|
|
415
|
+
audits: {
|
|
416
|
+
integrity: null,
|
|
417
|
+
completion: null
|
|
418
|
+
},
|
|
419
|
+
routeContext: {
|
|
420
|
+
projectRoot,
|
|
421
|
+
changeId: requestedChangeId || "change-001",
|
|
422
|
+
ambiguousChangeSelection: false
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const workflowRoot = path.join(projectRoot, ".da-vinci");
|
|
428
|
+
const changesRoot = path.join(workflowRoot, "changes");
|
|
429
|
+
const changeDirs = listImmediateDirs(changesRoot).filter((changeDir) => hasAnyFile(changeDir));
|
|
430
|
+
const changeIds = changeDirs.map((changeDir) => path.basename(changeDir)).sort();
|
|
431
|
+
|
|
432
|
+
let activeChangeDir = null;
|
|
433
|
+
let ambiguousChangeSelection = false;
|
|
434
|
+
if (!pathExists(workflowRoot)) {
|
|
435
|
+
findings.blockers.push("Missing `.da-vinci/` directory.");
|
|
436
|
+
} else if (requestedChangeId) {
|
|
437
|
+
activeChangeDir = path.join(changesRoot, requestedChangeId);
|
|
438
|
+
if (!pathExists(activeChangeDir) || !hasAnyFile(activeChangeDir)) {
|
|
439
|
+
findings.blockers.push(
|
|
440
|
+
`Requested change was not found or is empty: .da-vinci/changes/${requestedChangeId}`
|
|
441
|
+
);
|
|
442
|
+
activeChangeDir = null;
|
|
443
|
+
}
|
|
444
|
+
} else if (changeDirs.length === 1) {
|
|
445
|
+
activeChangeDir = changeDirs[0];
|
|
446
|
+
} else if (changeDirs.length > 1) {
|
|
447
|
+
ambiguousChangeSelection = true;
|
|
448
|
+
findings.blockers.push(
|
|
449
|
+
"Multiple non-empty change directories found. Re-run with `--change <change-id>`."
|
|
450
|
+
);
|
|
451
|
+
findings.notes.push(`Available change ids: ${changeIds.join(", ")}`);
|
|
452
|
+
activeChangeDir = pickLatestChange(changeDirs);
|
|
453
|
+
if (activeChangeDir) {
|
|
454
|
+
findings.notes.push(
|
|
455
|
+
`Latest inferred change for context only: ${path.basename(activeChangeDir)}`
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
} else {
|
|
459
|
+
findings.blockers.push("No non-empty change directory found under `.da-vinci/changes/`.");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const activeChangeId = activeChangeDir ? path.basename(activeChangeDir) : null;
|
|
463
|
+
const artifactState = {
|
|
464
|
+
workflowRootReady: pathExists(workflowRoot),
|
|
465
|
+
changeSelected: Boolean(activeChangeDir),
|
|
466
|
+
proposal: activeChangeDir ? pathExists(path.join(activeChangeDir, "proposal.md")) : false,
|
|
467
|
+
specFiles: activeChangeDir ? detectSpecFiles(path.join(activeChangeDir, "specs")) : [],
|
|
468
|
+
design: activeChangeDir ? pathExists(path.join(activeChangeDir, "design.md")) : false,
|
|
469
|
+
pencilDesign: activeChangeDir ? pathExists(path.join(activeChangeDir, "pencil-design.md")) : false,
|
|
470
|
+
pencilBindings: activeChangeDir ? pathExists(path.join(activeChangeDir, "pencil-bindings.md")) : false,
|
|
471
|
+
tasks: activeChangeDir ? pathExists(path.join(activeChangeDir, "tasks.md")) : false,
|
|
472
|
+
verification: activeChangeDir ? pathExists(path.join(activeChangeDir, "verification.md")) : false
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const tasksArtifactText = activeChangeDir ? readTextIfExists(path.join(activeChangeDir, "tasks.md")) : "";
|
|
476
|
+
const checkpointStatuses = activeChangeDir ? readCheckpointStatuses(activeChangeDir) : {};
|
|
477
|
+
const signalSummary = activeChangeId
|
|
478
|
+
? summarizeSignalsBySurface(
|
|
479
|
+
readExecutionSignals(projectRoot, {
|
|
480
|
+
changeId: activeChangeId
|
|
481
|
+
})
|
|
482
|
+
)
|
|
483
|
+
: {};
|
|
484
|
+
const integrityAudit = collectIntegrityAudit(projectRoot, workflowRoot, signalSummary);
|
|
485
|
+
|
|
486
|
+
if (activeChangeId) {
|
|
487
|
+
const persistedSelection = selectPersistedStateForChange(projectRoot, activeChangeId, {
|
|
488
|
+
staleWindowMs: options.staleWindowMs
|
|
489
|
+
});
|
|
490
|
+
if (persistedSelection.usable && persistedSelection.changeRecord) {
|
|
491
|
+
const persistedRecord = persistedSelection.changeRecord;
|
|
492
|
+
const stageRecord = getStageById(persistedRecord.stage) || getStageById("bootstrap");
|
|
493
|
+
const persistedTaskMetadata = readTaskGroupMetadata(projectRoot, activeChangeId);
|
|
494
|
+
const persistedFindings = {
|
|
495
|
+
blockers: Array.isArray(persistedRecord.failures) ? persistedRecord.failures.slice() : [],
|
|
496
|
+
warnings: Array.isArray(persistedRecord.warnings) ? persistedRecord.warnings.slice() : [],
|
|
497
|
+
notes: sanitizePersistedNotes(persistedRecord.notes)
|
|
498
|
+
};
|
|
499
|
+
let persistedStageId = stageRecord.id;
|
|
500
|
+
const persistedNeedsCompletionAudit =
|
|
501
|
+
persistedStageId === "verify" || persistedStageId === "complete";
|
|
502
|
+
const completionAudit = persistedNeedsCompletionAudit
|
|
503
|
+
? collectCompletionAudit(projectRoot, workflowRoot, activeChangeId, signalSummary)
|
|
504
|
+
: null;
|
|
505
|
+
persistedStageId = applyAuditFindings(
|
|
506
|
+
persistedStageId,
|
|
507
|
+
persistedFindings,
|
|
508
|
+
integrityAudit,
|
|
509
|
+
completionAudit
|
|
510
|
+
);
|
|
511
|
+
persistedStageId = applyExecutionSignalFindings(
|
|
512
|
+
persistedStageId,
|
|
513
|
+
persistedFindings,
|
|
514
|
+
signalSummary
|
|
515
|
+
);
|
|
516
|
+
persistedFindings.notes.push("workflow-status is using trusted persisted workflow state.");
|
|
517
|
+
dedupeFindings(persistedFindings);
|
|
518
|
+
|
|
519
|
+
const persistedGates =
|
|
520
|
+
persistedRecord && persistedRecord.gates && typeof persistedRecord.gates === "object"
|
|
521
|
+
? { ...persistedRecord.gates }
|
|
522
|
+
: {};
|
|
523
|
+
if (completionAudit) {
|
|
524
|
+
persistedGates[HANDOFF_GATES.VERIFY_TO_COMPLETE] =
|
|
525
|
+
completionAudit.status === "PASS" ? STATUS.PASS : STATUS.WARN;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return buildWorkflowResult({
|
|
529
|
+
projectRoot,
|
|
530
|
+
changeId: activeChangeId,
|
|
531
|
+
stageId: persistedStageId,
|
|
532
|
+
findings: persistedFindings,
|
|
533
|
+
checkpoints: checkpointStatuses,
|
|
534
|
+
gates: persistedGates,
|
|
535
|
+
audits: {
|
|
536
|
+
integrity: integrityAudit,
|
|
537
|
+
completion: completionAudit
|
|
538
|
+
},
|
|
539
|
+
routeContext: {
|
|
540
|
+
projectRoot,
|
|
541
|
+
changeId: activeChangeId,
|
|
542
|
+
ambiguousChangeSelection
|
|
543
|
+
},
|
|
544
|
+
source: "persisted",
|
|
545
|
+
taskGroups:
|
|
546
|
+
persistedTaskMetadata && Array.isArray(persistedTaskMetadata.taskGroups)
|
|
547
|
+
? persistedTaskMetadata.taskGroups
|
|
548
|
+
: Array.isArray(persistedRecord.taskGroups)
|
|
549
|
+
? persistedRecord.taskGroups
|
|
550
|
+
: []
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (!persistedSelection.usable && persistedSelection.reason) {
|
|
555
|
+
const reasonMessage = {
|
|
556
|
+
missing: "No persisted workflow state found for this change; deriving from artifacts.",
|
|
557
|
+
"parse-error": "Persisted workflow state is unreadable; deriving from artifacts.",
|
|
558
|
+
"version-mismatch": "Persisted workflow state version mismatch; deriving from artifacts.",
|
|
559
|
+
"change-missing": "Persisted workflow state has no entry for this change; deriving from artifacts.",
|
|
560
|
+
"invalid-timestamp": "Persisted workflow state timestamp invalid; deriving from artifacts.",
|
|
561
|
+
"time-stale": "Persisted workflow state is stale by time; deriving from artifacts.",
|
|
562
|
+
"fingerprint-mismatch": "Persisted workflow state conflicts with artifact truth; deriving from artifacts."
|
|
563
|
+
};
|
|
564
|
+
const message = reasonMessage[persistedSelection.reason];
|
|
565
|
+
if (message) {
|
|
566
|
+
findings.notes.push(message);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
let stageId = deriveStageFromArtifacts(artifactState, checkpointStatuses, findings);
|
|
572
|
+
const needsCompletionAudit = stageId === "verify" || stageId === "complete";
|
|
573
|
+
const completionAudit = needsCompletionAudit
|
|
574
|
+
? collectCompletionAudit(projectRoot, workflowRoot, activeChangeId, signalSummary)
|
|
575
|
+
: null;
|
|
576
|
+
stageId = applyAuditFindings(stageId, findings, integrityAudit, completionAudit);
|
|
577
|
+
stageId = applyExecutionSignalFindings(stageId, findings, signalSummary);
|
|
578
|
+
|
|
579
|
+
const gates = {
|
|
580
|
+
[HANDOFF_GATES.BREAKDOWN_TO_DESIGN]:
|
|
581
|
+
artifactState.proposal && artifactState.specFiles.length > 0 ? STATUS.PASS : STATUS.BLOCK,
|
|
582
|
+
[HANDOFF_GATES.DESIGN_TO_TASKS]:
|
|
583
|
+
artifactState.design && artifactState.pencilDesign && artifactState.pencilBindings
|
|
584
|
+
? STATUS.PASS
|
|
585
|
+
: STATUS.BLOCK,
|
|
586
|
+
[HANDOFF_GATES.TASKS_TO_BUILD]: artifactState.tasks ? STATUS.PASS : STATUS.BLOCK,
|
|
587
|
+
[HANDOFF_GATES.BUILD_TO_VERIFY]: artifactState.verification ? STATUS.PASS : STATUS.BLOCK,
|
|
588
|
+
[HANDOFF_GATES.VERIFY_TO_COMPLETE]:
|
|
589
|
+
completionAudit && completionAudit.status === "PASS" ? STATUS.PASS : STATUS.WARN
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const derivedTaskGroups = deriveTaskGroupMetadata(tasksArtifactText, checkpointStatuses);
|
|
593
|
+
if (activeChangeId && !ambiguousChangeSelection && derivedTaskGroups.length > 0) {
|
|
594
|
+
const metadataPayload = {
|
|
595
|
+
version: 1,
|
|
596
|
+
changeId: activeChangeId,
|
|
597
|
+
checkpointOutcome: checkpointStatuses[CHECKPOINT_LABELS.TASK] || STATUS.WARN,
|
|
598
|
+
taskGroups: derivedTaskGroups,
|
|
599
|
+
updatedAt: new Date().toISOString()
|
|
600
|
+
};
|
|
601
|
+
const metadataPath = writeTaskGroupMetadata(projectRoot, activeChangeId, metadataPayload);
|
|
602
|
+
if (metadataPath) {
|
|
603
|
+
findings.notes.push(`Task-group metadata refreshed: ${metadataPath}`);
|
|
604
|
+
}
|
|
605
|
+
} else if (activeChangeId && ambiguousChangeSelection && derivedTaskGroups.length > 0) {
|
|
606
|
+
findings.notes.push(
|
|
607
|
+
"Skipped task-group metadata persistence because change selection is ambiguous."
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
dedupeFindings(findings);
|
|
611
|
+
|
|
612
|
+
const derivedResult = buildWorkflowResult({
|
|
613
|
+
projectRoot,
|
|
614
|
+
changeId: activeChangeId,
|
|
615
|
+
stageId,
|
|
616
|
+
findings,
|
|
617
|
+
checkpoints: checkpointStatuses,
|
|
618
|
+
gates,
|
|
619
|
+
audits: {
|
|
620
|
+
integrity: integrityAudit,
|
|
621
|
+
completion: completionAudit
|
|
622
|
+
},
|
|
623
|
+
routeContext: {
|
|
624
|
+
projectRoot,
|
|
625
|
+
changeId: activeChangeId || requestedChangeId || "change-001",
|
|
626
|
+
ambiguousChangeSelection
|
|
627
|
+
},
|
|
628
|
+
taskGroups: derivedTaskGroups
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
if (activeChangeId && !ambiguousChangeSelection) {
|
|
632
|
+
try {
|
|
633
|
+
persistDerivedWorkflowResult(projectRoot, activeChangeId, derivedResult, {
|
|
634
|
+
metadataRefs: {
|
|
635
|
+
taskGroupsPath: activeChangeId
|
|
636
|
+
? path.join(projectRoot, ".da-vinci", "state", "task-groups", `${activeChangeId}.json`)
|
|
637
|
+
: null
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
derivedResult.notes.push("workflow-status persisted a fresh derived workflow snapshot.");
|
|
641
|
+
} catch (error) {
|
|
642
|
+
const message =
|
|
643
|
+
error && error.message
|
|
644
|
+
? error.message
|
|
645
|
+
: "unknown write error";
|
|
646
|
+
derivedResult.warnings = dedupeMessages([
|
|
647
|
+
...derivedResult.warnings,
|
|
648
|
+
`Failed to persist workflow snapshot: ${message}`
|
|
649
|
+
]);
|
|
650
|
+
if (derivedResult.checkpointState === STATUS.PASS) {
|
|
651
|
+
derivedResult.checkpointState = STATUS.WARN;
|
|
652
|
+
}
|
|
653
|
+
if (derivedResult.status === STATUS.PASS) {
|
|
654
|
+
derivedResult.status = STATUS.WARN;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
} else if (activeChangeId && ambiguousChangeSelection) {
|
|
658
|
+
derivedResult.notes = dedupeMessages([
|
|
659
|
+
...derivedResult.notes,
|
|
660
|
+
"Skipped workflow snapshot persistence because change selection is ambiguous."
|
|
661
|
+
]);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return derivedResult;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function buildWorkflowResult(params) {
|
|
668
|
+
const stage = getStageById(params.stageId) || getStageById("bootstrap");
|
|
669
|
+
const nextStep = buildRouteRecommendation(params.stageId, params.routeContext);
|
|
670
|
+
const checkpointState = statusFromFindings(params.findings);
|
|
671
|
+
|
|
672
|
+
return {
|
|
673
|
+
status: checkpointState,
|
|
674
|
+
failures: params.findings.blockers,
|
|
675
|
+
warnings: params.findings.warnings,
|
|
676
|
+
notes: params.findings.notes,
|
|
677
|
+
projectRoot: params.projectRoot,
|
|
678
|
+
changeId: params.changeId || null,
|
|
679
|
+
stage: stage.id,
|
|
680
|
+
stageLabel: stage.label,
|
|
681
|
+
stageOrder: stage.order,
|
|
682
|
+
checkpointState,
|
|
683
|
+
checkpoints: params.checkpoints,
|
|
684
|
+
gates: params.gates,
|
|
685
|
+
audits: params.audits,
|
|
686
|
+
nextStep,
|
|
687
|
+
source: params.source || "derived",
|
|
688
|
+
taskGroups: Array.isArray(params.taskGroups) ? params.taskGroups : []
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function formatWorkflowStatusReport(result) {
|
|
693
|
+
const lines = [
|
|
694
|
+
"Da Vinci workflow-status",
|
|
695
|
+
`Project: ${result.projectRoot}`,
|
|
696
|
+
`Change: ${result.changeId || "(not selected)"}`,
|
|
697
|
+
`Stage: ${result.stageLabel} (${result.stage})`,
|
|
698
|
+
`Checkpoint state: ${result.checkpointState}`
|
|
699
|
+
];
|
|
700
|
+
|
|
701
|
+
if (result.nextStep && result.nextStep.command) {
|
|
702
|
+
lines.push(`Next route: ${result.nextStep.command}`);
|
|
703
|
+
if (result.nextStep.reason) {
|
|
704
|
+
lines.push(`Route reason: ${result.nextStep.reason}`);
|
|
705
|
+
}
|
|
706
|
+
} else {
|
|
707
|
+
lines.push("Next route: none");
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (result.failures.length > 0) {
|
|
711
|
+
lines.push("Blockers:");
|
|
712
|
+
for (const message of result.failures) {
|
|
713
|
+
lines.push(`- ${message}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (result.warnings.length > 0) {
|
|
718
|
+
lines.push("Warnings:");
|
|
719
|
+
for (const message of result.warnings) {
|
|
720
|
+
lines.push(`- ${message}`);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (result.notes.length > 0) {
|
|
725
|
+
lines.push("Notes:");
|
|
726
|
+
for (const message of result.notes) {
|
|
727
|
+
lines.push(`- ${message}`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (result.audits.integrity) {
|
|
732
|
+
lines.push(`Integrity audit: ${result.audits.integrity.status}`);
|
|
733
|
+
}
|
|
734
|
+
if (result.audits.completion) {
|
|
735
|
+
lines.push(`Completion audit: ${result.audits.completion.status}`);
|
|
736
|
+
}
|
|
737
|
+
if (result.source) {
|
|
738
|
+
lines.push(`State source: ${result.source}`);
|
|
739
|
+
}
|
|
740
|
+
if (Array.isArray(result.taskGroups) && result.taskGroups.length > 0) {
|
|
741
|
+
lines.push("Task-group metadata:");
|
|
742
|
+
for (const group of result.taskGroups) {
|
|
743
|
+
lines.push(
|
|
744
|
+
`- ${group.taskGroupId || group.id || "group"}: ${group.status || "unknown"} (${group.completion || 0}%)`
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return lines.join("\n");
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function formatNextStepReport(result) {
|
|
753
|
+
const lines = [
|
|
754
|
+
"Da Vinci next-step",
|
|
755
|
+
`Project: ${result.projectRoot}`,
|
|
756
|
+
`Change: ${result.changeId || "(not selected)"}`,
|
|
757
|
+
`Stage: ${result.stageLabel} (${result.stage})`,
|
|
758
|
+
`Checkpoint state: ${result.checkpointState}`
|
|
759
|
+
];
|
|
760
|
+
|
|
761
|
+
if (!result.nextStep || !result.nextStep.command) {
|
|
762
|
+
lines.push("Next action: no further route is required.");
|
|
763
|
+
return lines.join("\n");
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
lines.push(`Next action: ${result.nextStep.command}`);
|
|
767
|
+
if (result.nextStep.reason) {
|
|
768
|
+
lines.push(`Reason: ${result.nextStep.reason}`);
|
|
769
|
+
}
|
|
770
|
+
if (Array.isArray(result.taskGroups) && result.taskGroups.length > 0) {
|
|
771
|
+
const active =
|
|
772
|
+
result.taskGroups.find((group) => group.status === "in_progress") ||
|
|
773
|
+
result.taskGroups.find((group) => group.status === "pending");
|
|
774
|
+
if (active) {
|
|
775
|
+
lines.push(`Task-group focus: ${active.taskGroupId || active.id} -> ${active.nextAction || "continue"}`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return lines.join("\n");
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
module.exports = {
|
|
782
|
+
deriveWorkflowStatus,
|
|
783
|
+
formatWorkflowStatusReport,
|
|
784
|
+
formatNextStepReport
|
|
785
|
+
};
|