@xenonbyte/da-vinci-workflow 0.2.3 → 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 (41) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +32 -7
  3. package/README.zh-CN.md +151 -7
  4. package/commands/claude/dv/build.md +5 -0
  5. package/commands/claude/dv/continue.md +4 -0
  6. package/commands/claude/dv/tasks.md +6 -0
  7. package/commands/claude/dv/verify.md +2 -0
  8. package/commands/codex/prompts/dv-build.md +5 -0
  9. package/commands/codex/prompts/dv-continue.md +4 -0
  10. package/commands/codex/prompts/dv-tasks.md +6 -0
  11. package/commands/codex/prompts/dv-verify.md +2 -0
  12. package/commands/gemini/dv/build.toml +5 -0
  13. package/commands/gemini/dv/continue.toml +4 -0
  14. package/commands/gemini/dv/tasks.toml +6 -0
  15. package/commands/gemini/dv/verify.toml +2 -0
  16. package/commands/templates/dv-continue.shared.md +4 -0
  17. package/docs/discipline-and-orchestration-upgrade.md +83 -0
  18. package/docs/dv-command-reference.md +18 -2
  19. package/docs/execution-chain-migration.md +23 -0
  20. package/docs/prompt-entrypoints.md +5 -0
  21. package/docs/skill-usage.md +16 -0
  22. package/docs/workflow-overview.md +17 -0
  23. package/docs/zh-CN/dv-command-reference.md +16 -2
  24. package/docs/zh-CN/execution-chain-migration.md +23 -0
  25. package/docs/zh-CN/prompt-entrypoints.md +5 -0
  26. package/docs/zh-CN/skill-usage.md +16 -0
  27. package/docs/zh-CN/workflow-overview.md +17 -0
  28. package/lib/audit-parsers.js +148 -1
  29. package/lib/cli.js +106 -1
  30. package/lib/execution-profile.js +143 -0
  31. package/lib/execution-signals.js +19 -1
  32. package/lib/lint-tasks.js +86 -2
  33. package/lib/planning-parsers.js +255 -18
  34. package/lib/supervisor-review.js +2 -1
  35. package/lib/task-execution.js +160 -0
  36. package/lib/task-review.js +197 -0
  37. package/lib/verify.js +152 -1
  38. package/lib/workflow-state.js +452 -30
  39. package/lib/worktree-preflight.js +214 -0
  40. package/package.json +1 -1
  41. package/references/artifact-templates.md +56 -6
@@ -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,
@@ -23,7 +29,14 @@ const {
23
29
  readTaskGroupMetadata,
24
30
  sanitizePersistedNotes
25
31
  } = require("./workflow-persisted-state");
26
- 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");
27
40
 
28
41
  const MAX_REPORTED_MESSAGES = 3;
29
42
 
@@ -50,6 +63,198 @@ function dedupeFindings(findings) {
50
63
  findings.notes = dedupeMessages(findings.notes);
51
64
  }
52
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
+
53
258
  function normalizeCheckpointStatus(status) {
54
259
  const normalized = String(status || "").toUpperCase();
55
260
  if (normalized === STATUS.PASS || normalized === STATUS.WARN || normalized === STATUS.BLOCK) {
@@ -130,6 +335,25 @@ function applyExecutionSignalFindings(stageId, findings, signalSummary) {
130
335
  findings.warnings.push(`Planning diff signal diff-spec reports ${diffSignal.status}.`);
131
336
  }
132
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
+
133
357
  const verificationSignal = signalSummary["verify-coverage"];
134
358
  if (verificationSignal && verificationSignal.status === STATUS.BLOCK) {
135
359
  findings.blockers.push("verify-coverage signal is BLOCK.");
@@ -362,6 +586,21 @@ function deriveTaskGroupMetadata(tasksMarkdownText, checkpointStatuses) {
362
586
  }
363
587
 
364
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
+ );
365
604
  const metadata = sections.map((section, index) => {
366
605
  const total = section.checklistItems.length;
367
606
  const done = section.checklistItems.filter((item) => item.checked).length;
@@ -372,6 +611,7 @@ function deriveTaskGroupMetadata(tasksMarkdownText, checkpointStatuses) {
372
611
  sectionStatus === "completed"
373
612
  ? "advance to next task group"
374
613
  : section.checklistItems.find((item) => !item.checked)?.text || "continue group work";
614
+ const groupMetadata = groupMetadataById.get(section.id) || {};
375
615
 
376
616
  return {
377
617
  taskGroupId: section.id,
@@ -381,6 +621,14 @@ function deriveTaskGroupMetadata(tasksMarkdownText, checkpointStatuses) {
381
621
  checkpointOutcome: normalizedTaskCheckpoint,
382
622
  evidence: section.checklistItems.filter((item) => item.checked).map((item) => item.text),
383
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,
384
632
  resumeCursor: {
385
633
  groupIndex: index,
386
634
  nextUncheckedItem:
@@ -392,19 +640,30 @@ function deriveTaskGroupMetadata(tasksMarkdownText, checkpointStatuses) {
392
640
  });
393
641
 
394
642
  if (metadata.length === 0 && taskArtifact.taskGroups.length > 0) {
395
- return taskArtifact.taskGroups.map((group, index) => ({
396
- taskGroupId: group.id,
397
- title: group.title,
398
- status: "pending",
399
- completion: 0,
400
- checkpointOutcome: normalizedTaskCheckpoint,
401
- evidence: [],
402
- nextAction: "start task group",
403
- resumeCursor: {
404
- groupIndex: index,
405
- nextUncheckedItem: null
406
- }
407
- }));
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
+ });
408
667
  }
409
668
 
410
669
  return metadata;
@@ -501,13 +760,22 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
501
760
 
502
761
  const tasksArtifactText = activeChangeDir ? readTextIfExists(path.join(activeChangeDir, "tasks.md")) : "";
503
762
  const checkpointStatuses = activeChangeDir ? readCheckpointStatuses(activeChangeDir) : {};
504
- const signalSummary = activeChangeId
505
- ? summarizeSignalsBySurface(
506
- readExecutionSignals(projectRoot, {
507
- changeId: activeChangeId
508
- })
509
- )
510
- : {};
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;
511
779
  const integrityAudit = collectIntegrityAudit(projectRoot, workflowRoot, signalSummary);
512
780
  const designCheckpointStatus = normalizeCheckpointStatus(checkpointStatuses[CHECKPOINT_LABELS.DESIGN]);
513
781
  const designSourceCheckpointStatus = normalizeCheckpointStatus(
@@ -549,6 +817,33 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
549
817
  persistedFindings,
550
818
  signalSummary
551
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
+ }
552
847
  persistedFindings.notes.push("workflow-status is using trusted persisted workflow state.");
553
848
  dedupeFindings(persistedFindings);
554
849
 
@@ -560,6 +855,39 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
560
855
  persistedGates[HANDOFF_GATES.VERIFY_TO_COMPLETE] =
561
856
  completionAudit.status === "PASS" ? STATUS.PASS : STATUS.WARN;
562
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
+ }
563
891
 
564
892
  return buildWorkflowResult({
565
893
  projectRoot,
@@ -578,12 +906,11 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
578
906
  ambiguousChangeSelection
579
907
  },
580
908
  source: "persisted",
581
- taskGroups:
582
- persistedTaskMetadata && Array.isArray(persistedTaskMetadata.taskGroups)
583
- ? persistedTaskMetadata.taskGroups
584
- : Array.isArray(persistedRecord.taskGroups)
585
- ? persistedRecord.taskGroups
586
- : []
909
+ taskGroups: persistedTaskGroups,
910
+ discipline: disciplineState,
911
+ executionProfile,
912
+ worktreePreflight,
913
+ verificationFreshness
587
914
  });
588
915
  }
589
916
 
@@ -611,6 +938,29 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
611
938
  : null;
612
939
  stageId = applyAuditFindings(stageId, findings, integrityAudit, completionAudit);
613
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
+ }
614
964
 
615
965
  const gates = {
616
966
  [HANDOFF_GATES.BREAKDOWN_TO_DESIGN]:
@@ -636,8 +986,38 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
636
986
  [HANDOFF_GATES.VERIFY_TO_COMPLETE]:
637
987
  completionAudit && completionAudit.status === "PASS" ? STATUS.PASS : STATUS.WARN
638
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
+ }
639
995
 
640
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
+ }
641
1021
  if (activeChangeId && !ambiguousChangeSelection && derivedTaskGroups.length > 0) {
642
1022
  const metadataPayload = {
643
1023
  version: 1,
@@ -673,7 +1053,11 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
673
1053
  changeId: activeChangeId || requestedChangeId || "change-001",
674
1054
  ambiguousChangeSelection
675
1055
  },
676
- taskGroups: derivedTaskGroups
1056
+ taskGroups: derivedTaskGroups,
1057
+ discipline: disciplineState,
1058
+ executionProfile,
1059
+ worktreePreflight,
1060
+ verificationFreshness
677
1061
  });
678
1062
 
679
1063
  if (activeChangeId && !ambiguousChangeSelection) {
@@ -733,7 +1117,11 @@ function buildWorkflowResult(params) {
733
1117
  audits: params.audits,
734
1118
  nextStep,
735
1119
  source: params.source || "derived",
736
- 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
737
1125
  };
738
1126
  }
739
1127
 
@@ -755,6 +1143,25 @@ function formatWorkflowStatusReport(result) {
755
1143
  lines.push("Next route: none");
756
1144
  }
757
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
+
758
1165
  if (result.failures.length > 0) {
759
1166
  lines.push("Blockers:");
760
1167
  for (const message of result.failures) {
@@ -815,6 +1222,21 @@ function formatNextStepReport(result) {
815
1222
  if (result.nextStep.reason) {
816
1223
  lines.push(`Reason: ${result.nextStep.reason}`);
817
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
+ }
818
1240
  if (Array.isArray(result.taskGroups) && result.taskGroups.length > 0) {
819
1241
  const active =
820
1242
  result.taskGroups.find((group) => group.status === "in_progress") ||