@xenonbyte/da-vinci-workflow 0.1.23 → 0.1.24

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.
@@ -0,0 +1,680 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
4
+ const { execFile } = require("child_process");
5
+ const {
6
+ parseCheckpointStatusMap,
7
+ getConfiguredDesignSupervisorReviewers
8
+ } = require("./audit-parsers");
9
+
10
+ const VALID_STATUSES = new Set(["PASS", "WARN", "BLOCK"]);
11
+ const VALID_REVIEW_SOURCES = new Set(["skill", "manual", "inferred"]);
12
+ const REVIEW_STATUS_SEVERITY = Object.freeze({
13
+ PASS: 0,
14
+ WARN: 1,
15
+ BLOCK: 2
16
+ });
17
+
18
+ function pathExists(targetPath) {
19
+ return fs.existsSync(targetPath);
20
+ }
21
+
22
+ function escapeRegExp(value) {
23
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
24
+ }
25
+
26
+ function normalizeStatus(status) {
27
+ if (!status) {
28
+ return "";
29
+ }
30
+
31
+ const normalized = String(status).trim().toUpperCase();
32
+ if (!VALID_STATUSES.has(normalized)) {
33
+ throw new Error("`--status` must be one of PASS, WARN, BLOCK.");
34
+ }
35
+ return normalized;
36
+ }
37
+
38
+ function normalizeReviewSource(source) {
39
+ if (!source) {
40
+ return "";
41
+ }
42
+ const normalized = String(source).trim().toLowerCase();
43
+ if (!VALID_REVIEW_SOURCES.has(normalized)) {
44
+ throw new Error("`--source` must be one of skill, manual, inferred.");
45
+ }
46
+ return normalized;
47
+ }
48
+
49
+ function readTextIfExists(filePath) {
50
+ if (!pathExists(filePath)) {
51
+ return "";
52
+ }
53
+ return fs.readFileSync(filePath, "utf8");
54
+ }
55
+
56
+ function parseReviewerList(value) {
57
+ return String(value || "")
58
+ .split(/[,\n;]/)
59
+ .map((token) => token.replace(/`/g, "").trim())
60
+ .filter(Boolean);
61
+ }
62
+
63
+ function resolveCodexBinaryPath(explicitPath) {
64
+ const candidate = String(explicitPath || process.env.DA_VINCI_CODEX_BIN || "codex").trim();
65
+ if (!candidate) {
66
+ throw new Error("Unable to resolve Codex binary path.");
67
+ }
68
+ return candidate;
69
+ }
70
+
71
+ function normalizePositiveInt(value, fallback, minimum = 1, maximum = Number.POSITIVE_INFINITY) {
72
+ const parsed = Number.parseInt(String(value || ""), 10);
73
+ if (!Number.isFinite(parsed)) {
74
+ return fallback;
75
+ }
76
+ return Math.max(minimum, Math.min(maximum, parsed));
77
+ }
78
+
79
+ function collectScreenshotPaths(projectRoot, changeId, options = {}) {
80
+ if (!changeId) {
81
+ return [];
82
+ }
83
+
84
+ const exportsDir = path.join(projectRoot, ".da-vinci", "changes", changeId, "exports");
85
+ if (!pathExists(exportsDir)) {
86
+ return [];
87
+ }
88
+
89
+ const entries = fs.readdirSync(exportsDir, { withFileTypes: true });
90
+ const imageFiles = entries
91
+ .filter((entry) => entry.isFile())
92
+ .map((entry) => path.join(exportsDir, entry.name))
93
+ .filter((filePath) => /\.(png|jpe?g|webp)$/i.test(filePath))
94
+ .sort((a, b) => a.localeCompare(b));
95
+
96
+ const maxImagesRaw = Number.parseInt(String(options.maxImages || ""), 10);
97
+ const maxImages = Number.isFinite(maxImagesRaw) && maxImagesRaw > 0 ? maxImagesRaw : 6;
98
+ return imageFiles.slice(0, maxImages);
99
+ }
100
+
101
+ function buildReviewerPrompt({ reviewer, projectRoot, changeId, pencilDesignPath }) {
102
+ const artifactPath = pencilDesignPath || "(unresolved pencil-design.md)";
103
+ return [
104
+ `Use the \`${reviewer}\` skill to review current design screenshots.`,
105
+ `Project root: ${projectRoot}`,
106
+ `Change id: ${changeId || "(unspecified)"}`,
107
+ `Target artifact: ${artifactPath}`,
108
+ "Return strict JSON with keys:",
109
+ "- status: PASS | WARN | BLOCK",
110
+ "- issues: string[]",
111
+ "- revision_outcome: string",
112
+ "Do not return markdown."
113
+ ].join("\n");
114
+ }
115
+
116
+ function parseReviewerPayload(rawText) {
117
+ const payload = JSON.parse(String(rawText || "").trim());
118
+ const status = normalizeStatus(payload.status);
119
+ const issues = Array.isArray(payload.issues)
120
+ ? payload.issues.map((issue) => String(issue || "").trim()).filter(Boolean)
121
+ : [];
122
+ const revisionOutcome = String(payload.revision_outcome || payload.revisionOutcome || "").trim();
123
+ return {
124
+ status,
125
+ issues,
126
+ revisionOutcome: revisionOutcome || "approved"
127
+ };
128
+ }
129
+
130
+ function execFileAsync(command, args, options = {}) {
131
+ return new Promise((resolve, reject) => {
132
+ execFile(command, args, options, (error, stdout, stderr) => {
133
+ if (error) {
134
+ error.stdout = stdout;
135
+ error.stderr = stderr;
136
+ reject(error);
137
+ return;
138
+ }
139
+ resolve({
140
+ stdout,
141
+ stderr
142
+ });
143
+ });
144
+ });
145
+ }
146
+
147
+ function sleep(delayMs) {
148
+ if (!Number.isFinite(delayMs) || delayMs <= 0) {
149
+ return Promise.resolve();
150
+ }
151
+ return new Promise((resolve) => {
152
+ setTimeout(resolve, delayMs);
153
+ });
154
+ }
155
+
156
+ async function runReviewerWithCodexOnce(options = {}) {
157
+ const {
158
+ codexBin,
159
+ reviewer,
160
+ projectRoot,
161
+ changeId,
162
+ pencilDesignPath,
163
+ screenshotPaths,
164
+ timeoutMs
165
+ } = options;
166
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "da-vinci-supervisor-runner-"));
167
+ const outputPath = path.join(tmpDir, "review.json");
168
+ const schemaPath = path.join(tmpDir, "schema.json");
169
+ try {
170
+ fs.writeFileSync(
171
+ schemaPath,
172
+ JSON.stringify(
173
+ {
174
+ type: "object",
175
+ required: ["status", "issues", "revision_outcome"],
176
+ additionalProperties: false,
177
+ properties: {
178
+ status: {
179
+ type: "string",
180
+ enum: ["PASS", "WARN", "BLOCK"]
181
+ },
182
+ issues: {
183
+ type: "array",
184
+ items: {
185
+ type: "string"
186
+ }
187
+ },
188
+ revision_outcome: {
189
+ type: "string"
190
+ }
191
+ }
192
+ },
193
+ null,
194
+ 2
195
+ )
196
+ );
197
+
198
+ const prompt = buildReviewerPrompt({
199
+ reviewer,
200
+ projectRoot,
201
+ changeId,
202
+ pencilDesignPath
203
+ });
204
+
205
+ const args = [
206
+ "exec",
207
+ "--ephemeral",
208
+ "--skip-git-repo-check",
209
+ "-C",
210
+ projectRoot,
211
+ "--sandbox",
212
+ "read-only",
213
+ "--output-schema",
214
+ schemaPath,
215
+ "--output-last-message",
216
+ outputPath
217
+ ];
218
+ for (const screenshotPath of screenshotPaths || []) {
219
+ args.push("-i", screenshotPath);
220
+ }
221
+ args.push(prompt);
222
+
223
+ const timeoutValue = normalizePositiveInt(timeoutMs, 0, 0, 30 * 60 * 1000);
224
+ try {
225
+ await execFileAsync(codexBin, args, {
226
+ encoding: "utf8",
227
+ maxBuffer: 8 * 1024 * 1024,
228
+ timeout: timeoutValue > 0 ? timeoutValue : undefined
229
+ });
230
+ } catch (error) {
231
+ const stdout = String(error.stdout || "").trim();
232
+ const stderr = String(error.stderr || "").trim();
233
+ const timeoutHint = error.killed ? " (timed out)" : "";
234
+ const message = stderr || stdout || error.message || "unknown error";
235
+ throw new Error(`Reviewer \`${reviewer}\` failed${timeoutHint}: ${message}`);
236
+ }
237
+
238
+ if (!pathExists(outputPath)) {
239
+ throw new Error(`Reviewer \`${reviewer}\` completed but produced no output JSON.`);
240
+ }
241
+
242
+ return parseReviewerPayload(fs.readFileSync(outputPath, "utf8"));
243
+ } finally {
244
+ fs.rmSync(tmpDir, { recursive: true, force: true });
245
+ }
246
+ }
247
+
248
+ async function runReviewerWithCodex(options = {}) {
249
+ const reviewer = String(options.reviewer || "").trim();
250
+ const retries = normalizePositiveInt(options.retries, 1, 0, 8);
251
+ const retryDelayMs = normalizePositiveInt(options.retryDelayMs, 400, 0, 60000);
252
+ const maxAttempts = retries + 1;
253
+ let attempt = 0;
254
+ let lastError = null;
255
+
256
+ while (attempt < maxAttempts) {
257
+ attempt += 1;
258
+ try {
259
+ const finding = await runReviewerWithCodexOnce(options);
260
+ return {
261
+ ...finding,
262
+ attempts: attempt
263
+ };
264
+ } catch (error) {
265
+ lastError = error;
266
+ if (attempt >= maxAttempts) {
267
+ break;
268
+ }
269
+ const backoffDelay = retryDelayMs * Math.pow(2, attempt - 1);
270
+ await sleep(backoffDelay);
271
+ }
272
+ }
273
+
274
+ throw new Error(
275
+ `Reviewer \`${reviewer}\` failed after ${maxAttempts} attempt(s): ${lastError ? lastError.message : "unknown error"}`
276
+ );
277
+ }
278
+
279
+ async function runReviewersInPool(reviewers, worker, concurrency) {
280
+ const queue = Array.isArray(reviewers) ? reviewers.slice() : [];
281
+ if (queue.length === 0) {
282
+ return [];
283
+ }
284
+
285
+ const findings = new Array(queue.length);
286
+ const workerCount = Math.min(normalizePositiveInt(concurrency, 2, 1, 16), queue.length);
287
+ let cursor = 0;
288
+
289
+ async function runWorker() {
290
+ while (true) {
291
+ const index = cursor;
292
+ cursor += 1;
293
+ if (index >= queue.length) {
294
+ return;
295
+ }
296
+ findings[index] = await worker(queue[index], index);
297
+ }
298
+ }
299
+
300
+ const workers = [];
301
+ for (let i = 0; i < workerCount; i += 1) {
302
+ workers.push(runWorker());
303
+ }
304
+ await Promise.all(workers);
305
+ return findings;
306
+ }
307
+
308
+ function aggregateReviewerFindings(findings, options = {}) {
309
+ if (!findings || findings.length === 0) {
310
+ return {
311
+ status: "BLOCK",
312
+ issueList: "No reviewer findings were produced.",
313
+ revisionOutcome: "blocked pending required fixes"
314
+ };
315
+ }
316
+
317
+ let finalStatus = "PASS";
318
+ for (const finding of findings) {
319
+ if (REVIEW_STATUS_SEVERITY[finding.status] > REVIEW_STATUS_SEVERITY[finalStatus]) {
320
+ finalStatus = finding.status;
321
+ }
322
+ }
323
+
324
+ const issueListParts = [];
325
+ for (const finding of findings) {
326
+ if (!finding.issues || finding.issues.length === 0) {
327
+ continue;
328
+ }
329
+ issueListParts.push(`${finding.reviewer}: ${finding.issues.join("; ")}`);
330
+ }
331
+ const issueList = issueListParts.length > 0 ? issueListParts.join(" | ") : "none";
332
+
333
+ let revisionOutcome = "approved";
334
+ if (finalStatus === "WARN") {
335
+ revisionOutcome = options.acceptWarn
336
+ ? "accepted with follow-up"
337
+ : "warn requires follow-up before completion";
338
+ } else if (finalStatus === "BLOCK") {
339
+ revisionOutcome = "blocked pending required fixes";
340
+ }
341
+
342
+ return {
343
+ status: finalStatus,
344
+ issueList,
345
+ revisionOutcome
346
+ };
347
+ }
348
+
349
+ function resolvePencilDesignPath(projectRoot, options = {}) {
350
+ if (options.pencilDesignPath) {
351
+ const explicitPath = path.resolve(projectRoot, options.pencilDesignPath);
352
+ return {
353
+ path: explicitPath,
354
+ changeId: options.changeId || null
355
+ };
356
+ }
357
+
358
+ const changeId = String(options.changeId || "").trim();
359
+ if (!changeId) {
360
+ throw new Error(
361
+ "`supervisor-review` requires either `--change <id>` or `--pencil-design <path>`."
362
+ );
363
+ }
364
+
365
+ return {
366
+ path: path.join(projectRoot, ".da-vinci", "changes", changeId, "pencil-design.md"),
367
+ changeId
368
+ };
369
+ }
370
+
371
+ function extractMcpRuntimeGateStatus(markdownText) {
372
+ const sectionMatch = String(markdownText || "").match(
373
+ /(?:^|\n)##\s+MCP Runtime Gate\s*\n([\s\S]*?)(?=\n##\s+|\s*$)/i
374
+ );
375
+ if (!sectionMatch) {
376
+ return "";
377
+ }
378
+
379
+ const statusMatch = String(sectionMatch[1]).match(
380
+ /(?:^|\n)\s*-\s*(?:Status|状态)\s*:\s*`?(PASS|WARN|BLOCK)`?\b/i
381
+ );
382
+ return statusMatch ? String(statusMatch[1]).toUpperCase() : "";
383
+ }
384
+
385
+ function extractCheckpointStatus(markdownText, checkpointLabel) {
386
+ const escapedLabel = escapeRegExp(checkpointLabel);
387
+ const match = String(markdownText || "").match(
388
+ new RegExp(`(?:^|\\n)\\s*-\\s*\`?${escapedLabel}\`?\\s*:\\s*\`?(PASS|WARN|BLOCK)\`?\\b`, "i")
389
+ );
390
+ return match ? String(match[1]).toUpperCase() : "";
391
+ }
392
+
393
+ function inferReview(markdownText, options = {}) {
394
+ const issues = [];
395
+ const checkpointStatuses = parseCheckpointStatusMap(markdownText);
396
+ const designCheckpointStatus =
397
+ checkpointStatuses["design checkpoint"] || extractCheckpointStatus(markdownText, "Design checkpoint");
398
+ const runtimeGateStatus = extractMcpRuntimeGateStatus(markdownText);
399
+ const hasScreenshotRecords = /(?:^|\n)##\s+Screenshot Review Records\b/i.test(
400
+ String(markdownText || "")
401
+ );
402
+ let status = "PASS";
403
+
404
+ if (!hasScreenshotRecords) {
405
+ issues.push("Missing `## Screenshot Review Records` evidence.");
406
+ status = "BLOCK";
407
+ }
408
+
409
+ if (designCheckpointStatus === "BLOCK") {
410
+ issues.push("`Design checkpoint` is BLOCK.");
411
+ status = "BLOCK";
412
+ } else if (designCheckpointStatus === "WARN" && status !== "BLOCK") {
413
+ issues.push("`Design checkpoint` is WARN.");
414
+ status = "WARN";
415
+ } else if (!designCheckpointStatus && status !== "BLOCK") {
416
+ issues.push("`Design checkpoint` status is not recorded.");
417
+ status = "WARN";
418
+ }
419
+
420
+ if (runtimeGateStatus === "BLOCK") {
421
+ issues.push("`MCP Runtime Gate` is BLOCK.");
422
+ status = "BLOCK";
423
+ } else if (runtimeGateStatus === "WARN" && status !== "BLOCK") {
424
+ issues.push("`MCP Runtime Gate` is WARN.");
425
+ status = "WARN";
426
+ }
427
+
428
+ const issueList = issues.length > 0 ? issues.join(" ") : "none";
429
+ let revisionOutcome = "approved";
430
+ if (status === "WARN") {
431
+ revisionOutcome = options.acceptWarn
432
+ ? "accepted with follow-up"
433
+ : "warn requires follow-up before completion";
434
+ } else if (status === "BLOCK") {
435
+ revisionOutcome = "blocked pending required fixes";
436
+ }
437
+
438
+ return {
439
+ status,
440
+ issueList,
441
+ revisionOutcome,
442
+ source: "inferred",
443
+ executedReviewers: [],
444
+ configuredReviewers: options.configuredReviewers || []
445
+ };
446
+ }
447
+
448
+ function hasAcceptedWarn(revisionOutcome) {
449
+ return /(accepted|accepted with follow-up|accepted warning|warn accepted|接受|已接受|接受警告)/i.test(
450
+ String(revisionOutcome || "")
451
+ );
452
+ }
453
+
454
+ function buildReviewSection(review) {
455
+ const now = new Date().toISOString();
456
+ const configuredReviewersText =
457
+ review.configuredReviewers && review.configuredReviewers.length > 0
458
+ ? review.configuredReviewers.join(", ")
459
+ : "none";
460
+ const executedReviewersText =
461
+ review.executedReviewers && review.executedReviewers.length > 0
462
+ ? review.executedReviewers.join(", ")
463
+ : "none";
464
+ return [
465
+ `## Design-Supervisor Review (CLI ${now})`,
466
+ "- Review runner: `da-vinci supervisor-review`",
467
+ `- Configured reviewers: ${configuredReviewersText}`,
468
+ `- Executed reviewers: ${executedReviewersText}`,
469
+ `- Review source: ${review.source}`,
470
+ `- Status: ${review.status}`,
471
+ `- Issue list: ${review.issueList}`,
472
+ `- Revision outcome: ${review.revisionOutcome}`
473
+ ].join("\n");
474
+ }
475
+
476
+ function appendReviewSection(existingText, review) {
477
+ const section = buildReviewSection(review);
478
+ const base = String(existingText || "").trim();
479
+ if (!base) {
480
+ return `# Pencil Design\n\n${section}\n`;
481
+ }
482
+ return `${base}\n\n${section}\n`;
483
+ }
484
+
485
+ async function runDesignSupervisorReview(options = {}) {
486
+ const projectRoot = path.resolve(options.projectPath || process.cwd());
487
+ if (!pathExists(projectRoot)) {
488
+ throw new Error(`Project path does not exist: ${projectRoot}`);
489
+ }
490
+
491
+ const resolved = resolvePencilDesignPath(projectRoot, {
492
+ pencilDesignPath: options.pencilDesignPath,
493
+ changeId: options.changeId
494
+ });
495
+ const pencilDesignPath = resolved.path;
496
+ const changeId = resolved.changeId;
497
+ const existingText = readTextIfExists(pencilDesignPath);
498
+ const daVinciText = readTextIfExists(path.join(projectRoot, "DA-VINCI.md"));
499
+ const configuredReviewers = options.configuredReviewers
500
+ ? parseReviewerList(options.configuredReviewers)
501
+ : getConfiguredDesignSupervisorReviewers(daVinciText);
502
+ const executedReviewersInput = parseReviewerList(options.executedReviewers);
503
+ const explicitSource = normalizeReviewSource(options.source);
504
+ const runReviewers = options.runReviewers === true;
505
+ const reviewConcurrency = normalizePositiveInt(options.reviewConcurrency, 2, 1, 16);
506
+ const reviewerRetries = normalizePositiveInt(options.reviewerRetries, 1, 0, 8);
507
+ const reviewerRetryDelayMs = normalizePositiveInt(options.reviewerRetryDelayMs, 400, 0, 60000);
508
+ const manualStatus = normalizeStatus(options.status);
509
+ const reviewerAttempts = {};
510
+ let review = null;
511
+
512
+ if (runReviewers) {
513
+ if (configuredReviewers.length === 0) {
514
+ throw new Error(
515
+ "Cannot run reviewers automatically because `Design-supervisor reviewers` is not configured."
516
+ );
517
+ }
518
+
519
+ const codexBin = resolveCodexBinaryPath(options.codexBin);
520
+ const screenshotPaths = collectScreenshotPaths(projectRoot, changeId, {
521
+ maxImages: options.maxImages
522
+ });
523
+ const findings = await runReviewersInPool(
524
+ configuredReviewers,
525
+ async (reviewer) => {
526
+ const finding = await runReviewerWithCodex({
527
+ codexBin,
528
+ reviewer,
529
+ projectRoot,
530
+ changeId,
531
+ pencilDesignPath,
532
+ screenshotPaths,
533
+ timeoutMs: options.reviewerTimeoutMs,
534
+ retries: reviewerRetries,
535
+ retryDelayMs: reviewerRetryDelayMs
536
+ });
537
+ reviewerAttempts[reviewer] = finding.attempts || 1;
538
+ return {
539
+ reviewer,
540
+ ...finding
541
+ };
542
+ },
543
+ reviewConcurrency
544
+ );
545
+
546
+ const aggregated = aggregateReviewerFindings(findings, {
547
+ acceptWarn: options.acceptWarn
548
+ });
549
+ review = {
550
+ status: aggregated.status,
551
+ issueList: aggregated.issueList,
552
+ revisionOutcome: aggregated.revisionOutcome,
553
+ source: "skill",
554
+ configuredReviewers: [...configuredReviewers],
555
+ executedReviewers: [...configuredReviewers]
556
+ };
557
+ }
558
+
559
+ if (!review && manualStatus) {
560
+ const issueList = String(options.issueList || "").trim() || "none";
561
+ let revisionOutcome = String(options.revisionOutcome || "").trim();
562
+ if (!revisionOutcome) {
563
+ if (manualStatus === "PASS") {
564
+ revisionOutcome = "approved";
565
+ } else if (manualStatus === "WARN") {
566
+ revisionOutcome = options.acceptWarn
567
+ ? "accepted with follow-up"
568
+ : "warn requires follow-up before completion";
569
+ } else {
570
+ revisionOutcome = "blocked pending required fixes";
571
+ }
572
+ }
573
+
574
+ let source = explicitSource;
575
+ if (!source) {
576
+ source = executedReviewersInput.length > 0 ? "skill" : "manual";
577
+ }
578
+ const executedReviewers =
579
+ source === "skill" && executedReviewersInput.length === 0
580
+ ? [...configuredReviewers]
581
+ : executedReviewersInput;
582
+
583
+ review = {
584
+ status: manualStatus,
585
+ issueList,
586
+ revisionOutcome,
587
+ source,
588
+ configuredReviewers: [...configuredReviewers],
589
+ executedReviewers
590
+ };
591
+ } else if (!review) {
592
+ review = inferReview(existingText, {
593
+ acceptWarn: options.acceptWarn,
594
+ configuredReviewers
595
+ });
596
+ if (explicitSource) {
597
+ review.source = explicitSource;
598
+ }
599
+ if (review.source === "skill") {
600
+ review.executedReviewers =
601
+ executedReviewersInput.length > 0 ? executedReviewersInput : [...configuredReviewers];
602
+ } else if (review.source !== "inferred") {
603
+ review.executedReviewers = executedReviewersInput;
604
+ }
605
+ }
606
+
607
+ let wrote = false;
608
+ if (options.write) {
609
+ fs.mkdirSync(path.dirname(pencilDesignPath), { recursive: true });
610
+ const nextText = appendReviewSection(existingText, review);
611
+ fs.writeFileSync(pencilDesignPath, nextText);
612
+ wrote = true;
613
+ }
614
+
615
+ return {
616
+ projectRoot,
617
+ changeId,
618
+ pencilDesignPath,
619
+ status: review.status,
620
+ issueList: review.issueList,
621
+ revisionOutcome: review.revisionOutcome,
622
+ acceptedWarn: review.status === "WARN" && hasAcceptedWarn(review.revisionOutcome),
623
+ source: review.source,
624
+ configuredReviewers: review.configuredReviewers || [],
625
+ executedReviewers: review.executedReviewers || [],
626
+ runReviewers,
627
+ reviewConcurrency: runReviewers ? reviewConcurrency : 0,
628
+ reviewerRetries: runReviewers ? reviewerRetries : 0,
629
+ reviewerRetryDelayMs: runReviewers ? reviewerRetryDelayMs : 0,
630
+ reviewerAttempts: runReviewers ? reviewerAttempts : {},
631
+ wrote
632
+ };
633
+ }
634
+
635
+ function formatDesignSupervisorReviewReport(result) {
636
+ const lines = [
637
+ "Design-supervisor review",
638
+ `Project: ${result.projectRoot}`,
639
+ `Change: ${result.changeId || "(unspecified)"}`,
640
+ `Artifact: ${result.pencilDesignPath}`,
641
+ `Status: ${result.status}`,
642
+ `Issue list: ${result.issueList}`,
643
+ `Revision outcome: ${result.revisionOutcome}`,
644
+ `Accepted WARN: ${result.acceptedWarn ? "yes" : "no"}`,
645
+ `Source: ${result.source}`,
646
+ `Configured reviewers: ${
647
+ result.configuredReviewers.length > 0 ? result.configuredReviewers.join(", ") : "none"
648
+ }`,
649
+ `Executed reviewers: ${
650
+ result.executedReviewers.length > 0 ? result.executedReviewers.join(", ") : "none"
651
+ }`,
652
+ `Auto reviewer run: ${result.runReviewers ? "yes" : "no"}`,
653
+ `Artifact updated: ${result.wrote ? "yes" : "no"}`
654
+ ];
655
+
656
+ if (result.runReviewers) {
657
+ lines.push(`Review concurrency: ${result.reviewConcurrency}`);
658
+ lines.push(`Reviewer retries: ${result.reviewerRetries}`);
659
+ lines.push(`Reviewer retry delay (ms): ${result.reviewerRetryDelayMs}`);
660
+ const reviewerAttemptTokens = Object.entries(result.reviewerAttempts || {}).map(
661
+ ([reviewer, attempts]) => `${reviewer}:${attempts}`
662
+ );
663
+ lines.push(
664
+ `Reviewer attempts: ${reviewerAttemptTokens.length > 0 ? reviewerAttemptTokens.join(", ") : "none"}`
665
+ );
666
+ }
667
+
668
+ if (result.status === "BLOCK") {
669
+ lines.push("Gate hint: BLOCK should be treated as a completion blocker when required.");
670
+ } else if (result.status === "WARN" && !result.acceptedWarn) {
671
+ lines.push("Gate hint: WARN is not accepted yet; completion may still block when required.");
672
+ }
673
+
674
+ return lines.join("\n");
675
+ }
676
+
677
+ module.exports = {
678
+ runDesignSupervisorReview,
679
+ formatDesignSupervisorReviewReport
680
+ };
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@xenonbyte/da-vinci-workflow",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "description": "Requirement-to-design-to-code workflow skill for Codex, Claude, and Gemini",
5
5
  "bin": {
6
- "da-vinci": "bin/da-vinci.js"
6
+ "da-vinci": "bin/da-vinci.js",
7
+ "design-supervisor": "bin/design-supervisor.js"
7
8
  },
8
9
  "files": [
9
10
  "SKILL.md",
@@ -34,7 +35,9 @@
34
35
  "test:pen-persistence": "node scripts/test-pen-persistence.js",
35
36
  "test:icon-search": "node scripts/test-icon-search.js",
36
37
  "test:icon-sync": "node scripts/test-icon-sync.js",
37
- "test:icon-aliases": "node scripts/test-icon-aliases.js"
38
+ "test:icon-aliases": "node scripts/test-icon-aliases.js",
39
+ "test:supervisor-review-cli": "node scripts/test-supervisor-review-cli.js",
40
+ "test:supervisor-review-integration": "node scripts/test-supervisor-review-integration.js"
38
41
  },
39
42
  "engines": {
40
43
  "node": ">=18"
@@ -559,6 +559,8 @@ Use this structure:
559
559
 
560
560
  ## Design-Supervisor Review
561
561
  - Configured reviewers
562
+ - Executed reviewers
563
+ - Review source (`skill` / `manual` / `inferred`)
562
564
  - Review mode
563
565
  - Review inputs
564
566
  - Whether reviewers were distinct from `Preferred adapters`