@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.
- package/CHANGELOG.md +13 -0
- package/README.md +32 -7
- package/README.zh-CN.md +151 -7
- package/commands/claude/dv/build.md +5 -0
- package/commands/claude/dv/continue.md +4 -0
- package/commands/claude/dv/tasks.md +6 -0
- package/commands/claude/dv/verify.md +2 -0
- package/commands/codex/prompts/dv-build.md +5 -0
- package/commands/codex/prompts/dv-continue.md +4 -0
- package/commands/codex/prompts/dv-tasks.md +6 -0
- package/commands/codex/prompts/dv-verify.md +2 -0
- package/commands/gemini/dv/build.toml +5 -0
- package/commands/gemini/dv/continue.toml +4 -0
- package/commands/gemini/dv/tasks.toml +6 -0
- package/commands/gemini/dv/verify.toml +2 -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 +18 -2
- package/docs/execution-chain-migration.md +23 -0
- package/docs/prompt-entrypoints.md +5 -0
- package/docs/skill-usage.md +16 -0
- package/docs/workflow-overview.md +17 -0
- package/docs/zh-CN/dv-command-reference.md +16 -2
- package/docs/zh-CN/execution-chain-migration.md +23 -0
- package/docs/zh-CN/prompt-entrypoints.md +5 -0
- package/docs/zh-CN/skill-usage.md +16 -0
- package/docs/zh-CN/workflow-overview.md +17 -0
- package/lib/audit-parsers.js +148 -1
- package/lib/cli.js +106 -1
- 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/supervisor-review.js +2 -1
- package/lib/task-execution.js +160 -0
- package/lib/task-review.js +197 -0
- package/lib/verify.js +152 -1
- package/lib/workflow-state.js +452 -30
- package/lib/worktree-preflight.js +214 -0
- package/package.json +1 -1
- package/references/artifact-templates.md +56 -6
package/lib/planning-parsers.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const crypto = require("crypto");
|
|
4
|
-
const {
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/lib/supervisor-review.js
CHANGED
|
@@ -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
|
|
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
|
+
};
|