@xenonbyte/da-vinci-workflow 0.2.5 → 0.2.7
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 +32 -0
- package/README.md +15 -9
- package/README.zh-CN.md +16 -9
- package/docs/dv-command-reference.md +21 -3
- package/docs/execution-chain-migration.md +14 -3
- package/docs/maintainer-bootstrap.md +102 -0
- package/docs/pencil-rendering-workflow.md +1 -1
- package/docs/skill-usage.md +31 -0
- package/docs/workflow-overview.md +40 -5
- package/docs/zh-CN/dv-command-reference.md +19 -3
- package/docs/zh-CN/maintainer-bootstrap.md +101 -0
- package/docs/zh-CN/pencil-rendering-workflow.md +1 -1
- package/docs/zh-CN/skill-usage.md +30 -0
- package/docs/zh-CN/workflow-overview.md +38 -5
- package/lib/audit.js +19 -0
- package/lib/cli/helpers.js +63 -2
- package/lib/cli.js +119 -2
- package/lib/gate-utils.js +56 -0
- package/lib/install.js +134 -6
- package/lib/isolated-worker-handoff.js +181 -0
- package/lib/lint-bindings.js +41 -28
- package/lib/lint-spec.js +403 -109
- package/lib/lint-tasks.js +571 -21
- package/lib/maintainer-readiness.js +317 -0
- package/lib/planning-parsers.js +190 -1
- package/lib/planning-quality-utils.js +81 -0
- package/lib/planning-signal-freshness.js +205 -0
- package/lib/scope-check.js +751 -82
- package/lib/sidecars.js +396 -1
- package/lib/supervisor-review.js +117 -6
- package/lib/task-execution.js +88 -16
- package/lib/task-review.js +14 -8
- package/lib/utils.js +15 -0
- package/lib/workflow-persisted-state.js +52 -32
- package/lib/workflow-state.js +1241 -249
- package/package.json +3 -2
package/lib/sidecars.js
CHANGED
|
@@ -17,6 +17,87 @@ const {
|
|
|
17
17
|
|
|
18
18
|
const SCHEMA_VERSION = "1.0.0";
|
|
19
19
|
|
|
20
|
+
function readJsonIfExists(targetPath) {
|
|
21
|
+
if (!targetPath || !pathExists(targetPath)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(fs.readFileSync(targetPath, "utf8"));
|
|
26
|
+
} catch (_error) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeEvidenceText(value) {
|
|
32
|
+
return String(value || "")
|
|
33
|
+
.toLowerCase()
|
|
34
|
+
.replace(/[`*_~]/g, "")
|
|
35
|
+
.replace(/\s+/g, " ")
|
|
36
|
+
.trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveProjectRootFromChangeDir(changeDir) {
|
|
40
|
+
return path.resolve(changeDir || process.cwd(), "..", "..", "..");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveSourceRefAbsolutePath(projectRoot, sourceRefPath) {
|
|
44
|
+
const sourcePath = String(sourceRefPath || "").trim();
|
|
45
|
+
if (!sourcePath) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (path.isAbsolute(sourcePath)) {
|
|
49
|
+
return sourcePath;
|
|
50
|
+
}
|
|
51
|
+
return path.join(projectRoot, sourcePath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function evaluateSourceRefsFreshness(changeDir, payload) {
|
|
55
|
+
const sourceRefs = Array.isArray(payload && payload.sourceRefs) ? payload.sourceRefs : [];
|
|
56
|
+
if (sourceRefs.length === 0) {
|
|
57
|
+
return {
|
|
58
|
+
fresh: false,
|
|
59
|
+
checked: 0,
|
|
60
|
+
stale: ["missing_source_refs"]
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const projectRoot = resolveProjectRootFromChangeDir(changeDir);
|
|
64
|
+
const stale = [];
|
|
65
|
+
for (const ref of sourceRefs) {
|
|
66
|
+
const absolutePath = resolveSourceRefAbsolutePath(projectRoot, ref && ref.path);
|
|
67
|
+
if (!absolutePath || !pathExists(absolutePath)) {
|
|
68
|
+
stale.push(`missing:${String((ref && ref.path) || "").trim() || "(unknown)"}`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const expected = String((ref && ref.digest) || "").trim();
|
|
72
|
+
const actual = digestFile(absolutePath);
|
|
73
|
+
if (expected && actual && expected !== actual) {
|
|
74
|
+
stale.push(`digest_mismatch:${String(ref.path || absolutePath)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
fresh: stale.length === 0,
|
|
80
|
+
checked: sourceRefs.length,
|
|
81
|
+
stale
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildCollectionTextIdMap(items, idField = "id", textField = "text") {
|
|
86
|
+
const map = new Map();
|
|
87
|
+
for (const item of Array.isArray(items) ? items : []) {
|
|
88
|
+
const id = String(item && item[idField] ? item[idField] : "").trim();
|
|
89
|
+
const text = normalizeEvidenceText(item && item[textField]);
|
|
90
|
+
if (!id || !text) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!map.has(text)) {
|
|
94
|
+
map.set(text, []);
|
|
95
|
+
}
|
|
96
|
+
map.get(text).push(id);
|
|
97
|
+
}
|
|
98
|
+
return map;
|
|
99
|
+
}
|
|
100
|
+
|
|
20
101
|
function buildEnvelope(projectRoot) {
|
|
21
102
|
return {
|
|
22
103
|
status: STATUS.PASS,
|
|
@@ -241,6 +322,316 @@ function writeSidecarFile(sidecarsDir, fileName, payload) {
|
|
|
241
322
|
return absolutePath;
|
|
242
323
|
}
|
|
243
324
|
|
|
325
|
+
function loadPlanningAnchorIndex(changeDir) {
|
|
326
|
+
const sidecarsDir = path.join(changeDir, "sidecars");
|
|
327
|
+
const spec = readJsonIfExists(path.join(sidecarsDir, "spec.index.json"));
|
|
328
|
+
const bindings = readJsonIfExists(path.join(sidecarsDir, "bindings.index.json"));
|
|
329
|
+
const notes = [];
|
|
330
|
+
const warnings = [];
|
|
331
|
+
|
|
332
|
+
if (!pathExists(sidecarsDir)) {
|
|
333
|
+
notes.push("Planning sidecars are missing; anchor resolution will use artifact fallback.");
|
|
334
|
+
return {
|
|
335
|
+
sidecarsDir,
|
|
336
|
+
available: false,
|
|
337
|
+
index: {
|
|
338
|
+
behavior: new Set(),
|
|
339
|
+
acceptance: new Set(),
|
|
340
|
+
state: new Set(),
|
|
341
|
+
mapping: new Set()
|
|
342
|
+
},
|
|
343
|
+
notes,
|
|
344
|
+
warnings
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!spec) {
|
|
349
|
+
warnings.push("Missing or unreadable `spec.index.json` in sidecars.");
|
|
350
|
+
}
|
|
351
|
+
if (!bindings) {
|
|
352
|
+
warnings.push("Missing or unreadable `bindings.index.json` in sidecars.");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const specFreshness = spec ? evaluateSourceRefsFreshness(changeDir, spec) : null;
|
|
356
|
+
const bindingsFreshness = bindings ? evaluateSourceRefsFreshness(changeDir, bindings) : null;
|
|
357
|
+
if (specFreshness && !specFreshness.fresh) {
|
|
358
|
+
warnings.push(
|
|
359
|
+
`spec.index.json appears stale (${specFreshness.stale.join(", ")}); anchor resolution will degrade.`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
if (bindingsFreshness && !bindingsFreshness.fresh) {
|
|
363
|
+
warnings.push(
|
|
364
|
+
`bindings.index.json appears stale (${bindingsFreshness.stale.join(", ")}); anchor resolution will degrade.`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const behavior = new Set(
|
|
369
|
+
((spec && spec.collections && spec.collections.behavior) || []).map((item) => item.id)
|
|
370
|
+
);
|
|
371
|
+
const acceptance = new Set(
|
|
372
|
+
((spec && spec.collections && spec.collections.acceptance) || []).map((item) => item.id)
|
|
373
|
+
);
|
|
374
|
+
const state = new Set(
|
|
375
|
+
((spec && spec.collections && spec.collections.states) || []).map((item) => item.id)
|
|
376
|
+
);
|
|
377
|
+
const mapping = new Set(((bindings && bindings.mappings) || []).map((item) => item.id));
|
|
378
|
+
|
|
379
|
+
const sourceFresh =
|
|
380
|
+
(!specFreshness || specFreshness.fresh) && (!bindingsFreshness || bindingsFreshness.fresh);
|
|
381
|
+
const available =
|
|
382
|
+
sourceFresh && (behavior.size > 0 || acceptance.size > 0 || state.size > 0 || mapping.size > 0);
|
|
383
|
+
if (!available) {
|
|
384
|
+
if (!sourceFresh) {
|
|
385
|
+
notes.push("Planning sidecars exist but are stale; anchor resolution falls back to artifact evidence.");
|
|
386
|
+
} else {
|
|
387
|
+
notes.push("Planning sidecars are present but empty for anchor id resolution.");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
sidecarsDir,
|
|
393
|
+
available,
|
|
394
|
+
fresh: sourceFresh,
|
|
395
|
+
freshness: {
|
|
396
|
+
spec: specFreshness,
|
|
397
|
+
bindings: bindingsFreshness
|
|
398
|
+
},
|
|
399
|
+
index: {
|
|
400
|
+
behavior,
|
|
401
|
+
acceptance,
|
|
402
|
+
state,
|
|
403
|
+
mapping
|
|
404
|
+
},
|
|
405
|
+
raw: {
|
|
406
|
+
spec,
|
|
407
|
+
bindings
|
|
408
|
+
},
|
|
409
|
+
notes,
|
|
410
|
+
warnings
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function resolvePlanningAnchorRefs(anchorRefs, options = {}) {
|
|
415
|
+
const refs = Array.isArray(anchorRefs) ? anchorRefs : [];
|
|
416
|
+
const sidecarIndex = options.sidecarIndex || {
|
|
417
|
+
available: false,
|
|
418
|
+
index: {
|
|
419
|
+
behavior: new Set(),
|
|
420
|
+
acceptance: new Set(),
|
|
421
|
+
state: new Set(),
|
|
422
|
+
mapping: new Set()
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
const artifactResolver = typeof options.resolveArtifactRef === "function" ? options.resolveArtifactRef : null;
|
|
426
|
+
const anchorIdResolver = typeof options.resolveAnchorIdRef === "function" ? options.resolveAnchorIdRef : null;
|
|
427
|
+
|
|
428
|
+
const resolved = [];
|
|
429
|
+
const unresolved = [];
|
|
430
|
+
const notes = [];
|
|
431
|
+
|
|
432
|
+
for (const ref of refs) {
|
|
433
|
+
const normalized = String(ref || "").trim();
|
|
434
|
+
if (!normalized) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const familyMatch = normalized.match(/^(behavior|acceptance|state|mapping)-/i);
|
|
439
|
+
if (familyMatch) {
|
|
440
|
+
const family = String(familyMatch[1] || "").toLowerCase();
|
|
441
|
+
if (sidecarIndex.available && sidecarIndex.index[family] && sidecarIndex.index[family].has(normalized)) {
|
|
442
|
+
resolved.push({
|
|
443
|
+
ref: normalized,
|
|
444
|
+
source: "sidecar",
|
|
445
|
+
family
|
|
446
|
+
});
|
|
447
|
+
} else {
|
|
448
|
+
let fallback = null;
|
|
449
|
+
if (anchorIdResolver) {
|
|
450
|
+
fallback = anchorIdResolver({
|
|
451
|
+
ref: normalized,
|
|
452
|
+
family,
|
|
453
|
+
sidecarAvailable: sidecarIndex.available,
|
|
454
|
+
sidecarFresh: sidecarIndex.fresh !== false
|
|
455
|
+
});
|
|
456
|
+
if (fallback && fallback.ok) {
|
|
457
|
+
resolved.push({
|
|
458
|
+
ref: normalized,
|
|
459
|
+
source: fallback.source || "artifact-parse",
|
|
460
|
+
family
|
|
461
|
+
});
|
|
462
|
+
if (fallback.note) {
|
|
463
|
+
notes.push(String(fallback.note));
|
|
464
|
+
}
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const fallbackReason =
|
|
469
|
+
fallback && fallback.reason ? String(fallback.reason).trim() : "";
|
|
470
|
+
const fallbackClassification =
|
|
471
|
+
fallback && fallback.classification ? String(fallback.classification).trim() : "";
|
|
472
|
+
unresolved.push({
|
|
473
|
+
ref: normalized,
|
|
474
|
+
reason:
|
|
475
|
+
fallbackReason ||
|
|
476
|
+
(sidecarIndex.available ? "unknown_sidecar_id" : "sidecar_unavailable"),
|
|
477
|
+
family,
|
|
478
|
+
classification: fallbackClassification ||
|
|
479
|
+
(sidecarIndex.available || sidecarIndex.fresh === false || fallbackReason
|
|
480
|
+
? "invalid_or_stale_sidecar_anchor"
|
|
481
|
+
: "sidecar_not_available")
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const artifactMatch = normalized.match(/^artifact:([^#\s]+)(?:#(.+))?$/i);
|
|
488
|
+
if (artifactMatch && artifactResolver) {
|
|
489
|
+
const artifactPath = String(artifactMatch[1] || "").trim();
|
|
490
|
+
const artifactToken = String(artifactMatch[2] || "").trim();
|
|
491
|
+
const artifactResult = artifactResolver({
|
|
492
|
+
artifactPath,
|
|
493
|
+
artifactToken
|
|
494
|
+
});
|
|
495
|
+
if (artifactResult && artifactResult.ok) {
|
|
496
|
+
resolved.push({
|
|
497
|
+
ref: normalized,
|
|
498
|
+
source: "artifact",
|
|
499
|
+
artifactPath,
|
|
500
|
+
artifactToken
|
|
501
|
+
});
|
|
502
|
+
} else {
|
|
503
|
+
unresolved.push({
|
|
504
|
+
ref: normalized,
|
|
505
|
+
reason:
|
|
506
|
+
artifactResult && artifactResult.reason ? artifactResult.reason : "artifact_ref_unresolved"
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
unresolved.push({
|
|
513
|
+
ref: normalized,
|
|
514
|
+
reason: "unsupported_anchor_format",
|
|
515
|
+
classification: "malformed_anchor"
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (!sidecarIndex.available) {
|
|
520
|
+
notes.push("Anchor id resolution degraded: sidecars unavailable; artifact refs are preferred.");
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
resolved,
|
|
525
|
+
unresolved,
|
|
526
|
+
notes
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function loadAnalyzeEvidenceIndex(changeDir) {
|
|
531
|
+
const sidecarIndex = loadPlanningAnchorIndex(changeDir);
|
|
532
|
+
const sidecarsDir = sidecarIndex.sidecarsDir;
|
|
533
|
+
const spec = sidecarIndex.raw && sidecarIndex.raw.spec ? sidecarIndex.raw.spec : null;
|
|
534
|
+
const bindings = sidecarIndex.raw && sidecarIndex.raw.bindings ? sidecarIndex.raw.bindings : null;
|
|
535
|
+
|
|
536
|
+
const behaviorMap = buildCollectionTextIdMap(spec && spec.collections && spec.collections.behavior);
|
|
537
|
+
const acceptanceMap = buildCollectionTextIdMap(spec && spec.collections && spec.collections.acceptance);
|
|
538
|
+
const stateMap = buildCollectionTextIdMap(spec && spec.collections && spec.collections.states);
|
|
539
|
+
const mappingByImplementation = new Map();
|
|
540
|
+
for (const mapping of (bindings && bindings.mappings) || []) {
|
|
541
|
+
const implementation = String(mapping && mapping.implementation ? mapping.implementation : "").trim();
|
|
542
|
+
if (!implementation) {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const normalized = implementation.replace(/\\/g, "/");
|
|
546
|
+
if (!mappingByImplementation.has(normalized)) {
|
|
547
|
+
mappingByImplementation.set(normalized, []);
|
|
548
|
+
}
|
|
549
|
+
mappingByImplementation.get(normalized).push(String(mapping.id || "").trim());
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
sidecarsDir,
|
|
554
|
+
available: sidecarIndex.available,
|
|
555
|
+
fresh: sidecarIndex.fresh === true,
|
|
556
|
+
notes: sidecarIndex.notes.slice(),
|
|
557
|
+
warnings: sidecarIndex.warnings.slice(),
|
|
558
|
+
maps: {
|
|
559
|
+
behavior: behaviorMap,
|
|
560
|
+
acceptance: acceptanceMap,
|
|
561
|
+
state: stateMap,
|
|
562
|
+
mappingByImplementation
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function resolveAnalyzeEvidenceRefs(evidenceItems, options = {}) {
|
|
568
|
+
const items = Array.isArray(evidenceItems) ? evidenceItems : [];
|
|
569
|
+
const index = options.index || {
|
|
570
|
+
available: false,
|
|
571
|
+
maps: {
|
|
572
|
+
behavior: new Map(),
|
|
573
|
+
acceptance: new Map(),
|
|
574
|
+
state: new Map(),
|
|
575
|
+
mappingByImplementation: new Map()
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
const refs = [];
|
|
579
|
+
const notes = [];
|
|
580
|
+
|
|
581
|
+
for (const item of items) {
|
|
582
|
+
const artifactPath = String(item && item.artifactPath ? item.artifactPath : "").trim();
|
|
583
|
+
const itemText = String(item && item.itemText ? item.itemText : "").trim();
|
|
584
|
+
const kind = String(item && item.kind ? item.kind : "").trim().toLowerCase();
|
|
585
|
+
const implementationPath = String(item && item.implementationPath ? item.implementationPath : "").trim();
|
|
586
|
+
const normalizedText = normalizeEvidenceText(itemText);
|
|
587
|
+
|
|
588
|
+
if (index.available && normalizedText) {
|
|
589
|
+
const candidateIds = [];
|
|
590
|
+
if (kind === "behavior" && index.maps.behavior.has(normalizedText)) {
|
|
591
|
+
candidateIds.push(...index.maps.behavior.get(normalizedText));
|
|
592
|
+
}
|
|
593
|
+
if (kind === "acceptance" && index.maps.acceptance.has(normalizedText)) {
|
|
594
|
+
candidateIds.push(...index.maps.acceptance.get(normalizedText));
|
|
595
|
+
}
|
|
596
|
+
if (kind === "state" && index.maps.state.has(normalizedText)) {
|
|
597
|
+
candidateIds.push(...index.maps.state.get(normalizedText));
|
|
598
|
+
}
|
|
599
|
+
for (const id of candidateIds) {
|
|
600
|
+
refs.push(`sidecar:${id}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (index.available && implementationPath) {
|
|
605
|
+
const normalizedImplementation = implementationPath.replace(/\\/g, "/");
|
|
606
|
+
const mappingIds = index.maps.mappingByImplementation.get(normalizedImplementation) || [];
|
|
607
|
+
for (const id of mappingIds) {
|
|
608
|
+
refs.push(`sidecar:${id}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (artifactPath) {
|
|
613
|
+
const fallbackToken = itemText
|
|
614
|
+
? itemText
|
|
615
|
+
.split(/\s+/)
|
|
616
|
+
.slice(0, 8)
|
|
617
|
+
.join(" ")
|
|
618
|
+
: "";
|
|
619
|
+
refs.push(
|
|
620
|
+
fallbackToken ? `artifact:${artifactPath}#${fallbackToken}` : `artifact:${artifactPath}`
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (!index.available) {
|
|
626
|
+
notes.push("Analyze evidence mapping degraded to artifact refs because sidecars are unavailable or stale.");
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return {
|
|
630
|
+
refs: unique(refs),
|
|
631
|
+
notes
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
244
635
|
function generatePlanningSidecars(projectPathInput, options = {}) {
|
|
245
636
|
const projectRoot = path.resolve(projectPathInput || process.cwd());
|
|
246
637
|
const write = options.write !== false;
|
|
@@ -365,5 +756,9 @@ function formatGenerateSidecarsReport(result) {
|
|
|
365
756
|
module.exports = {
|
|
366
757
|
SCHEMA_VERSION,
|
|
367
758
|
generatePlanningSidecars,
|
|
368
|
-
formatGenerateSidecarsReport
|
|
759
|
+
formatGenerateSidecarsReport,
|
|
760
|
+
loadPlanningAnchorIndex,
|
|
761
|
+
resolvePlanningAnchorRefs,
|
|
762
|
+
loadAnalyzeEvidenceIndex,
|
|
763
|
+
resolveAnalyzeEvidenceRefs
|
|
369
764
|
};
|
package/lib/supervisor-review.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const os = require("os");
|
|
4
|
-
const {
|
|
4
|
+
const { spawn } = require("child_process");
|
|
5
5
|
const {
|
|
6
6
|
escapeRegExp,
|
|
7
7
|
isPlainObject,
|
|
@@ -120,6 +120,19 @@ function truncateForError(value, maxLength = 240) {
|
|
|
120
120
|
return `${text.slice(0, maxLength)}...`;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
function buildReviewerExecutionDiagnostics(stdout, stderr) {
|
|
124
|
+
const diagnostics = [];
|
|
125
|
+
const normalizedStderr = truncateForError(stderr, 600);
|
|
126
|
+
const normalizedStdout = truncateForError(stdout, 600);
|
|
127
|
+
if (normalizedStderr) {
|
|
128
|
+
diagnostics.push(`stderr: ${normalizedStderr}`);
|
|
129
|
+
}
|
|
130
|
+
if (normalizedStdout) {
|
|
131
|
+
diagnostics.push(`stdout: ${normalizedStdout}`);
|
|
132
|
+
}
|
|
133
|
+
return diagnostics;
|
|
134
|
+
}
|
|
135
|
+
|
|
123
136
|
function parseReviewerPayload(rawText) {
|
|
124
137
|
const payload = parseJsonText(String(rawText || "").trim(), "reviewer JSON payload");
|
|
125
138
|
if (!isPlainObject(payload)) {
|
|
@@ -146,8 +159,97 @@ function parseReviewerPayload(rawText) {
|
|
|
146
159
|
|
|
147
160
|
function execFileAsync(command, args, options = {}) {
|
|
148
161
|
return new Promise((resolve, reject) => {
|
|
149
|
-
|
|
150
|
-
|
|
162
|
+
const encoding = options.encoding || "utf8";
|
|
163
|
+
const maxBuffer = Number.isFinite(options.maxBuffer) && options.maxBuffer > 0 ? options.maxBuffer : Infinity;
|
|
164
|
+
const child = spawn(command, args, {
|
|
165
|
+
cwd: options.cwd,
|
|
166
|
+
env: options.env,
|
|
167
|
+
shell: false,
|
|
168
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
let finished = false;
|
|
172
|
+
let timedOut = false;
|
|
173
|
+
let overflowed = false;
|
|
174
|
+
let stdoutSize = 0;
|
|
175
|
+
let stderrSize = 0;
|
|
176
|
+
const stdoutChunks = [];
|
|
177
|
+
const stderrChunks = [];
|
|
178
|
+
|
|
179
|
+
function finalizeError(error) {
|
|
180
|
+
if (finished) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
finished = true;
|
|
184
|
+
if (timeoutId) {
|
|
185
|
+
clearTimeout(timeoutId);
|
|
186
|
+
}
|
|
187
|
+
error.stdout = Buffer.concat(stdoutChunks).toString(encoding);
|
|
188
|
+
error.stderr = Buffer.concat(stderrChunks).toString(encoding);
|
|
189
|
+
reject(error);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function pushChunk(target, chunk, currentSize) {
|
|
193
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk || ""), encoding);
|
|
194
|
+
const nextSize = currentSize + buffer.length;
|
|
195
|
+
target.push(buffer);
|
|
196
|
+
return nextSize;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const timeoutMs = Number.isFinite(options.timeout) && options.timeout > 0 ? options.timeout : 0;
|
|
200
|
+
const timeoutId =
|
|
201
|
+
timeoutMs > 0
|
|
202
|
+
? setTimeout(() => {
|
|
203
|
+
timedOut = true;
|
|
204
|
+
child.kill();
|
|
205
|
+
}, timeoutMs)
|
|
206
|
+
: null;
|
|
207
|
+
|
|
208
|
+
child.on("error", (error) => {
|
|
209
|
+
finalizeError(error);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
child.stdout.on("data", (chunk) => {
|
|
213
|
+
stdoutSize = pushChunk(stdoutChunks, chunk, stdoutSize);
|
|
214
|
+
if (stdoutSize > maxBuffer && !overflowed) {
|
|
215
|
+
overflowed = true;
|
|
216
|
+
const error = new Error("stdout maxBuffer length exceeded");
|
|
217
|
+
error.code = "ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
|
|
218
|
+
child.kill();
|
|
219
|
+
finalizeError(error);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
child.stderr.on("data", (chunk) => {
|
|
224
|
+
stderrSize = pushChunk(stderrChunks, chunk, stderrSize);
|
|
225
|
+
if (stderrSize > maxBuffer && !overflowed) {
|
|
226
|
+
overflowed = true;
|
|
227
|
+
const error = new Error("stderr maxBuffer length exceeded");
|
|
228
|
+
error.code = "ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
|
|
229
|
+
child.kill();
|
|
230
|
+
finalizeError(error);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
child.on("close", (code, signal) => {
|
|
235
|
+
if (finished) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
finished = true;
|
|
239
|
+
if (timeoutId) {
|
|
240
|
+
clearTimeout(timeoutId);
|
|
241
|
+
}
|
|
242
|
+
const stdout = Buffer.concat(stdoutChunks).toString(encoding);
|
|
243
|
+
const stderr = Buffer.concat(stderrChunks).toString(encoding);
|
|
244
|
+
if (timedOut || code !== 0) {
|
|
245
|
+
const error = new Error(
|
|
246
|
+
timedOut
|
|
247
|
+
? `Process timed out after ${timeoutMs}ms`
|
|
248
|
+
: `Process exited with code ${code !== null ? code : "unknown"}`
|
|
249
|
+
);
|
|
250
|
+
error.code = code;
|
|
251
|
+
error.signal = signal;
|
|
252
|
+
error.killed = timedOut;
|
|
151
253
|
error.stdout = stdout;
|
|
152
254
|
error.stderr = stderr;
|
|
153
255
|
reject(error);
|
|
@@ -241,11 +343,12 @@ async function runReviewerWithCodexOnce(options = {}) {
|
|
|
241
343
|
for (const screenshotPath of screenshotPaths || []) {
|
|
242
344
|
args.push("-i", screenshotPath);
|
|
243
345
|
}
|
|
244
|
-
args.push(prompt);
|
|
346
|
+
args.push("--", prompt);
|
|
245
347
|
|
|
246
348
|
const timeoutValue = normalizePositiveInt(timeoutMs, 0, 0, 30 * 60 * 1000);
|
|
349
|
+
let execResult = null;
|
|
247
350
|
try {
|
|
248
|
-
await execFileAsync(codexBin, args, {
|
|
351
|
+
execResult = await execFileAsync(codexBin, args, {
|
|
249
352
|
encoding: "utf8",
|
|
250
353
|
maxBuffer: resolvedMaxBuffer,
|
|
251
354
|
timeout: timeoutValue > 0 ? timeoutValue : undefined
|
|
@@ -272,7 +375,15 @@ async function runReviewerWithCodexOnce(options = {}) {
|
|
|
272
375
|
}
|
|
273
376
|
|
|
274
377
|
if (!pathExists(outputPath)) {
|
|
275
|
-
|
|
378
|
+
const diagnostics = buildReviewerExecutionDiagnostics(
|
|
379
|
+
execResult && execResult.stdout,
|
|
380
|
+
execResult && execResult.stderr
|
|
381
|
+
);
|
|
382
|
+
throw new Error(
|
|
383
|
+
`Reviewer \`${reviewer}\` completed but produced no output JSON.${
|
|
384
|
+
diagnostics.length > 0 ? ` ${diagnostics.join(" | ")}` : ""
|
|
385
|
+
}`
|
|
386
|
+
);
|
|
276
387
|
}
|
|
277
388
|
|
|
278
389
|
return parseReviewerPayload(fs.readFileSync(outputPath, "utf8"));
|