@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.
- package/CHANGELOG.md +15 -0
- package/README.md +12 -1
- package/README.zh-CN.md +12 -1
- package/SKILL.md +2 -2
- package/bin/design-supervisor.js +39 -0
- package/commands/claude/dv/design.md +1 -1
- package/commands/codex/prompts/dv-design.md +1 -1
- package/commands/gemini/dv/design.toml +1 -1
- package/docs/constraint-files.md +4 -1
- package/docs/dv-command-reference.md +6 -0
- package/docs/zh-CN/constraint-files.md +4 -1
- package/docs/zh-CN/dv-command-reference.md +6 -0
- package/lib/audit-parsers.js +156 -10
- package/lib/audit.js +89 -3
- package/lib/cli.js +97 -0
- package/lib/supervisor-review.js +680 -0
- package/package.json +6 -3
- package/references/artifact-templates.md +2 -0
- package/scripts/test-audit-design-supervisor.js +189 -0
- package/scripts/test-supervisor-review-cli.js +619 -0
- package/scripts/test-supervisor-review-integration.js +115 -0
|
@@ -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.
|
|
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`
|