@xenonbyte/da-vinci-workflow 0.2.2 → 0.2.4
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 +28 -0
- package/README.md +49 -14
- package/README.zh-CN.md +169 -14
- package/commands/claude/dv/breakdown.md +8 -0
- package/commands/claude/dv/build.md +16 -0
- package/commands/claude/dv/continue.md +4 -0
- package/commands/claude/dv/design.md +5 -2
- package/commands/claude/dv/tasks.md +14 -0
- package/commands/claude/dv/verify.md +11 -0
- package/commands/codex/prompts/dv-breakdown.md +8 -0
- package/commands/codex/prompts/dv-build.md +16 -0
- package/commands/codex/prompts/dv-continue.md +4 -0
- package/commands/codex/prompts/dv-design.md +5 -2
- package/commands/codex/prompts/dv-tasks.md +14 -0
- package/commands/codex/prompts/dv-verify.md +10 -0
- package/commands/gemini/dv/breakdown.toml +8 -0
- package/commands/gemini/dv/build.toml +16 -0
- package/commands/gemini/dv/continue.toml +4 -0
- package/commands/gemini/dv/design.toml +5 -2
- package/commands/gemini/dv/tasks.toml +14 -0
- package/commands/gemini/dv/verify.toml +10 -0
- package/commands/templates/dv-continue.shared.md +4 -0
- package/docs/discipline-and-orchestration-upgrade.md +83 -0
- package/docs/dv-command-reference.md +61 -2
- package/docs/execution-chain-migration.md +23 -0
- package/docs/execution-chain-plan.md +10 -3
- package/docs/mode-use-cases.md +2 -1
- package/docs/pencil-rendering-workflow.md +15 -12
- package/docs/prompt-entrypoints.md +5 -0
- package/docs/prompt-presets/README.md +1 -1
- package/docs/prompt-presets/desktop-app.md +3 -3
- package/docs/prompt-presets/mobile-app.md +3 -3
- package/docs/prompt-presets/tablet-app.md +3 -3
- package/docs/prompt-presets/web-app.md +3 -3
- package/docs/skill-usage.md +61 -38
- package/docs/workflow-examples.md +16 -13
- package/docs/workflow-overview.md +19 -0
- package/docs/zh-CN/dv-command-reference.md +59 -2
- package/docs/zh-CN/execution-chain-migration.md +23 -0
- package/docs/zh-CN/mode-use-cases.md +2 -1
- package/docs/zh-CN/pencil-rendering-workflow.md +15 -12
- package/docs/zh-CN/prompt-entrypoints.md +5 -0
- package/docs/zh-CN/prompt-presets/README.md +1 -1
- package/docs/zh-CN/prompt-presets/desktop-app.md +3 -3
- package/docs/zh-CN/prompt-presets/mobile-app.md +3 -3
- package/docs/zh-CN/prompt-presets/tablet-app.md +3 -3
- package/docs/zh-CN/prompt-presets/web-app.md +3 -3
- package/docs/zh-CN/skill-usage.md +61 -38
- package/docs/zh-CN/workflow-examples.md +15 -13
- package/docs/zh-CN/workflow-overview.md +19 -0
- package/examples/greenfield-spec-markupflow/.da-vinci/state/execution-signals/demo__lint-tasks.json +16 -0
- package/lib/audit-parsers.js +166 -10
- package/lib/audit.js +3 -26
- package/lib/cli.js +156 -2
- package/lib/design-source-registry.js +146 -0
- package/lib/execution-profile.js +143 -0
- package/lib/execution-signals.js +19 -1
- package/lib/lint-tasks.js +86 -2
- package/lib/planning-parsers.js +255 -18
- package/lib/save-current-design.js +790 -0
- package/lib/supervisor-review.js +3 -2
- package/lib/task-execution.js +160 -0
- package/lib/task-review.js +197 -0
- package/lib/verify.js +152 -1
- package/lib/workflow-bootstrap.js +2 -13
- package/lib/workflow-persisted-state.js +3 -1
- package/lib/workflow-state.js +503 -33
- package/lib/worktree-preflight.js +214 -0
- package/package.json +1 -1
- package/references/artifact-templates.md +56 -6
- package/tui/catalog.js +103 -0
- package/tui/index.js +2274 -418
package/lib/workflow-state.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
1
2
|
const path = require("path");
|
|
2
3
|
const { auditProject } = require("./audit");
|
|
3
|
-
const {
|
|
4
|
+
const {
|
|
5
|
+
parseCheckpointStatusMap,
|
|
6
|
+
normalizeCheckpointLabel,
|
|
7
|
+
parseDisciplineMarkers,
|
|
8
|
+
DISCIPLINE_MARKER_NAMES
|
|
9
|
+
} = require("./audit-parsers");
|
|
4
10
|
const { pathExists, readTextIfExists } = require("./utils");
|
|
5
11
|
const {
|
|
6
12
|
parseTasksArtifact,
|
|
@@ -13,7 +19,8 @@ const {
|
|
|
13
19
|
STATUS,
|
|
14
20
|
CHECKPOINT_LABELS,
|
|
15
21
|
HANDOFF_GATES,
|
|
16
|
-
getStageById
|
|
22
|
+
getStageById,
|
|
23
|
+
mergeStatuses
|
|
17
24
|
} = require("./workflow-contract");
|
|
18
25
|
const {
|
|
19
26
|
selectPersistedStateForChange,
|
|
@@ -22,7 +29,14 @@ const {
|
|
|
22
29
|
readTaskGroupMetadata,
|
|
23
30
|
sanitizePersistedNotes
|
|
24
31
|
} = require("./workflow-persisted-state");
|
|
25
|
-
const {
|
|
32
|
+
const {
|
|
33
|
+
readExecutionSignals,
|
|
34
|
+
summarizeSignalsBySurface,
|
|
35
|
+
listSignalsBySurfacePrefix
|
|
36
|
+
} = require("./execution-signals");
|
|
37
|
+
const { deriveExecutionProfile } = require("./execution-profile");
|
|
38
|
+
const { collectVerificationFreshness } = require("./verify");
|
|
39
|
+
const { runWorktreePreflight } = require("./worktree-preflight");
|
|
26
40
|
|
|
27
41
|
const MAX_REPORTED_MESSAGES = 3;
|
|
28
42
|
|
|
@@ -49,6 +63,206 @@ function dedupeFindings(findings) {
|
|
|
49
63
|
findings.notes = dedupeMessages(findings.notes);
|
|
50
64
|
}
|
|
51
65
|
|
|
66
|
+
function resolveDisciplineGateMode() {
|
|
67
|
+
const strictEnv = String(process.env.DA_VINCI_DISCIPLINE_REQUIRE_APPROVAL || "").trim();
|
|
68
|
+
const strict = strictEnv === "1" || /^true$/i.test(strictEnv);
|
|
69
|
+
return {
|
|
70
|
+
strict
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function statusTokenMatches(status, acceptedTokens) {
|
|
75
|
+
const normalized = String(status || "").trim().toUpperCase();
|
|
76
|
+
return acceptedTokens.includes(normalized);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function inspectDisciplineState(changeDir) {
|
|
80
|
+
const designPath = path.join(changeDir, "design.md");
|
|
81
|
+
const pencilDesignPath = path.join(changeDir, "pencil-design.md");
|
|
82
|
+
const pencilBindingsPath = path.join(changeDir, "pencil-bindings.md");
|
|
83
|
+
const tasksPath = path.join(changeDir, "tasks.md");
|
|
84
|
+
const designText = readTextIfExists(designPath);
|
|
85
|
+
const tasksText = readTextIfExists(tasksPath);
|
|
86
|
+
const designMarkers = parseDisciplineMarkers(designText);
|
|
87
|
+
const taskMarkers = parseDisciplineMarkers(tasksText);
|
|
88
|
+
const gateMode = resolveDisciplineGateMode();
|
|
89
|
+
const hasAnyMarker =
|
|
90
|
+
designMarkers.ordered.length > 0 ||
|
|
91
|
+
taskMarkers.ordered.length > 0 ||
|
|
92
|
+
designMarkers.malformed.length > 0 ||
|
|
93
|
+
taskMarkers.malformed.length > 0;
|
|
94
|
+
|
|
95
|
+
const designApproval =
|
|
96
|
+
designMarkers.markers[DISCIPLINE_MARKER_NAMES.designApproval] ||
|
|
97
|
+
taskMarkers.markers[DISCIPLINE_MARKER_NAMES.designApproval] ||
|
|
98
|
+
null;
|
|
99
|
+
const planSelfReview = taskMarkers.markers[DISCIPLINE_MARKER_NAMES.planSelfReview] || null;
|
|
100
|
+
const operatorReviewAck = taskMarkers.markers[DISCIPLINE_MARKER_NAMES.operatorReviewAck] || null;
|
|
101
|
+
|
|
102
|
+
const blockers = [];
|
|
103
|
+
const warnings = [];
|
|
104
|
+
const notes = [];
|
|
105
|
+
const designApprovalArtifacts = [
|
|
106
|
+
{ label: "design.md", path: designPath },
|
|
107
|
+
{ label: "pencil-design.md", path: pencilDesignPath },
|
|
108
|
+
{ label: "pencil-bindings.md", path: pencilBindingsPath }
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
for (const malformed of [...designMarkers.malformed, ...taskMarkers.malformed]) {
|
|
112
|
+
warnings.push(`Malformed discipline marker at line ${malformed.line}: ${malformed.reason}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let designApprovalState = "missing";
|
|
116
|
+
let designApprovalStale = false;
|
|
117
|
+
if (designApproval) {
|
|
118
|
+
if (!statusTokenMatches(designApproval.status, ["APPROVED", "PASS", "ACCEPTED"])) {
|
|
119
|
+
designApprovalState = "rejected";
|
|
120
|
+
blockers.push(
|
|
121
|
+
`Design approval marker is not approved (${designApproval.status}); keep workflow in tasks/design.`
|
|
122
|
+
);
|
|
123
|
+
} else {
|
|
124
|
+
designApprovalState = "approved";
|
|
125
|
+
if (designApproval.time) {
|
|
126
|
+
const approvalMs = Date.parse(designApproval.time);
|
|
127
|
+
const staleArtifacts = [];
|
|
128
|
+
if (Number.isFinite(approvalMs)) {
|
|
129
|
+
for (const artifact of designApprovalArtifacts) {
|
|
130
|
+
let artifactMtimeMs = 0;
|
|
131
|
+
try {
|
|
132
|
+
artifactMtimeMs = fs.statSync(artifact.path).mtimeMs;
|
|
133
|
+
} catch (_error) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (artifactMtimeMs > approvalMs) {
|
|
137
|
+
staleArtifacts.push(artifact.label);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (staleArtifacts.length > 0) {
|
|
142
|
+
designApprovalState = "stale";
|
|
143
|
+
designApprovalStale = true;
|
|
144
|
+
blockers.push(
|
|
145
|
+
`Design approval marker is stale because design artifacts changed after approval timestamp: ${staleArtifacts.join(", ")}.`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
warnings.push("Design approval marker is missing timestamp; staleness checks are limited.");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} else if (hasAnyMarker || gateMode.strict) {
|
|
153
|
+
blockers.push(
|
|
154
|
+
"Missing required design approval marker (`- design approval: APPROVED @ <ISO8601>`) for disciplined handoff."
|
|
155
|
+
);
|
|
156
|
+
} else {
|
|
157
|
+
warnings.push(
|
|
158
|
+
"Design approval marker is missing; legacy compatibility mode keeps this advisory. Set DA_VINCI_DISCIPLINE_REQUIRE_APPROVAL=1 to enforce."
|
|
159
|
+
);
|
|
160
|
+
notes.push("Legacy compatibility mode: missing discipline markers are advisory.");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!planSelfReview) {
|
|
164
|
+
warnings.push("Missing plan self-review marker in tasks.md (`- plan self review: PASS @ <ISO8601>`).");
|
|
165
|
+
} else if (!statusTokenMatches(planSelfReview.status, ["PASS", "APPROVED", "DONE"])) {
|
|
166
|
+
warnings.push(`Plan self-review marker is not PASS (${planSelfReview.status}).`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!operatorReviewAck) {
|
|
170
|
+
warnings.push("Missing operator review acknowledgment marker in tasks.md (`- operator review ack: ACKNOWLEDGED @ <ISO8601>`).");
|
|
171
|
+
} else if (!statusTokenMatches(operatorReviewAck.status, ["ACKNOWLEDGED", "ACK", "CONFIRMED", "APPROVED"])) {
|
|
172
|
+
warnings.push(`Operator review acknowledgment marker is not acknowledged (${operatorReviewAck.status}).`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
strictMode: gateMode.strict,
|
|
177
|
+
hasAnyMarker,
|
|
178
|
+
designApproval: {
|
|
179
|
+
state: designApprovalState,
|
|
180
|
+
stale: designApprovalStale,
|
|
181
|
+
marker: designApproval
|
|
182
|
+
},
|
|
183
|
+
planSelfReview: {
|
|
184
|
+
marker: planSelfReview
|
|
185
|
+
},
|
|
186
|
+
operatorReviewAck: {
|
|
187
|
+
marker: operatorReviewAck
|
|
188
|
+
},
|
|
189
|
+
malformed: [...designMarkers.malformed, ...taskMarkers.malformed],
|
|
190
|
+
blockers,
|
|
191
|
+
warnings,
|
|
192
|
+
notes
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function mergeSignalBySurface(signals) {
|
|
197
|
+
const summary = {};
|
|
198
|
+
for (const signal of signals || []) {
|
|
199
|
+
const key = String(signal.surface || "").trim();
|
|
200
|
+
if (!key || summary[key]) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
summary[key] = signal;
|
|
204
|
+
}
|
|
205
|
+
return summary;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function applyTaskExecutionAndReviewFindings(findings, signals) {
|
|
209
|
+
const taskExecutionSignals = listSignalsBySurfacePrefix(signals, "task-execution.");
|
|
210
|
+
const taskReviewSignals = listSignalsBySurfacePrefix(signals, "task-review.");
|
|
211
|
+
const latestTaskExecution = mergeSignalBySurface(taskExecutionSignals);
|
|
212
|
+
const latestTaskReview = mergeSignalBySurface(taskReviewSignals);
|
|
213
|
+
|
|
214
|
+
for (const signal of Object.values(latestTaskExecution)) {
|
|
215
|
+
const envelope = signal.details && signal.details.envelope ? signal.details.envelope : null;
|
|
216
|
+
const taskGroupId =
|
|
217
|
+
(envelope && envelope.taskGroupId) ||
|
|
218
|
+
String(signal.surface || "").replace(/^task-execution\./, "") ||
|
|
219
|
+
"unknown";
|
|
220
|
+
if (signal.status === STATUS.BLOCK) {
|
|
221
|
+
findings.blockers.push(`Task group ${taskGroupId} is BLOCKED in implementer status envelope.`);
|
|
222
|
+
} else if (signal.status === STATUS.WARN) {
|
|
223
|
+
findings.warnings.push(`Task group ${taskGroupId} has unresolved implementer concerns/context needs.`);
|
|
224
|
+
}
|
|
225
|
+
if (envelope && envelope.summary) {
|
|
226
|
+
findings.notes.push(`Implementer summary ${taskGroupId}: ${envelope.summary}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const reviewStateByGroup = {};
|
|
231
|
+
for (const signal of Object.values(latestTaskReview)) {
|
|
232
|
+
const envelope = signal.details && signal.details.envelope ? signal.details.envelope : null;
|
|
233
|
+
const taskGroupId =
|
|
234
|
+
(envelope && envelope.taskGroupId) ||
|
|
235
|
+
String(signal.surface || "").replace(/^task-review\./, "").split(".")[0] ||
|
|
236
|
+
"unknown";
|
|
237
|
+
const stage = envelope && envelope.stage ? envelope.stage : String(signal.surface || "").split(".").pop();
|
|
238
|
+
if (!reviewStateByGroup[taskGroupId]) {
|
|
239
|
+
reviewStateByGroup[taskGroupId] = {};
|
|
240
|
+
}
|
|
241
|
+
reviewStateByGroup[taskGroupId][stage] = signal.status;
|
|
242
|
+
if (signal.status === STATUS.BLOCK) {
|
|
243
|
+
findings.blockers.push(`Task review ${taskGroupId}/${stage} is BLOCK.`);
|
|
244
|
+
} else if (signal.status === STATUS.WARN) {
|
|
245
|
+
findings.warnings.push(`Task review ${taskGroupId}/${stage} is WARN and requires follow-up.`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (const [taskGroupId, state] of Object.entries(reviewStateByGroup)) {
|
|
250
|
+
if (state.quality && state.quality === STATUS.PASS && state.spec !== STATUS.PASS) {
|
|
251
|
+
findings.blockers.push(
|
|
252
|
+
`Task review ordering violation for ${taskGroupId}: quality review passed before spec review PASS.`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function normalizeCheckpointStatus(status) {
|
|
259
|
+
const normalized = String(status || "").toUpperCase();
|
|
260
|
+
if (normalized === STATUS.PASS || normalized === STATUS.WARN || normalized === STATUS.BLOCK) {
|
|
261
|
+
return normalized;
|
|
262
|
+
}
|
|
263
|
+
return STATUS.PASS;
|
|
264
|
+
}
|
|
265
|
+
|
|
52
266
|
function collectIntegrityAudit(projectRoot, workflowRoot, signalSummary) {
|
|
53
267
|
if (!pathExists(workflowRoot)) {
|
|
54
268
|
return null;
|
|
@@ -121,6 +335,25 @@ function applyExecutionSignalFindings(stageId, findings, signalSummary) {
|
|
|
121
335
|
findings.warnings.push(`Planning diff signal diff-spec reports ${diffSignal.status}.`);
|
|
122
336
|
}
|
|
123
337
|
|
|
338
|
+
const lintTasksSignal = signalSummary["lint-tasks"];
|
|
339
|
+
const strictPromotion =
|
|
340
|
+
String(process.env.DA_VINCI_DISCIPLINE_STRICT_PROMOTION || "").trim() === "1" ||
|
|
341
|
+
/^true$/i.test(String(process.env.DA_VINCI_DISCIPLINE_STRICT_PROMOTION || "").trim());
|
|
342
|
+
if (lintTasksSignal && lintTasksSignal.status === STATUS.BLOCK) {
|
|
343
|
+
findings.blockers.push("lint-tasks signal is BLOCK and prevents promotion into build.");
|
|
344
|
+
if (nextStageId === "build" || nextStageId === "verify" || nextStageId === "complete") {
|
|
345
|
+
nextStageId = "tasks";
|
|
346
|
+
}
|
|
347
|
+
} else if (lintTasksSignal && lintTasksSignal.status === STATUS.WARN) {
|
|
348
|
+
findings.warnings.push("lint-tasks signal is WARN.");
|
|
349
|
+
if (strictPromotion && (nextStageId === "build" || nextStageId === "verify" || nextStageId === "complete")) {
|
|
350
|
+
findings.blockers.push(
|
|
351
|
+
"DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-tasks WARN blocks promotion into build."
|
|
352
|
+
);
|
|
353
|
+
nextStageId = "tasks";
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
124
357
|
const verificationSignal = signalSummary["verify-coverage"];
|
|
125
358
|
if (verificationSignal && verificationSignal.status === STATUS.BLOCK) {
|
|
126
359
|
findings.blockers.push("verify-coverage signal is BLOCK.");
|
|
@@ -266,6 +499,24 @@ function deriveStageFromArtifacts(artifactState, checkpointStatuses, findings) {
|
|
|
266
499
|
findings.warnings.push("`design checkpoint` is WARN.");
|
|
267
500
|
}
|
|
268
501
|
|
|
502
|
+
const designSourceCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.DESIGN_SOURCE];
|
|
503
|
+
if (designSourceCheckpoint === STATUS.BLOCK) {
|
|
504
|
+
findings.blockers.push("`design source checkpoint` is BLOCK.");
|
|
505
|
+
return "design";
|
|
506
|
+
}
|
|
507
|
+
if (designSourceCheckpoint === STATUS.WARN) {
|
|
508
|
+
findings.warnings.push("`design source checkpoint` is WARN.");
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const runtimeGateCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.RUNTIME_GATE];
|
|
512
|
+
if (runtimeGateCheckpoint === STATUS.BLOCK) {
|
|
513
|
+
findings.blockers.push("`mcp runtime gate` is BLOCK.");
|
|
514
|
+
return "design";
|
|
515
|
+
}
|
|
516
|
+
if (runtimeGateCheckpoint === STATUS.WARN) {
|
|
517
|
+
findings.warnings.push("`mcp runtime gate` is WARN.");
|
|
518
|
+
}
|
|
519
|
+
|
|
269
520
|
const mappingCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.MAPPING];
|
|
270
521
|
if (mappingCheckpoint === STATUS.BLOCK) {
|
|
271
522
|
findings.blockers.push("`mapping checkpoint` is BLOCK.");
|
|
@@ -335,6 +586,21 @@ function deriveTaskGroupMetadata(tasksMarkdownText, checkpointStatuses) {
|
|
|
335
586
|
}
|
|
336
587
|
|
|
337
588
|
const normalizedTaskCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.TASK] || STATUS.WARN;
|
|
589
|
+
const groupMetadataById = new Map(
|
|
590
|
+
(taskArtifact.taskGroups || []).map((group) => [
|
|
591
|
+
group.id,
|
|
592
|
+
{
|
|
593
|
+
targetFiles: Array.isArray(group.targetFiles) ? group.targetFiles : [],
|
|
594
|
+
fileReferences: Array.isArray(group.fileReferences) ? group.fileReferences : [],
|
|
595
|
+
verificationActions: Array.isArray(group.verificationActions) ? group.verificationActions : [],
|
|
596
|
+
verificationCommands: Array.isArray(group.verificationCommands) ? group.verificationCommands : [],
|
|
597
|
+
executionIntent: Array.isArray(group.executionIntent) ? group.executionIntent : [],
|
|
598
|
+
reviewIntent: group.reviewIntent === true,
|
|
599
|
+
testingIntent: group.testingIntent === true,
|
|
600
|
+
codeChangeLikely: group.codeChangeLikely === true
|
|
601
|
+
}
|
|
602
|
+
])
|
|
603
|
+
);
|
|
338
604
|
const metadata = sections.map((section, index) => {
|
|
339
605
|
const total = section.checklistItems.length;
|
|
340
606
|
const done = section.checklistItems.filter((item) => item.checked).length;
|
|
@@ -345,6 +611,7 @@ function deriveTaskGroupMetadata(tasksMarkdownText, checkpointStatuses) {
|
|
|
345
611
|
sectionStatus === "completed"
|
|
346
612
|
? "advance to next task group"
|
|
347
613
|
: section.checklistItems.find((item) => !item.checked)?.text || "continue group work";
|
|
614
|
+
const groupMetadata = groupMetadataById.get(section.id) || {};
|
|
348
615
|
|
|
349
616
|
return {
|
|
350
617
|
taskGroupId: section.id,
|
|
@@ -354,6 +621,14 @@ function deriveTaskGroupMetadata(tasksMarkdownText, checkpointStatuses) {
|
|
|
354
621
|
checkpointOutcome: normalizedTaskCheckpoint,
|
|
355
622
|
evidence: section.checklistItems.filter((item) => item.checked).map((item) => item.text),
|
|
356
623
|
nextAction,
|
|
624
|
+
targetFiles: groupMetadata.targetFiles || [],
|
|
625
|
+
fileReferences: groupMetadata.fileReferences || [],
|
|
626
|
+
verificationActions: groupMetadata.verificationActions || [],
|
|
627
|
+
verificationCommands: groupMetadata.verificationCommands || [],
|
|
628
|
+
executionIntent: groupMetadata.executionIntent || [],
|
|
629
|
+
reviewIntent: groupMetadata.reviewIntent === true,
|
|
630
|
+
testingIntent: groupMetadata.testingIntent === true,
|
|
631
|
+
codeChangeLikely: groupMetadata.codeChangeLikely === true,
|
|
357
632
|
resumeCursor: {
|
|
358
633
|
groupIndex: index,
|
|
359
634
|
nextUncheckedItem:
|
|
@@ -365,19 +640,30 @@ function deriveTaskGroupMetadata(tasksMarkdownText, checkpointStatuses) {
|
|
|
365
640
|
});
|
|
366
641
|
|
|
367
642
|
if (metadata.length === 0 && taskArtifact.taskGroups.length > 0) {
|
|
368
|
-
return taskArtifact.taskGroups.map((group, index) =>
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
643
|
+
return taskArtifact.taskGroups.map((group, index) => {
|
|
644
|
+
const groupMetadata = groupMetadataById.get(group.id) || {};
|
|
645
|
+
return {
|
|
646
|
+
taskGroupId: group.id,
|
|
647
|
+
title: group.title,
|
|
648
|
+
status: "pending",
|
|
649
|
+
completion: 0,
|
|
650
|
+
checkpointOutcome: normalizedTaskCheckpoint,
|
|
651
|
+
evidence: [],
|
|
652
|
+
nextAction: "start task group",
|
|
653
|
+
targetFiles: groupMetadata.targetFiles || [],
|
|
654
|
+
fileReferences: groupMetadata.fileReferences || [],
|
|
655
|
+
verificationActions: groupMetadata.verificationActions || [],
|
|
656
|
+
verificationCommands: groupMetadata.verificationCommands || [],
|
|
657
|
+
executionIntent: groupMetadata.executionIntent || [],
|
|
658
|
+
reviewIntent: groupMetadata.reviewIntent === true,
|
|
659
|
+
testingIntent: groupMetadata.testingIntent === true,
|
|
660
|
+
codeChangeLikely: groupMetadata.codeChangeLikely === true,
|
|
661
|
+
resumeCursor: {
|
|
662
|
+
groupIndex: index,
|
|
663
|
+
nextUncheckedItem: null
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
});
|
|
381
667
|
}
|
|
382
668
|
|
|
383
669
|
return metadata;
|
|
@@ -474,14 +760,32 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
|
|
|
474
760
|
|
|
475
761
|
const tasksArtifactText = activeChangeDir ? readTextIfExists(path.join(activeChangeDir, "tasks.md")) : "";
|
|
476
762
|
const checkpointStatuses = activeChangeDir ? readCheckpointStatuses(activeChangeDir) : {};
|
|
477
|
-
const
|
|
478
|
-
?
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
763
|
+
const changeSignals = activeChangeId
|
|
764
|
+
? readExecutionSignals(projectRoot, {
|
|
765
|
+
changeId: activeChangeId
|
|
766
|
+
})
|
|
767
|
+
: [];
|
|
768
|
+
const signalSummary = summarizeSignalsBySurface(changeSignals);
|
|
769
|
+
const disciplineState = activeChangeDir ? inspectDisciplineState(activeChangeDir) : null;
|
|
770
|
+
const freshnessArtifactPaths = activeChangeDir
|
|
771
|
+
? {
|
|
772
|
+
proposalPath: path.join(activeChangeDir, "proposal.md"),
|
|
773
|
+
tasksPath: path.join(activeChangeDir, "tasks.md"),
|
|
774
|
+
bindingsPath: path.join(activeChangeDir, "pencil-bindings.md"),
|
|
775
|
+
pencilDesignPath: path.join(activeChangeDir, "pencil-design.md"),
|
|
776
|
+
verificationPath: path.join(activeChangeDir, "verification.md")
|
|
777
|
+
}
|
|
778
|
+
: null;
|
|
484
779
|
const integrityAudit = collectIntegrityAudit(projectRoot, workflowRoot, signalSummary);
|
|
780
|
+
const designCheckpointStatus = normalizeCheckpointStatus(checkpointStatuses[CHECKPOINT_LABELS.DESIGN]);
|
|
781
|
+
const designSourceCheckpointStatus = normalizeCheckpointStatus(
|
|
782
|
+
checkpointStatuses[CHECKPOINT_LABELS.DESIGN_SOURCE]
|
|
783
|
+
);
|
|
784
|
+
const runtimeGateCheckpointStatus = normalizeCheckpointStatus(
|
|
785
|
+
checkpointStatuses[CHECKPOINT_LABELS.RUNTIME_GATE]
|
|
786
|
+
);
|
|
787
|
+
const mappingCheckpointStatus = normalizeCheckpointStatus(checkpointStatuses[CHECKPOINT_LABELS.MAPPING]);
|
|
788
|
+
const taskCheckpointStatus = normalizeCheckpointStatus(checkpointStatuses[CHECKPOINT_LABELS.TASK]);
|
|
485
789
|
|
|
486
790
|
if (activeChangeId) {
|
|
487
791
|
const persistedSelection = selectPersistedStateForChange(projectRoot, activeChangeId, {
|
|
@@ -513,6 +817,33 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
|
|
|
513
817
|
persistedFindings,
|
|
514
818
|
signalSummary
|
|
515
819
|
);
|
|
820
|
+
applyTaskExecutionAndReviewFindings(persistedFindings, changeSignals);
|
|
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
|
+
}
|
|
516
847
|
persistedFindings.notes.push("workflow-status is using trusted persisted workflow state.");
|
|
517
848
|
dedupeFindings(persistedFindings);
|
|
518
849
|
|
|
@@ -524,6 +855,39 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
|
|
|
524
855
|
persistedGates[HANDOFF_GATES.VERIFY_TO_COMPLETE] =
|
|
525
856
|
completionAudit.status === "PASS" ? STATUS.PASS : STATUS.WARN;
|
|
526
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
|
+
}
|
|
527
891
|
|
|
528
892
|
return buildWorkflowResult({
|
|
529
893
|
projectRoot,
|
|
@@ -542,12 +906,11 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
|
|
|
542
906
|
ambiguousChangeSelection
|
|
543
907
|
},
|
|
544
908
|
source: "persisted",
|
|
545
|
-
taskGroups:
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
: []
|
|
909
|
+
taskGroups: persistedTaskGroups,
|
|
910
|
+
discipline: disciplineState,
|
|
911
|
+
executionProfile,
|
|
912
|
+
worktreePreflight,
|
|
913
|
+
verificationFreshness
|
|
551
914
|
});
|
|
552
915
|
}
|
|
553
916
|
|
|
@@ -575,21 +938,86 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
|
|
|
575
938
|
: null;
|
|
576
939
|
stageId = applyAuditFindings(stageId, findings, integrityAudit, completionAudit);
|
|
577
940
|
stageId = applyExecutionSignalFindings(stageId, findings, signalSummary);
|
|
941
|
+
applyTaskExecutionAndReviewFindings(findings, changeSignals);
|
|
942
|
+
if (disciplineState) {
|
|
943
|
+
findings.blockers.push(...disciplineState.blockers);
|
|
944
|
+
findings.warnings.push(...disciplineState.warnings);
|
|
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
|
+
}
|
|
578
964
|
|
|
579
965
|
const gates = {
|
|
580
966
|
[HANDOFF_GATES.BREAKDOWN_TO_DESIGN]:
|
|
581
967
|
artifactState.proposal && artifactState.specFiles.length > 0 ? STATUS.PASS : STATUS.BLOCK,
|
|
582
|
-
[HANDOFF_GATES.DESIGN_TO_TASKS]:
|
|
968
|
+
[HANDOFF_GATES.DESIGN_TO_TASKS]: mergeStatuses([
|
|
583
969
|
artifactState.design && artifactState.pencilDesign && artifactState.pencilBindings
|
|
584
970
|
? STATUS.PASS
|
|
585
971
|
: STATUS.BLOCK,
|
|
586
|
-
|
|
972
|
+
designCheckpointStatus,
|
|
973
|
+
designSourceCheckpointStatus,
|
|
974
|
+
runtimeGateCheckpointStatus,
|
|
975
|
+
mappingCheckpointStatus
|
|
976
|
+
]),
|
|
977
|
+
[HANDOFF_GATES.TASKS_TO_BUILD]: mergeStatuses([
|
|
978
|
+
artifactState.tasks ? STATUS.PASS : STATUS.BLOCK,
|
|
979
|
+
designCheckpointStatus,
|
|
980
|
+
designSourceCheckpointStatus,
|
|
981
|
+
runtimeGateCheckpointStatus,
|
|
982
|
+
mappingCheckpointStatus,
|
|
983
|
+
taskCheckpointStatus
|
|
984
|
+
]),
|
|
587
985
|
[HANDOFF_GATES.BUILD_TO_VERIFY]: artifactState.verification ? STATUS.PASS : STATUS.BLOCK,
|
|
588
986
|
[HANDOFF_GATES.VERIFY_TO_COMPLETE]:
|
|
589
987
|
completionAudit && completionAudit.status === "PASS" ? STATUS.PASS : STATUS.WARN
|
|
590
988
|
};
|
|
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
|
+
}
|
|
591
995
|
|
|
592
996
|
const derivedTaskGroups = deriveTaskGroupMetadata(tasksArtifactText, checkpointStatuses);
|
|
997
|
+
const executionProfile = deriveExecutionProfile({
|
|
998
|
+
stage: stageId,
|
|
999
|
+
taskGroups: derivedTaskGroups
|
|
1000
|
+
});
|
|
1001
|
+
let worktreePreflight = null;
|
|
1002
|
+
if (activeChangeId && (stageId === "build" || stageId === "verify")) {
|
|
1003
|
+
worktreePreflight = runWorktreePreflight(projectRoot, {
|
|
1004
|
+
parallelPreferred: executionProfile.mode === "bounded_parallel"
|
|
1005
|
+
});
|
|
1006
|
+
if (
|
|
1007
|
+
executionProfile.mode === "bounded_parallel" &&
|
|
1008
|
+
worktreePreflight.summary &&
|
|
1009
|
+
worktreePreflight.summary.recommendedIsolation
|
|
1010
|
+
) {
|
|
1011
|
+
executionProfile.effectiveMode = "serial";
|
|
1012
|
+
executionProfile.rationale = dedupeMessages([
|
|
1013
|
+
...(executionProfile.rationale || []),
|
|
1014
|
+
"worktree preflight recommends isolation; effective mode downgraded to serial"
|
|
1015
|
+
]);
|
|
1016
|
+
findings.warnings.push(
|
|
1017
|
+
"Bounded-parallel profile downgraded to serial until worktree isolation is ready or explicitly accepted."
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
593
1021
|
if (activeChangeId && !ambiguousChangeSelection && derivedTaskGroups.length > 0) {
|
|
594
1022
|
const metadataPayload = {
|
|
595
1023
|
version: 1,
|
|
@@ -625,7 +1053,11 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
|
|
|
625
1053
|
changeId: activeChangeId || requestedChangeId || "change-001",
|
|
626
1054
|
ambiguousChangeSelection
|
|
627
1055
|
},
|
|
628
|
-
taskGroups: derivedTaskGroups
|
|
1056
|
+
taskGroups: derivedTaskGroups,
|
|
1057
|
+
discipline: disciplineState,
|
|
1058
|
+
executionProfile,
|
|
1059
|
+
worktreePreflight,
|
|
1060
|
+
verificationFreshness
|
|
629
1061
|
});
|
|
630
1062
|
|
|
631
1063
|
if (activeChangeId && !ambiguousChangeSelection) {
|
|
@@ -685,7 +1117,11 @@ function buildWorkflowResult(params) {
|
|
|
685
1117
|
audits: params.audits,
|
|
686
1118
|
nextStep,
|
|
687
1119
|
source: params.source || "derived",
|
|
688
|
-
taskGroups: Array.isArray(params.taskGroups) ? params.taskGroups : []
|
|
1120
|
+
taskGroups: Array.isArray(params.taskGroups) ? params.taskGroups : [],
|
|
1121
|
+
discipline: params.discipline || null,
|
|
1122
|
+
executionProfile: params.executionProfile || null,
|
|
1123
|
+
worktreePreflight: params.worktreePreflight || null,
|
|
1124
|
+
verificationFreshness: params.verificationFreshness || null
|
|
689
1125
|
};
|
|
690
1126
|
}
|
|
691
1127
|
|
|
@@ -707,6 +1143,25 @@ function formatWorkflowStatusReport(result) {
|
|
|
707
1143
|
lines.push("Next route: none");
|
|
708
1144
|
}
|
|
709
1145
|
|
|
1146
|
+
if (result.discipline && result.discipline.designApproval) {
|
|
1147
|
+
lines.push(`Discipline design approval: ${result.discipline.designApproval.state}`);
|
|
1148
|
+
}
|
|
1149
|
+
if (result.executionProfile) {
|
|
1150
|
+
lines.push(
|
|
1151
|
+
`Execution profile: ${result.executionProfile.mode}${
|
|
1152
|
+
result.executionProfile.effectiveMode
|
|
1153
|
+
? ` (effective ${result.executionProfile.effectiveMode})`
|
|
1154
|
+
: ""
|
|
1155
|
+
}, max parallel ${result.executionProfile.maxParallel}`
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
if (result.worktreePreflight) {
|
|
1159
|
+
lines.push(`Worktree preflight: ${result.worktreePreflight.status} (advisory)`);
|
|
1160
|
+
}
|
|
1161
|
+
if (result.verificationFreshness) {
|
|
1162
|
+
lines.push(`Verification freshness: ${result.verificationFreshness.fresh ? "fresh" : "stale"}`);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
710
1165
|
if (result.failures.length > 0) {
|
|
711
1166
|
lines.push("Blockers:");
|
|
712
1167
|
for (const message of result.failures) {
|
|
@@ -767,6 +1222,21 @@ function formatNextStepReport(result) {
|
|
|
767
1222
|
if (result.nextStep.reason) {
|
|
768
1223
|
lines.push(`Reason: ${result.nextStep.reason}`);
|
|
769
1224
|
}
|
|
1225
|
+
if (result.executionProfile) {
|
|
1226
|
+
lines.push(
|
|
1227
|
+
`Execution profile: ${result.executionProfile.mode}${
|
|
1228
|
+
result.executionProfile.effectiveMode
|
|
1229
|
+
? ` (effective ${result.executionProfile.effectiveMode})`
|
|
1230
|
+
: ""
|
|
1231
|
+
}, max parallel ${result.executionProfile.maxParallel}`
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
if (result.worktreePreflight) {
|
|
1235
|
+
lines.push(`Worktree preflight: ${result.worktreePreflight.status} (advisory)`);
|
|
1236
|
+
}
|
|
1237
|
+
if (result.verificationFreshness && !result.verificationFreshness.fresh) {
|
|
1238
|
+
lines.push("Completion evidence is stale; stay in verify until fresh evidence is recorded.");
|
|
1239
|
+
}
|
|
770
1240
|
if (Array.isArray(result.taskGroups) && result.taskGroups.length > 0) {
|
|
771
1241
|
const active =
|
|
772
1242
|
result.taskGroups.find((group) => group.status === "in_progress") ||
|