@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.
Files changed (72) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +49 -14
  3. package/README.zh-CN.md +169 -14
  4. package/commands/claude/dv/breakdown.md +8 -0
  5. package/commands/claude/dv/build.md +16 -0
  6. package/commands/claude/dv/continue.md +4 -0
  7. package/commands/claude/dv/design.md +5 -2
  8. package/commands/claude/dv/tasks.md +14 -0
  9. package/commands/claude/dv/verify.md +11 -0
  10. package/commands/codex/prompts/dv-breakdown.md +8 -0
  11. package/commands/codex/prompts/dv-build.md +16 -0
  12. package/commands/codex/prompts/dv-continue.md +4 -0
  13. package/commands/codex/prompts/dv-design.md +5 -2
  14. package/commands/codex/prompts/dv-tasks.md +14 -0
  15. package/commands/codex/prompts/dv-verify.md +10 -0
  16. package/commands/gemini/dv/breakdown.toml +8 -0
  17. package/commands/gemini/dv/build.toml +16 -0
  18. package/commands/gemini/dv/continue.toml +4 -0
  19. package/commands/gemini/dv/design.toml +5 -2
  20. package/commands/gemini/dv/tasks.toml +14 -0
  21. package/commands/gemini/dv/verify.toml +10 -0
  22. package/commands/templates/dv-continue.shared.md +4 -0
  23. package/docs/discipline-and-orchestration-upgrade.md +83 -0
  24. package/docs/dv-command-reference.md +61 -2
  25. package/docs/execution-chain-migration.md +23 -0
  26. package/docs/execution-chain-plan.md +10 -3
  27. package/docs/mode-use-cases.md +2 -1
  28. package/docs/pencil-rendering-workflow.md +15 -12
  29. package/docs/prompt-entrypoints.md +5 -0
  30. package/docs/prompt-presets/README.md +1 -1
  31. package/docs/prompt-presets/desktop-app.md +3 -3
  32. package/docs/prompt-presets/mobile-app.md +3 -3
  33. package/docs/prompt-presets/tablet-app.md +3 -3
  34. package/docs/prompt-presets/web-app.md +3 -3
  35. package/docs/skill-usage.md +61 -38
  36. package/docs/workflow-examples.md +16 -13
  37. package/docs/workflow-overview.md +19 -0
  38. package/docs/zh-CN/dv-command-reference.md +59 -2
  39. package/docs/zh-CN/execution-chain-migration.md +23 -0
  40. package/docs/zh-CN/mode-use-cases.md +2 -1
  41. package/docs/zh-CN/pencil-rendering-workflow.md +15 -12
  42. package/docs/zh-CN/prompt-entrypoints.md +5 -0
  43. package/docs/zh-CN/prompt-presets/README.md +1 -1
  44. package/docs/zh-CN/prompt-presets/desktop-app.md +3 -3
  45. package/docs/zh-CN/prompt-presets/mobile-app.md +3 -3
  46. package/docs/zh-CN/prompt-presets/tablet-app.md +3 -3
  47. package/docs/zh-CN/prompt-presets/web-app.md +3 -3
  48. package/docs/zh-CN/skill-usage.md +61 -38
  49. package/docs/zh-CN/workflow-examples.md +15 -13
  50. package/docs/zh-CN/workflow-overview.md +19 -0
  51. package/examples/greenfield-spec-markupflow/.da-vinci/state/execution-signals/demo__lint-tasks.json +16 -0
  52. package/lib/audit-parsers.js +166 -10
  53. package/lib/audit.js +3 -26
  54. package/lib/cli.js +156 -2
  55. package/lib/design-source-registry.js +146 -0
  56. package/lib/execution-profile.js +143 -0
  57. package/lib/execution-signals.js +19 -1
  58. package/lib/lint-tasks.js +86 -2
  59. package/lib/planning-parsers.js +255 -18
  60. package/lib/save-current-design.js +790 -0
  61. package/lib/supervisor-review.js +3 -2
  62. package/lib/task-execution.js +160 -0
  63. package/lib/task-review.js +197 -0
  64. package/lib/verify.js +152 -1
  65. package/lib/workflow-bootstrap.js +2 -13
  66. package/lib/workflow-persisted-state.js +3 -1
  67. package/lib/workflow-state.js +503 -33
  68. package/lib/worktree-preflight.js +214 -0
  69. package/package.json +1 -1
  70. package/references/artifact-templates.md +56 -6
  71. package/tui/catalog.js +103 -0
  72. package/tui/index.js +2274 -418
@@ -1,6 +1,12 @@
1
+ const fs = require("fs");
1
2
  const path = require("path");
2
3
  const { auditProject } = require("./audit");
3
- const { parseCheckpointStatusMap, normalizeCheckpointLabel } = require("./audit-parsers");
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 { readExecutionSignals, summarizeSignalsBySurface } = require("./execution-signals");
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
- 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
- }));
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 signalSummary = activeChangeId
478
- ? summarizeSignalsBySurface(
479
- readExecutionSignals(projectRoot, {
480
- changeId: activeChangeId
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
- persistedTaskMetadata && Array.isArray(persistedTaskMetadata.taskGroups)
547
- ? persistedTaskMetadata.taskGroups
548
- : Array.isArray(persistedRecord.taskGroups)
549
- ? persistedRecord.taskGroups
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
- [HANDOFF_GATES.TASKS_TO_BUILD]: artifactState.tasks ? STATUS.PASS : STATUS.BLOCK,
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") ||