@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,7 +1,11 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
  const crypto = require("crypto");
4
- const { getMarkdownSection } = require("./audit-parsers");
4
+ const {
5
+ getMarkdownSection,
6
+ parseDisciplineMarkers,
7
+ DISCIPLINE_MARKER_NAMES
8
+ } = require("./audit-parsers");
5
9
  const { parseRuntimeSpecMarkdown } = require("./artifact-parsers");
6
10
  const { pathExists, readTextIfExists } = require("./utils");
7
11
 
@@ -350,33 +354,239 @@ function parsePageMapArtifact(text) {
350
354
  };
351
355
  }
352
356
 
357
+ function parseInlineCodeTokens(line) {
358
+ const tokens = [];
359
+ const matches = String(line || "").matchAll(/`([^`]+)`/g);
360
+ for (const match of matches) {
361
+ const token = String(match[1] || "").trim();
362
+ if (token) {
363
+ tokens.push(token);
364
+ }
365
+ }
366
+ return tokens;
367
+ }
368
+
369
+ function normalizeExecutionIntent(text) {
370
+ const normalized = normalizeText(text);
371
+ if (!normalized) {
372
+ return "";
373
+ }
374
+ if (/bounded parallel|parallel bounded|parallel-safe/.test(normalized)) {
375
+ return "bounded_parallel";
376
+ }
377
+ if (/serial|sequential|one by one/.test(normalized)) {
378
+ return "serial";
379
+ }
380
+ if (/review required|review-required|review heavy|spec review|quality review/.test(normalized)) {
381
+ return "review_required";
382
+ }
383
+ return "";
384
+ }
385
+
386
+ function hasVerificationIntent(text) {
387
+ return /verify|verification|coverage|assert|validate|smoke/i.test(String(text || ""));
388
+ }
389
+
390
+ function hasTestingIntent(text) {
391
+ return /(?:^|\b)(test|tests|unit test|integration test|e2e|regression|tdd|coverage)(?:\b|$)/i.test(
392
+ String(text || "")
393
+ );
394
+ }
395
+
396
+ function hasReviewIntent(text) {
397
+ return /review|reviewer|spec review|quality review|qa|audit/i.test(String(text || ""));
398
+ }
399
+
400
+ function looksLikeCodeChange(text) {
401
+ return /implement|modify|update|add|create|refactor|fix|remove|rename|rewrite|harden|extend/i.test(
402
+ String(text || "")
403
+ );
404
+ }
405
+
406
+ function isPlaceholderText(text) {
407
+ return /\b(TBD|TODO|implement later|later fill|to be decided)\b/i.test(String(text || ""));
408
+ }
409
+
410
+ function extractFileReferences(text) {
411
+ const references = [];
412
+ const seen = new Set();
413
+ const addReference = (value) => {
414
+ const candidate = String(value || "")
415
+ .replace(/[`"'(),]/g, "")
416
+ .trim();
417
+ if (!candidate) {
418
+ return;
419
+ }
420
+ if (!/[./]/.test(candidate)) {
421
+ return;
422
+ }
423
+ if (candidate.length < 3) {
424
+ return;
425
+ }
426
+ if (seen.has(candidate)) {
427
+ return;
428
+ }
429
+ seen.add(candidate);
430
+ references.push(candidate);
431
+ };
432
+
433
+ for (const token of parseInlineCodeTokens(text)) {
434
+ addReference(token);
435
+ }
436
+ const plainMatches = String(text || "").matchAll(/(?:^|[\s(])([A-Za-z0-9._/-]+(?:\.[A-Za-z0-9_*.-]+|\/))(?:$|[\s),])/g);
437
+ for (const match of plainMatches) {
438
+ addReference(match[1]);
439
+ }
440
+ return references;
441
+ }
442
+
443
+ function extractVerificationCommands(text) {
444
+ const commands = [];
445
+ const supportedCommandPattern =
446
+ /^(?:da-vinci\s+verify-(?:bindings|implementation|structure|coverage)|npm|pnpm|yarn|bun|node|npx|pytest|go test|cargo test|dotnet test|mvn test|gradle test|make test|vitest|jest)\b/i;
447
+ const addCommand = (value) => {
448
+ const command = String(value || "").trim();
449
+ if (!command) {
450
+ return;
451
+ }
452
+ if (!/\s/.test(command)) {
453
+ return;
454
+ }
455
+ if (!supportedCommandPattern.test(command)) {
456
+ return;
457
+ }
458
+ commands.push(command);
459
+ };
460
+ for (const token of parseInlineCodeTokens(text)) {
461
+ addCommand(token);
462
+ }
463
+ return unique(commands);
464
+ }
465
+
466
+ function analyzeTaskGroup(section) {
467
+ const targetFiles = [];
468
+ const verificationActions = [];
469
+ const verificationCommands = [];
470
+ const placeholderItems = [];
471
+ const rawFileReferences = [];
472
+ const executionHints = [];
473
+
474
+ const lines = Array.isArray(section.lines) ? section.lines : [];
475
+ for (let index = 0; index < lines.length; index += 1) {
476
+ const line = String(lines[index] || "");
477
+ rawFileReferences.push(...extractFileReferences(line));
478
+ if (isPlaceholderText(line)) {
479
+ placeholderItems.push(line.trim());
480
+ }
481
+ if (hasVerificationIntent(line)) {
482
+ verificationActions.push(line.trim());
483
+ }
484
+ verificationCommands.push(...extractVerificationCommands(line));
485
+ const executionIntent = normalizeExecutionIntent(line);
486
+ if (executionIntent) {
487
+ executionHints.push(executionIntent);
488
+ }
489
+
490
+ if (/^\s*target files\s*:\s*$/i.test(line)) {
491
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
492
+ const followLine = String(lines[cursor] || "");
493
+ if (/^\s*$/.test(followLine)) {
494
+ continue;
495
+ }
496
+ if (/^\s{0,3}##\s+/.test(followLine)) {
497
+ break;
498
+ }
499
+ if (/^\s*[A-Za-z][A-Za-z0-9 _-]{0,80}\s*:\s*$/.test(followLine)) {
500
+ break;
501
+ }
502
+ const itemMatch = followLine.match(/^\s*-\s+(.+)$/);
503
+ if (!itemMatch) {
504
+ break;
505
+ }
506
+ const entry = String(itemMatch[1] || "").trim();
507
+ if (!entry) {
508
+ continue;
509
+ }
510
+ targetFiles.push(...extractFileReferences(entry));
511
+ }
512
+ }
513
+ }
514
+
515
+ for (const item of section.checklistItems) {
516
+ rawFileReferences.push(...extractFileReferences(item.text));
517
+ if (isPlaceholderText(item.text)) {
518
+ placeholderItems.push(item.text);
519
+ }
520
+ if (hasVerificationIntent(item.text)) {
521
+ verificationActions.push(item.text);
522
+ }
523
+ verificationCommands.push(...extractVerificationCommands(item.text));
524
+ const executionIntent = normalizeExecutionIntent(item.text);
525
+ if (executionIntent) {
526
+ executionHints.push(executionIntent);
527
+ }
528
+ }
529
+
530
+ const mergedFileReferences = unique([...targetFiles, ...rawFileReferences]);
531
+ const titleExecutionIntent = normalizeExecutionIntent(section.title);
532
+ if (titleExecutionIntent) {
533
+ executionHints.push(titleExecutionIntent);
534
+ }
535
+
536
+ const joinedContent = [section.title, ...lines, ...section.checklistItems.map((item) => item.text)].join("\n");
537
+ return {
538
+ ...section,
539
+ targetFiles: unique(targetFiles),
540
+ fileReferences: mergedFileReferences,
541
+ verificationActions: unique(verificationActions.filter(Boolean)),
542
+ verificationCommands: unique(verificationCommands),
543
+ executionIntent: unique(executionHints),
544
+ reviewIntent: hasReviewIntent(joinedContent),
545
+ testingIntent: hasTestingIntent(joinedContent),
546
+ codeChangeLikely: looksLikeCodeChange(joinedContent),
547
+ placeholderItems: unique(placeholderItems.filter(Boolean))
548
+ };
549
+ }
550
+
353
551
  function parseTasksArtifact(text) {
354
552
  const lines = String(text || "").replace(/\r\n?/g, "\n").split("\n");
355
553
  const taskGroups = [];
356
554
  const checklistItems = [];
357
555
  const checkpointItems = [];
358
556
  const sections = [];
557
+ const markers = parseDisciplineMarkers(text);
359
558
  let currentSection = null;
360
559
 
361
- for (const line of lines) {
560
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
561
+ const line = lines[lineIndex];
362
562
  const groupMatch = line.match(/^\s{0,3}##\s+(\d+(?:\.\d+)*)\.\s+(.+)$/);
363
563
  if (groupMatch) {
364
564
  const group = {
365
565
  id: groupMatch[1],
366
- title: String(groupMatch[2] || "").trim()
566
+ title: String(groupMatch[2] || "").trim(),
567
+ startLine: lineIndex + 1
367
568
  };
368
569
  taskGroups.push(group);
369
570
  if (currentSection) {
370
- sections.push(currentSection);
571
+ currentSection.endLine = lineIndex;
572
+ sections.push(analyzeTaskGroup(currentSection));
371
573
  }
372
574
  currentSection = {
373
575
  id: group.id,
374
576
  title: group.title,
375
- checklistItems: []
577
+ startLine: lineIndex + 1,
578
+ endLine: lineIndex + 1,
579
+ checklistItems: [],
580
+ lines: []
376
581
  };
377
582
  continue;
378
583
  }
379
584
 
585
+ if (currentSection) {
586
+ currentSection.lines.push(line);
587
+ currentSection.endLine = lineIndex + 1;
588
+ }
589
+
380
590
  const checklistMatch = line.match(/^\s*-\s*\[([ xX])\]\s+(.+)$/);
381
591
  if (checklistMatch) {
382
592
  const checked = String(checklistMatch[1] || "").toLowerCase() === "x";
@@ -384,33 +594,60 @@ function parseTasksArtifact(text) {
384
594
  if (!textValue) {
385
595
  continue;
386
596
  }
387
- checklistItems.push({
597
+ const item = {
388
598
  checked,
389
- text: textValue
390
- });
599
+ text: textValue,
600
+ line: lineIndex + 1
601
+ };
602
+ checklistItems.push(item);
391
603
  if (currentSection) {
392
- currentSection.checklistItems.push({
393
- checked,
394
- text: textValue
395
- });
604
+ currentSection.checklistItems.push(item);
396
605
  }
397
606
  if (/checkpoint/i.test(textValue)) {
398
- checkpointItems.push({
399
- checked,
400
- text: textValue
401
- });
607
+ checkpointItems.push(item);
402
608
  }
403
609
  }
404
610
  }
405
611
  if (currentSection) {
406
- sections.push(currentSection);
612
+ sections.push(analyzeTaskGroup(currentSection));
407
613
  }
408
614
 
615
+ const sectionById = new Map(sections.map((section) => [section.id, section]));
616
+ const normalizedTaskGroups = taskGroups.map((group) => {
617
+ const section = sectionById.get(group.id);
618
+ if (!section) {
619
+ return group;
620
+ }
621
+ return {
622
+ id: group.id,
623
+ title: group.title,
624
+ startLine: group.startLine,
625
+ endLine: section.endLine,
626
+ targetFiles: section.targetFiles,
627
+ fileReferences: section.fileReferences,
628
+ verificationActions: section.verificationActions,
629
+ verificationCommands: section.verificationCommands,
630
+ executionIntent: section.executionIntent,
631
+ reviewIntent: section.reviewIntent,
632
+ testingIntent: section.testingIntent,
633
+ codeChangeLikely: section.codeChangeLikely,
634
+ placeholderItems: section.placeholderItems,
635
+ checklistItems: section.checklistItems
636
+ };
637
+ });
638
+
409
639
  return {
410
- taskGroups,
640
+ taskGroups: normalizedTaskGroups,
411
641
  checklistItems,
412
642
  checkpointItems,
413
643
  sections,
644
+ markers,
645
+ markerSummary: {
646
+ hasDesignApproval: Boolean(markers.markers[DISCIPLINE_MARKER_NAMES.designApproval]),
647
+ hasPlanSelfReview: Boolean(markers.markers[DISCIPLINE_MARKER_NAMES.planSelfReview]),
648
+ hasOperatorReviewAck: Boolean(markers.markers[DISCIPLINE_MARKER_NAMES.operatorReviewAck]),
649
+ malformedCount: markers.malformed.length
650
+ },
414
651
  text: String(text || "")
415
652
  };
416
653
  }
@@ -67,7 +67,8 @@ function resolveCodexBinaryPath(explicitPath) {
67
67
  }
68
68
 
69
69
  function normalizePositiveInt(value, fallback, minimum = 1, maximum = Number.POSITIVE_INFINITY) {
70
- const parsed = Number.parseInt(String(value || ""), 10);
70
+ const raw = value === undefined || value === null ? "" : String(value).trim();
71
+ const parsed = Number.parseInt(raw, 10);
71
72
  if (!Number.isFinite(parsed)) {
72
73
  return fallback;
73
74
  }
@@ -0,0 +1,160 @@
1
+ const path = require("path");
2
+ const { resolveChangeDir, unique } = require("./planning-parsers");
3
+ const { writeExecutionSignal } = require("./execution-signals");
4
+
5
+ const VALID_IMPLEMENTER_STATUSES = new Set([
6
+ "DONE",
7
+ "DONE_WITH_CONCERNS",
8
+ "NEEDS_CONTEXT",
9
+ "BLOCKED"
10
+ ]);
11
+
12
+ function normalizeStatus(value) {
13
+ const normalized = String(value || "").trim().toUpperCase();
14
+ if (!VALID_IMPLEMENTER_STATUSES.has(normalized)) {
15
+ throw new Error(
16
+ "`task-execution --status` must be one of DONE, DONE_WITH_CONCERNS, NEEDS_CONTEXT, BLOCKED."
17
+ );
18
+ }
19
+ return normalized;
20
+ }
21
+
22
+ function normalizeList(value) {
23
+ if (Array.isArray(value)) {
24
+ return unique(value.map((item) => String(item || "").trim()).filter(Boolean));
25
+ }
26
+ return unique(
27
+ String(value || "")
28
+ .split(/[,\n;]/)
29
+ .map((item) => item.trim())
30
+ .filter(Boolean)
31
+ );
32
+ }
33
+
34
+ function normalizeTaskGroupId(value) {
35
+ const normalized = String(value || "").trim();
36
+ if (!normalized) {
37
+ throw new Error("`task-execution` requires `--task-group <id>`.");
38
+ }
39
+ return normalized;
40
+ }
41
+
42
+ function mapImplementerStatusToSignal(status) {
43
+ if (status === "DONE") {
44
+ return "PASS";
45
+ }
46
+ if (status === "DONE_WITH_CONCERNS" || status === "NEEDS_CONTEXT") {
47
+ return "WARN";
48
+ }
49
+ return "BLOCK";
50
+ }
51
+
52
+ function buildSurface(taskGroupId) {
53
+ const safeId = String(taskGroupId || "")
54
+ .trim()
55
+ .replace(/[^A-Za-z0-9._-]+/g, "_");
56
+ return `task-execution.${safeId}`;
57
+ }
58
+
59
+ function normalizeTaskExecutionEnvelope(input = {}) {
60
+ const taskGroupId = normalizeTaskGroupId(input.taskGroupId);
61
+ const status = normalizeStatus(input.status);
62
+ const summary = String(input.summary || "").trim();
63
+ if (!summary) {
64
+ throw new Error("`task-execution` requires `--summary <text>`.");
65
+ }
66
+ const changedFiles = normalizeList(input.changedFiles);
67
+ const testEvidence = normalizeList(input.testEvidence);
68
+ const concerns = normalizeList(input.concerns);
69
+ const blockers = normalizeList(input.blockers);
70
+
71
+ return {
72
+ taskGroupId,
73
+ status,
74
+ summary,
75
+ changedFiles,
76
+ testEvidence,
77
+ concerns,
78
+ blockers,
79
+ recordedAt: new Date().toISOString()
80
+ };
81
+ }
82
+
83
+ function writeTaskExecutionEnvelope(projectPathInput, options = {}) {
84
+ const projectRoot = path.resolve(projectPathInput || process.cwd());
85
+ const requestedChangeId = options.changeId ? String(options.changeId).trim() : "";
86
+ const resolved = resolveChangeDir(projectRoot, requestedChangeId);
87
+ if (!resolved.changeDir || !resolved.changeId) {
88
+ const resolveError =
89
+ Array.isArray(resolved.failures) && resolved.failures.length > 0
90
+ ? String(resolved.failures[0] || "").trim()
91
+ : "";
92
+ throw new Error(resolveError || "Unable to resolve active change for task-execution.");
93
+ }
94
+
95
+ const envelope = normalizeTaskExecutionEnvelope(options);
96
+ const signalPath = writeExecutionSignal(projectRoot, {
97
+ changeId: resolved.changeId,
98
+ surface: buildSurface(envelope.taskGroupId),
99
+ status: mapImplementerStatusToSignal(envelope.status),
100
+ advisory: false,
101
+ strict: true,
102
+ failures: envelope.status === "BLOCKED" ? envelope.blockers : [],
103
+ warnings:
104
+ envelope.status === "DONE_WITH_CONCERNS" || envelope.status === "NEEDS_CONTEXT"
105
+ ? envelope.concerns
106
+ : [],
107
+ notes: [envelope.summary, ...envelope.testEvidence.map((item) => `test: ${item}`)],
108
+ details: {
109
+ type: "task_execution",
110
+ envelope
111
+ }
112
+ });
113
+
114
+ return {
115
+ status: mapImplementerStatusToSignal(envelope.status),
116
+ projectRoot,
117
+ changeId: resolved.changeId,
118
+ taskGroupId: envelope.taskGroupId,
119
+ implementerStatus: envelope.status,
120
+ summary: envelope.summary,
121
+ changedFiles: envelope.changedFiles,
122
+ testEvidence: envelope.testEvidence,
123
+ concerns: envelope.concerns,
124
+ blockers: envelope.blockers,
125
+ signalPath
126
+ };
127
+ }
128
+
129
+ function formatTaskExecutionReport(result) {
130
+ const lines = [
131
+ "Task execution envelope",
132
+ `Project: ${result.projectRoot}`,
133
+ `Change: ${result.changeId}`,
134
+ `Task group: ${result.taskGroupId}`,
135
+ `Implementer status: ${result.implementerStatus}`,
136
+ `Signal status: ${result.status}`,
137
+ `Summary: ${result.summary}`,
138
+ `Signal path: ${result.signalPath}`
139
+ ];
140
+ if (result.changedFiles.length > 0) {
141
+ lines.push(`Changed files: ${result.changedFiles.join(", ")}`);
142
+ }
143
+ if (result.testEvidence.length > 0) {
144
+ lines.push(`Test evidence: ${result.testEvidence.join(", ")}`);
145
+ }
146
+ if (result.concerns.length > 0) {
147
+ lines.push(`Concerns: ${result.concerns.join(", ")}`);
148
+ }
149
+ if (result.blockers.length > 0) {
150
+ lines.push(`Blockers: ${result.blockers.join(", ")}`);
151
+ }
152
+ return lines.join("\n");
153
+ }
154
+
155
+ module.exports = {
156
+ VALID_IMPLEMENTER_STATUSES,
157
+ normalizeTaskExecutionEnvelope,
158
+ writeTaskExecutionEnvelope,
159
+ formatTaskExecutionReport
160
+ };
@@ -0,0 +1,197 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { resolveChangeDir, unique } = require("./planning-parsers");
4
+ const { readExecutionSignals, writeExecutionSignal } = require("./execution-signals");
5
+ const { writeFileAtomic, readTextIfExists } = require("./utils");
6
+
7
+ const VALID_REVIEW_STAGES = new Set(["spec", "quality"]);
8
+ const VALID_REVIEW_STATUSES = new Set(["PASS", "WARN", "BLOCK"]);
9
+
10
+ function normalizeReviewStage(value) {
11
+ const normalized = String(value || "").trim().toLowerCase();
12
+ if (!VALID_REVIEW_STAGES.has(normalized)) {
13
+ throw new Error("`task-review --stage` must be `spec` or `quality`.");
14
+ }
15
+ return normalized;
16
+ }
17
+
18
+ function normalizeReviewStatus(value) {
19
+ const normalized = String(value || "").trim().toUpperCase();
20
+ if (!VALID_REVIEW_STATUSES.has(normalized)) {
21
+ throw new Error("`task-review --status` must be PASS, WARN, or BLOCK.");
22
+ }
23
+ return normalized;
24
+ }
25
+
26
+ function normalizeTaskGroupId(value) {
27
+ const normalized = String(value || "").trim();
28
+ if (!normalized) {
29
+ throw new Error("`task-review` requires `--task-group <id>`.");
30
+ }
31
+ return normalized;
32
+ }
33
+
34
+ function normalizeList(value) {
35
+ if (Array.isArray(value)) {
36
+ return unique(value.map((item) => String(item || "").trim()).filter(Boolean));
37
+ }
38
+ return unique(
39
+ String(value || "")
40
+ .split(/[,\n;]/)
41
+ .map((item) => item.trim())
42
+ .filter(Boolean)
43
+ );
44
+ }
45
+
46
+ function buildSafeTaskGroupId(taskGroupId) {
47
+ return String(taskGroupId || "")
48
+ .trim()
49
+ .replace(/[^A-Za-z0-9._-]+/g, "_");
50
+ }
51
+
52
+ function buildTaskReviewSurface(taskGroupId, stage) {
53
+ return `task-review.${buildSafeTaskGroupId(taskGroupId)}.${stage}`;
54
+ }
55
+
56
+ function mapReviewStatusToSignalStatus(status) {
57
+ return status;
58
+ }
59
+
60
+ function normalizeTaskReviewEnvelope(input = {}) {
61
+ const taskGroupId = normalizeTaskGroupId(input.taskGroupId);
62
+ const stage = normalizeReviewStage(input.stage);
63
+ const status = normalizeReviewStatus(input.status);
64
+ const summary = String(input.summary || "").trim();
65
+ if (!summary) {
66
+ throw new Error("`task-review` requires `--summary <text>`.");
67
+ }
68
+ const issues = normalizeList(input.issues);
69
+ const reviewer = String(input.reviewer || "").trim() || "unspecified";
70
+
71
+ return {
72
+ taskGroupId,
73
+ stage,
74
+ status,
75
+ summary,
76
+ issues,
77
+ reviewer,
78
+ recordedAt: new Date().toISOString()
79
+ };
80
+ }
81
+
82
+ function findLatestTaskReviewSignal(signals, taskGroupId, stage) {
83
+ const surface = buildTaskReviewSurface(taskGroupId, stage);
84
+ return (signals || [])
85
+ .filter((signal) => String(signal.surface || "") === surface)
86
+ .sort((left, right) => String(right.timestamp || "").localeCompare(String(left.timestamp || "")))[0] || null;
87
+ }
88
+
89
+ function appendTaskReviewEvidence(verificationText, envelope) {
90
+ const existing = String(verificationText || "").trim();
91
+ const section = [
92
+ `### Task Review ${envelope.taskGroupId} (${envelope.stage})`,
93
+ `- Status: ${envelope.status}`,
94
+ `- Reviewer: ${envelope.reviewer}`,
95
+ `- Summary: ${envelope.summary}`,
96
+ `- Issues: ${envelope.issues.length > 0 ? envelope.issues.join("; ") : "none"}`,
97
+ `- Recorded at: ${envelope.recordedAt}`
98
+ ].join("\n");
99
+
100
+ if (!existing) {
101
+ return `# Verification\n\n## Task Review Evidence\n\n${section}\n`;
102
+ }
103
+ if (!/^\s*##\s+Task Review Evidence\s*$/im.test(existing)) {
104
+ return `${existing}\n\n## Task Review Evidence\n\n${section}\n`;
105
+ }
106
+ return `${existing}\n\n${section}\n`;
107
+ }
108
+
109
+ function writeTaskReviewEnvelope(projectPathInput, options = {}) {
110
+ const projectRoot = path.resolve(projectPathInput || process.cwd());
111
+ const requestedChangeId = options.changeId ? String(options.changeId).trim() : "";
112
+ const resolved = resolveChangeDir(projectRoot, requestedChangeId);
113
+ if (!resolved.changeDir || !resolved.changeId) {
114
+ const resolveError =
115
+ Array.isArray(resolved.failures) && resolved.failures.length > 0
116
+ ? String(resolved.failures[0] || "").trim()
117
+ : "";
118
+ throw new Error(resolveError || "Unable to resolve active change for task-review.");
119
+ }
120
+
121
+ const envelope = normalizeTaskReviewEnvelope(options);
122
+ const existingSignals = readExecutionSignals(projectRoot, { changeId: resolved.changeId });
123
+
124
+ if (envelope.stage === "quality") {
125
+ const specSignal = findLatestTaskReviewSignal(existingSignals, envelope.taskGroupId, "spec");
126
+ if (!specSignal || String(specSignal.status || "").toUpperCase() !== "PASS") {
127
+ throw new Error(
128
+ `Quality review for task group ${envelope.taskGroupId} requires a prior spec review PASS.`
129
+ );
130
+ }
131
+ }
132
+
133
+ const signalPath = writeExecutionSignal(projectRoot, {
134
+ changeId: resolved.changeId,
135
+ surface: buildTaskReviewSurface(envelope.taskGroupId, envelope.stage),
136
+ status: mapReviewStatusToSignalStatus(envelope.status),
137
+ advisory: false,
138
+ strict: true,
139
+ failures: envelope.status === "BLOCK" ? envelope.issues : [],
140
+ warnings: envelope.status === "WARN" ? envelope.issues : [],
141
+ notes: [envelope.summary, `reviewer: ${envelope.reviewer}`],
142
+ details: {
143
+ type: "task_review",
144
+ envelope
145
+ }
146
+ });
147
+
148
+ let verificationPath = null;
149
+ if (options.writeVerification === true) {
150
+ verificationPath = path.join(resolved.changeDir, "verification.md");
151
+ fs.mkdirSync(path.dirname(verificationPath), { recursive: true });
152
+ const nextVerification = appendTaskReviewEvidence(readTextIfExists(verificationPath), envelope);
153
+ writeFileAtomic(verificationPath, nextVerification);
154
+ }
155
+
156
+ return {
157
+ status: envelope.status,
158
+ projectRoot,
159
+ changeId: resolved.changeId,
160
+ taskGroupId: envelope.taskGroupId,
161
+ stage: envelope.stage,
162
+ summary: envelope.summary,
163
+ issues: envelope.issues,
164
+ reviewer: envelope.reviewer,
165
+ signalPath,
166
+ verificationPath
167
+ };
168
+ }
169
+
170
+ function formatTaskReviewReport(result) {
171
+ const lines = [
172
+ "Task review envelope",
173
+ `Project: ${result.projectRoot}`,
174
+ `Change: ${result.changeId}`,
175
+ `Task group: ${result.taskGroupId}`,
176
+ `Stage: ${result.stage}`,
177
+ `Status: ${result.status}`,
178
+ `Reviewer: ${result.reviewer}`,
179
+ `Summary: ${result.summary}`,
180
+ `Signal path: ${result.signalPath}`
181
+ ];
182
+ if (result.issues.length > 0) {
183
+ lines.push(`Issues: ${result.issues.join("; ")}`);
184
+ }
185
+ if (result.verificationPath) {
186
+ lines.push(`Verification write-back: ${result.verificationPath}`);
187
+ }
188
+ return lines.join("\n");
189
+ }
190
+
191
+ module.exports = {
192
+ VALID_REVIEW_STAGES,
193
+ VALID_REVIEW_STATUSES,
194
+ normalizeTaskReviewEnvelope,
195
+ writeTaskReviewEnvelope,
196
+ formatTaskReviewReport
197
+ };