@xenonbyte/da-vinci-workflow 0.1.25 → 0.1.26
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 +23 -0
- package/README.md +39 -10
- package/README.zh-CN.md +28 -10
- package/SKILL.md +3 -0
- package/commands/claude/dv/design.md +1 -0
- package/commands/codex/prompts/dv-design.md +1 -0
- package/commands/gemini/dv/design.toml +1 -0
- package/docs/dv-command-reference.md +14 -2
- package/docs/pencil-rendering-workflow.md +9 -7
- package/docs/prompt-presets/README.md +4 -0
- package/docs/visual-assist-presets/README.md +4 -0
- package/docs/workflow-examples.md +13 -11
- package/docs/workflow-overview.md +2 -0
- package/docs/zh-CN/dv-command-reference.md +14 -2
- package/docs/zh-CN/pencil-rendering-workflow.md +9 -7
- package/docs/zh-CN/prompt-presets/README.md +5 -1
- package/docs/zh-CN/visual-assist-presets/README.md +5 -1
- package/docs/zh-CN/workflow-examples.md +13 -11
- package/docs/zh-CN/workflow-overview.md +2 -0
- package/examples/greenfield-spec-markupflow/README.md +6 -1
- package/lib/async-offload-worker.js +26 -0
- package/lib/async-offload.js +82 -0
- package/lib/audit-parsers.js +152 -32
- package/lib/audit.js +84 -23
- package/lib/cli.js +749 -433
- package/lib/fs-safety.js +1 -4
- package/lib/icon-aliases.js +7 -7
- package/lib/icon-search.js +21 -14
- package/lib/icon-sync.js +220 -41
- package/lib/install.js +128 -60
- package/lib/mcp-runtime-gate.js +4 -7
- package/lib/pen-persistence.js +318 -46
- package/lib/pencil-lock.js +237 -25
- package/lib/pencil-preflight.js +233 -12
- package/lib/pencil-session.js +216 -36
- package/lib/supervisor-review.js +56 -34
- package/lib/utils.js +121 -0
- package/lib/workflow-bootstrap.js +255 -0
- package/package.json +13 -3
- package/references/checkpoints.md +2 -0
- package/references/design-inputs.md +2 -0
- package/references/pencil-design-to-code.md +2 -0
- package/scripts/fixtures/complex-sample.pen +0 -295
- package/scripts/fixtures/mock-pencil.js +0 -49
- package/scripts/test-audit-context-delta.js +0 -446
- package/scripts/test-audit-design-supervisor.js +0 -691
- package/scripts/test-audit-safety.js +0 -92
- package/scripts/test-icon-aliases.js +0 -96
- package/scripts/test-icon-search.js +0 -77
- package/scripts/test-icon-sync.js +0 -178
- package/scripts/test-mcp-runtime-gate.js +0 -287
- package/scripts/test-mode-consistency.js +0 -344
- package/scripts/test-pen-persistence.js +0 -403
- package/scripts/test-pencil-lock.js +0 -130
- package/scripts/test-pencil-preflight.js +0 -169
- package/scripts/test-pencil-session.js +0 -192
- package/scripts/test-persistence-flows.js +0 -345
- package/scripts/test-supervisor-review-cli.js +0 -619
- package/scripts/test-supervisor-review-integration.js +0 -115
package/lib/pencil-session.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
+
const { readJsonFile } = require("./utils");
|
|
3
4
|
const {
|
|
4
5
|
ensurePenFile,
|
|
5
6
|
writePenFromPayloadFiles,
|
|
6
7
|
comparePenSync,
|
|
8
|
+
comparePenBaselineAlignment,
|
|
9
|
+
formatPenBaselineAlignmentReport,
|
|
10
|
+
syncPenSource,
|
|
7
11
|
writeJsonFileAtomic
|
|
8
12
|
} = require("./pen-persistence");
|
|
9
13
|
const {
|
|
@@ -11,6 +15,7 @@ const {
|
|
|
11
15
|
releasePencilLock,
|
|
12
16
|
getPencilLockStatus
|
|
13
17
|
} = require("./pencil-lock");
|
|
18
|
+
const { runModuleExportInWorker } = require("./async-offload");
|
|
14
19
|
|
|
15
20
|
function resolveProjectRoot(projectPath) {
|
|
16
21
|
return path.resolve(projectPath || process.cwd());
|
|
@@ -25,7 +30,7 @@ function readSessionState(projectPath) {
|
|
|
25
30
|
if (!fs.existsSync(sessionStatePath)) {
|
|
26
31
|
return null;
|
|
27
32
|
}
|
|
28
|
-
return
|
|
33
|
+
return readJsonFile(sessionStatePath, `Pencil session state JSON at ${sessionStatePath}`);
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
function writeSessionState(projectPath, payload) {
|
|
@@ -59,42 +64,170 @@ function assertLockHeldByProject(projectPath, options = {}) {
|
|
|
59
64
|
return status;
|
|
60
65
|
}
|
|
61
66
|
|
|
67
|
+
function releaseLockQuietly(projectPath, options = {}) {
|
|
68
|
+
try {
|
|
69
|
+
return releasePencilLock({
|
|
70
|
+
projectPath,
|
|
71
|
+
homeDir: options.homeDir,
|
|
72
|
+
force: options.force
|
|
73
|
+
});
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
lockPath: null,
|
|
77
|
+
released: false,
|
|
78
|
+
hadLock: null,
|
|
79
|
+
lock: null,
|
|
80
|
+
releaseError: error
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function createSessionLifecycleError(message, cause, extra = {}) {
|
|
86
|
+
const wrapped = new Error(message);
|
|
87
|
+
if (cause && typeof cause === "object") {
|
|
88
|
+
wrapped.cause = cause;
|
|
89
|
+
}
|
|
90
|
+
Object.assign(wrapped, extra);
|
|
91
|
+
return wrapped;
|
|
92
|
+
}
|
|
93
|
+
|
|
62
94
|
function beginPencilSession(options) {
|
|
63
95
|
const projectRoot = resolveProjectRoot(options.projectPath);
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
96
|
+
const baselinePaths = Array.isArray(options.baselinePaths) ? options.baselinePaths : [];
|
|
97
|
+
const hasBaselinePaths = baselinePaths.length > 0;
|
|
98
|
+
const hasPreferredSource = Boolean(options.preferredSource);
|
|
99
|
+
const shouldSyncPreferredSource = options.syncPreferredSource === true;
|
|
100
|
+
|
|
101
|
+
if (hasPreferredSource && !hasBaselinePaths) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
"Baseline preference requires comparison paths. Provide at least one `--baseline <path>`."
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (shouldSyncPreferredSource && !hasPreferredSource) {
|
|
108
|
+
throw new Error("`--sync-preferred-source` requires `--prefer-source <path>`.");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (shouldSyncPreferredSource && !hasBaselinePaths) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
"`--sync-preferred-source` requires baseline comparison input. Provide `--baseline <path>`."
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
70
117
|
const lockResult = acquirePencilLock({
|
|
71
118
|
projectPath: projectRoot,
|
|
72
119
|
owner: options.owner,
|
|
73
120
|
waitMs: options.waitMs,
|
|
74
121
|
homeDir: options.homeDir
|
|
75
122
|
});
|
|
123
|
+
let ensureResult = null;
|
|
124
|
+
let baselineCheck = null;
|
|
125
|
+
let baselineSync = null;
|
|
76
126
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
lastPersistedHash: ensureResult.state.snapshotHash,
|
|
85
|
-
lastPersistedAt: ensureResult.state.persistedAt,
|
|
86
|
-
lastTopLevelCount: ensureResult.state.topLevelCount
|
|
87
|
-
});
|
|
88
|
-
const sessionStatePath = writeSessionState(projectRoot, session);
|
|
127
|
+
try {
|
|
128
|
+
ensureResult = ensurePenFile({
|
|
129
|
+
outputPath: options.penPath,
|
|
130
|
+
version: options.version,
|
|
131
|
+
verifyWithPencil: options.verifyWithPencil,
|
|
132
|
+
pencilBin: options.pencilBin
|
|
133
|
+
});
|
|
89
134
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
135
|
+
if (hasBaselinePaths) {
|
|
136
|
+
baselineCheck = comparePenBaselineAlignment({
|
|
137
|
+
penPath: ensureResult.outputPath,
|
|
138
|
+
baselinePaths,
|
|
139
|
+
preferredSource: options.preferredSource
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (
|
|
143
|
+
baselineCheck.status === "BLOCK" &&
|
|
144
|
+
baselineCheck.decision === "diverged_prefer_external" &&
|
|
145
|
+
options.syncPreferredSource === true &&
|
|
146
|
+
baselineCheck.preferredSourcePath
|
|
147
|
+
) {
|
|
148
|
+
baselineSync = syncPenSource({
|
|
149
|
+
sourcePath: baselineCheck.preferredSourcePath,
|
|
150
|
+
targetPath: ensureResult.outputPath,
|
|
151
|
+
stateSource: "pencil-session:begin-sync"
|
|
152
|
+
});
|
|
153
|
+
ensureResult = {
|
|
154
|
+
...ensureResult,
|
|
155
|
+
statePath: baselineSync.statePath,
|
|
156
|
+
state: baselineSync.state
|
|
157
|
+
};
|
|
158
|
+
baselineCheck = comparePenBaselineAlignment({
|
|
159
|
+
penPath: ensureResult.outputPath,
|
|
160
|
+
baselinePaths,
|
|
161
|
+
preferredSource: options.preferredSource
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (baselineCheck.status === "BLOCK") {
|
|
166
|
+
throw new Error(
|
|
167
|
+
[
|
|
168
|
+
"Pencil baseline alignment failed before session start.",
|
|
169
|
+
formatPenBaselineAlignmentReport(baselineCheck)
|
|
170
|
+
].join("\n")
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const beganAt = new Date().toISOString();
|
|
176
|
+
const session = buildSessionState(projectRoot, {
|
|
177
|
+
status: "active",
|
|
178
|
+
beganAt,
|
|
179
|
+
lastActivityAt: beganAt,
|
|
180
|
+
penPath: path.resolve(options.penPath),
|
|
181
|
+
lockPath: lockResult.lockPath,
|
|
182
|
+
lockOwner: lockResult.lock.owner,
|
|
183
|
+
penStatePath: ensureResult.statePath,
|
|
184
|
+
lastPersistedHash: ensureResult.state.snapshotHash,
|
|
185
|
+
lastPersistedAt: ensureResult.state.persistedAt,
|
|
186
|
+
lastTopLevelCount: ensureResult.state.topLevelCount,
|
|
187
|
+
baselineCheck: baselineCheck
|
|
188
|
+
? {
|
|
189
|
+
checkedAt: new Date().toISOString(),
|
|
190
|
+
status: baselineCheck.status,
|
|
191
|
+
decision: baselineCheck.decision,
|
|
192
|
+
preferredSourcePath: baselineCheck.preferredSourcePath,
|
|
193
|
+
baselinePaths: baselineCheck.baselinePaths,
|
|
194
|
+
inSync: baselineCheck.inSync
|
|
195
|
+
}
|
|
196
|
+
: null,
|
|
197
|
+
baselineSync: baselineSync
|
|
198
|
+
? {
|
|
199
|
+
syncedAt: new Date().toISOString(),
|
|
200
|
+
sourcePath: baselineSync.sourcePath,
|
|
201
|
+
targetPath: baselineSync.targetPath,
|
|
202
|
+
snapshotHash: baselineSync.state.snapshotHash
|
|
203
|
+
}
|
|
204
|
+
: null
|
|
205
|
+
});
|
|
206
|
+
const sessionStatePath = writeSessionState(projectRoot, session);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
projectRoot,
|
|
210
|
+
penPath: ensureResult.outputPath,
|
|
211
|
+
session,
|
|
212
|
+
sessionStatePath,
|
|
213
|
+
ensureResult,
|
|
214
|
+
lockResult
|
|
215
|
+
};
|
|
216
|
+
} catch (error) {
|
|
217
|
+
const releaseResult = releaseLockQuietly(projectRoot, {
|
|
218
|
+
homeDir: options.homeDir
|
|
219
|
+
});
|
|
220
|
+
if (releaseResult.releaseError) {
|
|
221
|
+
throw createSessionLifecycleError(
|
|
222
|
+
`${error.message}\nAlso failed to release lock during begin rollback: ${releaseResult.releaseError.message}`,
|
|
223
|
+
error,
|
|
224
|
+
{
|
|
225
|
+
rollbackReleaseError: releaseResult.releaseError
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
98
231
|
}
|
|
99
232
|
|
|
100
233
|
function persistPencilSession(options) {
|
|
@@ -132,6 +265,7 @@ function persistPencilSession(options) {
|
|
|
132
265
|
version: options.version
|
|
133
266
|
});
|
|
134
267
|
|
|
268
|
+
const persistedAt = new Date().toISOString();
|
|
135
269
|
const updatedSession = buildSessionState(projectRoot, {
|
|
136
270
|
...session,
|
|
137
271
|
status: "active",
|
|
@@ -140,7 +274,8 @@ function persistPencilSession(options) {
|
|
|
140
274
|
lastPersistedHash: writeResult.state.snapshotHash,
|
|
141
275
|
lastPersistedAt: writeResult.state.persistedAt,
|
|
142
276
|
lastTopLevelCount: writeResult.state.topLevelCount,
|
|
143
|
-
lastSyncVerifiedAt:
|
|
277
|
+
lastSyncVerifiedAt: persistedAt,
|
|
278
|
+
lastActivityAt: persistedAt,
|
|
144
279
|
inSync: syncResult.inSync
|
|
145
280
|
});
|
|
146
281
|
const sessionStatePath = writeSessionState(projectRoot, updatedSession);
|
|
@@ -163,6 +298,8 @@ function endPencilSession(options) {
|
|
|
163
298
|
throw new Error("No Pencil session state exists for this project. Run `pencil-session begin` first.");
|
|
164
299
|
}
|
|
165
300
|
|
|
301
|
+
assertLockHeldByProject(projectRoot, { homeDir: options.homeDir });
|
|
302
|
+
|
|
166
303
|
const resolvedPenPath = path.resolve(options.penPath || session.penPath || "");
|
|
167
304
|
if (!resolvedPenPath) {
|
|
168
305
|
throw new Error("A registered `.pen` path is required for session shutdown.");
|
|
@@ -188,21 +325,44 @@ function endPencilSession(options) {
|
|
|
188
325
|
}
|
|
189
326
|
}
|
|
190
327
|
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
homeDir: options.homeDir,
|
|
194
|
-
force: options.force
|
|
195
|
-
});
|
|
328
|
+
const endedWithForce = options.force === true;
|
|
329
|
+
const forceWithoutSync = endedWithForce && !syncResult;
|
|
196
330
|
|
|
331
|
+
const endedAt = new Date().toISOString();
|
|
197
332
|
const updatedSession = buildSessionState(projectRoot, {
|
|
198
333
|
...session,
|
|
199
334
|
status: "closed",
|
|
200
335
|
penPath: resolvedPenPath,
|
|
201
|
-
endedAt
|
|
202
|
-
|
|
336
|
+
endedAt,
|
|
337
|
+
endedWithForce,
|
|
338
|
+
forceWithoutSync,
|
|
339
|
+
lastSyncVerifiedAt: syncResult ? endedAt : session.lastSyncVerifiedAt || null,
|
|
340
|
+
lastActivityAt: endedAt,
|
|
203
341
|
inSync: syncResult ? syncResult.inSync : session.inSync === true
|
|
204
342
|
});
|
|
205
343
|
const sessionStatePath = writeSessionState(projectRoot, updatedSession);
|
|
344
|
+
let releaseResult;
|
|
345
|
+
try {
|
|
346
|
+
releaseResult = releasePencilLock({
|
|
347
|
+
projectPath: projectRoot,
|
|
348
|
+
homeDir: options.homeDir,
|
|
349
|
+
force: options.force
|
|
350
|
+
});
|
|
351
|
+
} catch (releaseError) {
|
|
352
|
+
let rollbackError = null;
|
|
353
|
+
try {
|
|
354
|
+
writeSessionState(projectRoot, session);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
rollbackError = error;
|
|
357
|
+
}
|
|
358
|
+
throw createSessionLifecycleError(
|
|
359
|
+
rollbackError
|
|
360
|
+
? `Failed to release Pencil lock while ending session: ${releaseError.message}\nAlso failed to restore active session state: ${rollbackError.message}`
|
|
361
|
+
: `Failed to release Pencil lock while ending session: ${releaseError.message}`,
|
|
362
|
+
releaseError,
|
|
363
|
+
rollbackError ? { sessionRollbackError: rollbackError } : {}
|
|
364
|
+
);
|
|
365
|
+
}
|
|
206
366
|
|
|
207
367
|
return {
|
|
208
368
|
projectRoot,
|
|
@@ -224,12 +384,32 @@ function getPencilSessionStatus(options = {}) {
|
|
|
224
384
|
};
|
|
225
385
|
}
|
|
226
386
|
|
|
387
|
+
function beginPencilSessionAsync(options = {}) {
|
|
388
|
+
return runModuleExportInWorker(__filename, "beginPencilSession", [options]);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function persistPencilSessionAsync(options = {}) {
|
|
392
|
+
return runModuleExportInWorker(__filename, "persistPencilSession", [options]);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function endPencilSessionAsync(options = {}) {
|
|
396
|
+
return runModuleExportInWorker(__filename, "endPencilSession", [options]);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function getPencilSessionStatusAsync(options = {}) {
|
|
400
|
+
return runModuleExportInWorker(__filename, "getPencilSessionStatus", [options]);
|
|
401
|
+
}
|
|
402
|
+
|
|
227
403
|
module.exports = {
|
|
228
404
|
getSessionStatePath,
|
|
229
405
|
readSessionState,
|
|
230
406
|
writeSessionState,
|
|
231
407
|
beginPencilSession,
|
|
408
|
+
beginPencilSessionAsync,
|
|
232
409
|
persistPencilSession,
|
|
410
|
+
persistPencilSessionAsync,
|
|
233
411
|
endPencilSession,
|
|
234
|
-
|
|
412
|
+
endPencilSessionAsync,
|
|
413
|
+
getPencilSessionStatus,
|
|
414
|
+
getPencilSessionStatusAsync
|
|
235
415
|
};
|
package/lib/supervisor-review.js
CHANGED
|
@@ -3,6 +3,17 @@ const path = require("path");
|
|
|
3
3
|
const os = require("os");
|
|
4
4
|
const { execFile } = require("child_process");
|
|
5
5
|
const {
|
|
6
|
+
escapeRegExp,
|
|
7
|
+
isPlainObject,
|
|
8
|
+
parseJsonText,
|
|
9
|
+
pathExists,
|
|
10
|
+
readTextIfExists,
|
|
11
|
+
writeFileAtomic
|
|
12
|
+
} = require("./utils");
|
|
13
|
+
const {
|
|
14
|
+
hasMarkdownHeading,
|
|
15
|
+
getMarkdownSection,
|
|
16
|
+
isAcceptedWarnOutcome,
|
|
6
17
|
parseCheckpointStatusMap,
|
|
7
18
|
getConfiguredDesignSupervisorReviewers
|
|
8
19
|
} = require("./audit-parsers");
|
|
@@ -14,14 +25,8 @@ const REVIEW_STATUS_SEVERITY = Object.freeze({
|
|
|
14
25
|
WARN: 1,
|
|
15
26
|
BLOCK: 2
|
|
16
27
|
});
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return fs.existsSync(targetPath);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function escapeRegExp(value) {
|
|
23
|
-
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24
|
-
}
|
|
28
|
+
const DEFAULT_REVIEWER_EXEC_MAX_BUFFER = 50 * 1024 * 1024;
|
|
29
|
+
const DEFAULT_REVIEWER_RETRY_MAX_DELAY_MS = 5000;
|
|
25
30
|
|
|
26
31
|
function normalizeStatus(status) {
|
|
27
32
|
if (!status) {
|
|
@@ -46,13 +51,6 @@ function normalizeReviewSource(source) {
|
|
|
46
51
|
return normalized;
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
function readTextIfExists(filePath) {
|
|
50
|
-
if (!pathExists(filePath)) {
|
|
51
|
-
return "";
|
|
52
|
-
}
|
|
53
|
-
return fs.readFileSync(filePath, "utf8");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
54
|
function parseReviewerList(value) {
|
|
57
55
|
return String(value || "")
|
|
58
56
|
.split(/[,\n;]/)
|
|
@@ -114,7 +112,13 @@ function buildReviewerPrompt({ reviewer, projectRoot, changeId, pencilDesignPath
|
|
|
114
112
|
}
|
|
115
113
|
|
|
116
114
|
function parseReviewerPayload(rawText) {
|
|
117
|
-
const payload =
|
|
115
|
+
const payload = parseJsonText(String(rawText || "").trim(), "reviewer JSON payload");
|
|
116
|
+
if (!isPlainObject(payload)) {
|
|
117
|
+
throw new Error("Invalid reviewer JSON payload: expected a JSON object.");
|
|
118
|
+
}
|
|
119
|
+
if (!payload.status) {
|
|
120
|
+
throw new Error("Invalid reviewer JSON payload: missing required `status`.");
|
|
121
|
+
}
|
|
118
122
|
const status = normalizeStatus(payload.status);
|
|
119
123
|
const issues = Array.isArray(payload.issues)
|
|
120
124
|
? payload.issues.map((issue) => String(issue || "").trim()).filter(Boolean)
|
|
@@ -161,8 +165,14 @@ async function runReviewerWithCodexOnce(options = {}) {
|
|
|
161
165
|
changeId,
|
|
162
166
|
pencilDesignPath,
|
|
163
167
|
screenshotPaths,
|
|
164
|
-
timeoutMs
|
|
168
|
+
timeoutMs,
|
|
169
|
+
maxBuffer
|
|
165
170
|
} = options;
|
|
171
|
+
const configuredMaxBuffer = Number(maxBuffer);
|
|
172
|
+
const resolvedMaxBuffer =
|
|
173
|
+
Number.isFinite(configuredMaxBuffer) && configuredMaxBuffer > 0
|
|
174
|
+
? configuredMaxBuffer
|
|
175
|
+
: DEFAULT_REVIEWER_EXEC_MAX_BUFFER;
|
|
166
176
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "da-vinci-supervisor-runner-"));
|
|
167
177
|
const outputPath = path.join(tmpDir, "review.json");
|
|
168
178
|
const schemaPath = path.join(tmpDir, "schema.json");
|
|
@@ -224,7 +234,7 @@ async function runReviewerWithCodexOnce(options = {}) {
|
|
|
224
234
|
try {
|
|
225
235
|
await execFileAsync(codexBin, args, {
|
|
226
236
|
encoding: "utf8",
|
|
227
|
-
maxBuffer:
|
|
237
|
+
maxBuffer: resolvedMaxBuffer,
|
|
228
238
|
timeout: timeoutValue > 0 ? timeoutValue : undefined
|
|
229
239
|
});
|
|
230
240
|
} catch (error) {
|
|
@@ -249,6 +259,12 @@ async function runReviewerWithCodex(options = {}) {
|
|
|
249
259
|
const reviewer = String(options.reviewer || "").trim();
|
|
250
260
|
const retries = normalizePositiveInt(options.retries, 1, 0, 8);
|
|
251
261
|
const retryDelayMs = normalizePositiveInt(options.retryDelayMs, 400, 0, 60000);
|
|
262
|
+
const retryMaxDelayMs = normalizePositiveInt(
|
|
263
|
+
options.retryMaxDelayMs,
|
|
264
|
+
DEFAULT_REVIEWER_RETRY_MAX_DELAY_MS,
|
|
265
|
+
0,
|
|
266
|
+
60000
|
|
267
|
+
);
|
|
252
268
|
const maxAttempts = retries + 1;
|
|
253
269
|
let attempt = 0;
|
|
254
270
|
let lastError = null;
|
|
@@ -266,7 +282,7 @@ async function runReviewerWithCodex(options = {}) {
|
|
|
266
282
|
if (attempt >= maxAttempts) {
|
|
267
283
|
break;
|
|
268
284
|
}
|
|
269
|
-
const backoffDelay = retryDelayMs * Math.pow(2, attempt - 1);
|
|
285
|
+
const backoffDelay = Math.min(retryDelayMs * Math.pow(2, attempt - 1), retryMaxDelayMs);
|
|
270
286
|
await sleep(backoffDelay);
|
|
271
287
|
}
|
|
272
288
|
}
|
|
@@ -369,14 +385,12 @@ function resolvePencilDesignPath(projectRoot, options = {}) {
|
|
|
369
385
|
}
|
|
370
386
|
|
|
371
387
|
function extractMcpRuntimeGateStatus(markdownText) {
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
);
|
|
375
|
-
if (!sectionMatch) {
|
|
388
|
+
const section = getMarkdownSection(markdownText, "MCP Runtime Gate");
|
|
389
|
+
if (!section) {
|
|
376
390
|
return "";
|
|
377
391
|
}
|
|
378
392
|
|
|
379
|
-
const statusMatch = String(
|
|
393
|
+
const statusMatch = String(section).match(
|
|
380
394
|
/(?:^|\n)\s*-\s*(?:Status|状态)\s*:\s*`?(PASS|WARN|BLOCK)`?\b/i
|
|
381
395
|
);
|
|
382
396
|
return statusMatch ? String(statusMatch[1]).toUpperCase() : "";
|
|
@@ -396,13 +410,11 @@ function inferReview(markdownText, options = {}) {
|
|
|
396
410
|
const designCheckpointStatus =
|
|
397
411
|
checkpointStatuses["design checkpoint"] || extractCheckpointStatus(markdownText, "Design checkpoint");
|
|
398
412
|
const runtimeGateStatus = extractMcpRuntimeGateStatus(markdownText);
|
|
399
|
-
const hasScreenshotRecords =
|
|
400
|
-
String(markdownText || "")
|
|
401
|
-
);
|
|
413
|
+
const hasScreenshotRecords = hasMarkdownHeading(markdownText, "Screenshot Review Records");
|
|
402
414
|
let status = "PASS";
|
|
403
415
|
|
|
404
416
|
if (!hasScreenshotRecords) {
|
|
405
|
-
issues.push("Missing
|
|
417
|
+
issues.push("Missing `Screenshot Review Records` evidence.");
|
|
406
418
|
status = "BLOCK";
|
|
407
419
|
}
|
|
408
420
|
|
|
@@ -446,9 +458,7 @@ function inferReview(markdownText, options = {}) {
|
|
|
446
458
|
}
|
|
447
459
|
|
|
448
460
|
function hasAcceptedWarn(revisionOutcome) {
|
|
449
|
-
return
|
|
450
|
-
String(revisionOutcome || "")
|
|
451
|
-
);
|
|
461
|
+
return isAcceptedWarnOutcome(revisionOutcome);
|
|
452
462
|
}
|
|
453
463
|
|
|
454
464
|
function buildReviewSection(review) {
|
|
@@ -505,6 +515,12 @@ async function runDesignSupervisorReview(options = {}) {
|
|
|
505
515
|
const reviewConcurrency = normalizePositiveInt(options.reviewConcurrency, 2, 1, 16);
|
|
506
516
|
const reviewerRetries = normalizePositiveInt(options.reviewerRetries, 1, 0, 8);
|
|
507
517
|
const reviewerRetryDelayMs = normalizePositiveInt(options.reviewerRetryDelayMs, 400, 0, 60000);
|
|
518
|
+
const reviewerRetryMaxDelayMs = normalizePositiveInt(
|
|
519
|
+
options.reviewerRetryMaxDelayMs,
|
|
520
|
+
DEFAULT_REVIEWER_RETRY_MAX_DELAY_MS,
|
|
521
|
+
0,
|
|
522
|
+
60000
|
|
523
|
+
);
|
|
508
524
|
const manualStatus = normalizeStatus(options.status);
|
|
509
525
|
const reviewerAttempts = {};
|
|
510
526
|
let review = null;
|
|
@@ -531,10 +547,11 @@ async function runDesignSupervisorReview(options = {}) {
|
|
|
531
547
|
pencilDesignPath,
|
|
532
548
|
screenshotPaths,
|
|
533
549
|
timeoutMs: options.reviewerTimeoutMs,
|
|
550
|
+
maxBuffer: options.reviewerMaxBuffer,
|
|
534
551
|
retries: reviewerRetries,
|
|
535
|
-
retryDelayMs: reviewerRetryDelayMs
|
|
552
|
+
retryDelayMs: reviewerRetryDelayMs,
|
|
553
|
+
retryMaxDelayMs: reviewerRetryMaxDelayMs
|
|
536
554
|
});
|
|
537
|
-
reviewerAttempts[reviewer] = finding.attempts || 1;
|
|
538
555
|
return {
|
|
539
556
|
reviewer,
|
|
540
557
|
...finding
|
|
@@ -542,6 +559,9 @@ async function runDesignSupervisorReview(options = {}) {
|
|
|
542
559
|
},
|
|
543
560
|
reviewConcurrency
|
|
544
561
|
);
|
|
562
|
+
for (const finding of findings) {
|
|
563
|
+
reviewerAttempts[finding.reviewer] = finding.attempts || 1;
|
|
564
|
+
}
|
|
545
565
|
|
|
546
566
|
const aggregated = aggregateReviewerFindings(findings, {
|
|
547
567
|
acceptWarn: options.acceptWarn
|
|
@@ -608,7 +628,7 @@ async function runDesignSupervisorReview(options = {}) {
|
|
|
608
628
|
if (options.write) {
|
|
609
629
|
fs.mkdirSync(path.dirname(pencilDesignPath), { recursive: true });
|
|
610
630
|
const nextText = appendReviewSection(existingText, review);
|
|
611
|
-
|
|
631
|
+
writeFileAtomic(pencilDesignPath, nextText);
|
|
612
632
|
wrote = true;
|
|
613
633
|
}
|
|
614
634
|
|
|
@@ -627,6 +647,7 @@ async function runDesignSupervisorReview(options = {}) {
|
|
|
627
647
|
reviewConcurrency: runReviewers ? reviewConcurrency : 0,
|
|
628
648
|
reviewerRetries: runReviewers ? reviewerRetries : 0,
|
|
629
649
|
reviewerRetryDelayMs: runReviewers ? reviewerRetryDelayMs : 0,
|
|
650
|
+
reviewerRetryMaxDelayMs: runReviewers ? reviewerRetryMaxDelayMs : 0,
|
|
630
651
|
reviewerAttempts: runReviewers ? reviewerAttempts : {},
|
|
631
652
|
wrote
|
|
632
653
|
};
|
|
@@ -657,6 +678,7 @@ function formatDesignSupervisorReviewReport(result) {
|
|
|
657
678
|
lines.push(`Review concurrency: ${result.reviewConcurrency}`);
|
|
658
679
|
lines.push(`Reviewer retries: ${result.reviewerRetries}`);
|
|
659
680
|
lines.push(`Reviewer retry delay (ms): ${result.reviewerRetryDelayMs}`);
|
|
681
|
+
lines.push(`Reviewer retry max delay (ms): ${result.reviewerRetryMaxDelayMs}`);
|
|
660
682
|
const reviewerAttemptTokens = Object.entries(result.reviewerAttempts || {}).map(
|
|
661
683
|
([reviewer, attempts]) => `${reviewer}:${attempts}`
|
|
662
684
|
);
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const HAS_SYNC_WAIT =
|
|
5
|
+
typeof SharedArrayBuffer === "function" &&
|
|
6
|
+
typeof Atomics === "object" &&
|
|
7
|
+
Atomics !== null &&
|
|
8
|
+
typeof Atomics.wait === "function";
|
|
9
|
+
const SYNC_SLEEP_ARRAY = HAS_SYNC_WAIT ? new Int32Array(new SharedArrayBuffer(4)) : null;
|
|
10
|
+
|
|
11
|
+
function isPlainObject(value) {
|
|
12
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function escapeRegExp(value) {
|
|
16
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function pathExists(targetPath) {
|
|
20
|
+
return fs.existsSync(targetPath);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readTextIfExists(targetPath, options = {}) {
|
|
24
|
+
const encoding = options.encoding || "utf8";
|
|
25
|
+
if (!pathExists(targetPath)) {
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
return fs.readFileSync(targetPath, encoding);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function uniqueValues(values) {
|
|
32
|
+
return Array.from(new Set((values || []).filter(Boolean)));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseJsonText(raw, context = "JSON payload") {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(String(raw));
|
|
38
|
+
} catch (error) {
|
|
39
|
+
const wrapped = new Error(
|
|
40
|
+
`Invalid ${context}: ${error && error.message ? error.message : String(error)}`
|
|
41
|
+
);
|
|
42
|
+
if (error && typeof error === "object") {
|
|
43
|
+
wrapped.cause = error;
|
|
44
|
+
}
|
|
45
|
+
throw wrapped;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readJsonFile(targetPath, context = null) {
|
|
50
|
+
const resolvedTargetPath = path.resolve(targetPath);
|
|
51
|
+
const label = context || `JSON file at ${resolvedTargetPath}`;
|
|
52
|
+
return parseJsonText(fs.readFileSync(resolvedTargetPath, "utf8"), label);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function sleepSync(durationMs, context = "perform synchronous sleep") {
|
|
56
|
+
const timeoutMs = Math.max(0, Number(durationMs) || 0);
|
|
57
|
+
if (timeoutMs === 0) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!HAS_SYNC_WAIT || !SYNC_SLEEP_ARRAY) {
|
|
62
|
+
throw new Error(`Unable to ${context}: Atomics.wait is unavailable in this runtime.`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
Atomics.wait(SYNC_SLEEP_ARRAY, 0, 0, timeoutMs);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const wrapped = new Error(
|
|
69
|
+
`Unable to ${context}: ${error && error.message ? error.message : String(error)}`
|
|
70
|
+
);
|
|
71
|
+
if (error && typeof error === "object") {
|
|
72
|
+
wrapped.cause = error;
|
|
73
|
+
}
|
|
74
|
+
throw wrapped;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildAtomicTempPath(targetPath) {
|
|
79
|
+
const resolvedTargetPath = path.resolve(targetPath);
|
|
80
|
+
return path.join(
|
|
81
|
+
path.dirname(resolvedTargetPath),
|
|
82
|
+
`.${path.basename(resolvedTargetPath)}.tmp-${process.pid}-${Date.now()}-${Math.random()
|
|
83
|
+
.toString(16)
|
|
84
|
+
.slice(2)}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function writeFileAtomic(targetPath, content, options = {}) {
|
|
89
|
+
const resolvedTargetPath = path.resolve(targetPath);
|
|
90
|
+
const encoding = options.encoding || "utf8";
|
|
91
|
+
const tempPath = buildAtomicTempPath(resolvedTargetPath);
|
|
92
|
+
fs.mkdirSync(path.dirname(resolvedTargetPath), { recursive: true });
|
|
93
|
+
fs.writeFileSync(tempPath, content, encoding);
|
|
94
|
+
fs.renameSync(tempPath, resolvedTargetPath);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function writeFileExclusiveAtomic(targetPath, content, options = {}) {
|
|
98
|
+
const resolvedTargetPath = path.resolve(targetPath);
|
|
99
|
+
const encoding = options.encoding || "utf8";
|
|
100
|
+
const tempPath = buildAtomicTempPath(resolvedTargetPath);
|
|
101
|
+
fs.mkdirSync(path.dirname(resolvedTargetPath), { recursive: true });
|
|
102
|
+
fs.writeFileSync(tempPath, content, encoding);
|
|
103
|
+
try {
|
|
104
|
+
fs.linkSync(tempPath, resolvedTargetPath);
|
|
105
|
+
} finally {
|
|
106
|
+
fs.rmSync(tempPath, { force: true });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = {
|
|
111
|
+
isPlainObject,
|
|
112
|
+
escapeRegExp,
|
|
113
|
+
pathExists,
|
|
114
|
+
readTextIfExists,
|
|
115
|
+
uniqueValues,
|
|
116
|
+
parseJsonText,
|
|
117
|
+
readJsonFile,
|
|
118
|
+
sleepSync,
|
|
119
|
+
writeFileAtomic,
|
|
120
|
+
writeFileExclusiveAtomic
|
|
121
|
+
};
|