@xenonbyte/da-vinci-workflow 0.2.6 → 0.2.8

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 CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.2.8 - 2026-04-05
4
+
5
+ ### Added
6
+ - optional workflow decision tracing via `.da-vinci/logs/workflow-decisions/YYYY-MM-DD.ndjson`, gated by `DA_VINCI_TRACE_WORKFLOW_DECISIONS=1`
7
+ - compact trace coverage for persisted-state trust, canonical task-group seed fallback, task-group focus override, stale planning-signal fallback, verification freshness downgrade, and worktree-isolation downgrade
8
+ - targeted regression coverage in `scripts/test-workflow-decision-tracing.js` for eligible surfaces, silence rules, sink-write failures, and trace-schema validation behavior
9
+
10
+ ### Changed
11
+ - `workflow-status` and `next-step` now emit bounded decision traces only on the explicitly allowed surfaces, while keeping route/state truth unchanged
12
+ - workflow trace records now reject invalid family/key/outcome combinations with visible diagnostic feedback instead of silently disappearing
13
+ - current release notes in `README.md` and `README.zh-CN.md` now point to the `0.2.8` workflow decision tracing release
14
+
15
+ ### Fixed
16
+ - missing review or verification evidence no longer produces fake `evidenceRefs` in workflow decision traces
17
+ - persisted-state fallback traces no longer imply a fingerprint comparison happened for non-fingerprint fallback paths
18
+ - workflow tracing diagnostics remain non-blocking even when trace persistence fails or candidate trace records are invalid
19
+
20
+ ## v0.2.7 - 2026-04-05
21
+
22
+ ### Added
23
+ - `bounded-worker-isolation-contract` OpenSpec change with six contract slices covering advisory bounded-parallel baseline, isolated workspace rules, worker handoff payloads, sequencing, evidence writeback, and downgrade safety
24
+ - `lib/isolated-worker-handoff.js` plus phase 1-4 regression tests for worker handoff payload constraints and contract closeout checks
25
+
26
+ ### Changed
27
+ - `task-execution`, `task-review`, and `workflow-state` now keep isolated-worker evidence aligned with the new contract, including explicit partial-progress handling, review-order enforcement, out-of-scope-write blocking, and bounded-parallel downgrade visibility
28
+ - `quality:ci:contracts` now includes bounded worker isolation contract regressions instead of relying only on docs/asset consistency lanes
29
+ - supervisor-review reviewer execution now invokes `codex exec` with an explicit prompt separator and closed stdin, matching the real bridge behavior used by the integration smoke
30
+
31
+ ### Fixed
32
+ - reviewer bridge diagnostics now keep `stdout` / `stderr` context when Codex exits without writing the expected structured JSON output
33
+ - supervisor-review CLI and integration smoke fixtures now attach valid exported PNG screenshots so real reviewer runs can execute end to end
34
+ - release docs now reflect the current published version and release highlights
35
+
3
36
  ## v0.2.6 - 2026-04-04
4
37
 
5
38
  ### Added
package/README.md CHANGED
@@ -34,15 +34,15 @@ Use `da-vinci maintainer-readiness` as the canonical maintainer diagnosis surfac
34
34
 
35
35
  Latest published npm package:
36
36
 
37
- - `@xenonbyte/da-vinci-workflow@0.2.6`
37
+ - `@xenonbyte/da-vinci-workflow@0.2.8`
38
38
 
39
- Release highlights for `0.2.6`:
39
+ Release highlights for `0.2.8`:
40
40
 
41
- - quality-gate alignment landed across `lint-spec` + `scope-check` + `lint-tasks`, with shared gate-envelope utilities and explicit clarify/analyze/task-checkpoint routing
42
- - `scope-check` analyze gate now catches empty `pencil-bindings.md` page-traceability drift and hardens orphan-task detection (with planning-anchor aware advisory fallback)
43
- - `lint-tasks` now derives upstream clarify/analyze context from current artifacts when persisted signals are stale, including clarify bounded-context carry-forward
44
- - planning-signal freshness for `lint-tasks` now includes `proposal.md` and page-map dependencies to avoid stale task-checkpoint trust
45
- - workflow promotion and integrity audit now keep clarify bounded context visible as notes while preserving non-blocking bounded semantics
41
+ - optional workflow decision tracing is now available for `workflow-status` and `next-step` through `DA_VINCI_TRACE_WORKFLOW_DECISIONS=1`, with records written to `.da-vinci/logs/workflow-decisions/YYYY-MM-DD.ndjson`
42
+ - the initial trace-family allowlist now covers persisted-state trust, task-group seed fallback, task-group focus override, stale planning-signal fallback, verification freshness downgrade, and worktree-isolation downgrade
43
+ - workflow decision traces remain bounded and diagnostic-only: they do not change routing truth, do not run on non-eligible commands, and do not block normal command execution when trace persistence fails
44
+ - trace records now reject invalid schema combinations with visible diagnostics instead of silently disappearing
45
+ - trace payloads no longer claim missing evidence exists, and persisted fallback traces no longer imply a fingerprint comparison happened when it did not
46
46
 
47
47
  ## Discipline And Orchestration Upgrade
48
48
 
package/README.zh-CN.md CHANGED
@@ -37,15 +37,15 @@ Da Vinci 是一个把产品需求一路推进到结构化规格、Pencil 设计
37
37
 
38
38
  最新已发布 npm 包:
39
39
 
40
- - `@xenonbyte/da-vinci-workflow@0.2.6`
40
+ - `@xenonbyte/da-vinci-workflow@0.2.8`
41
41
 
42
- `0.2.6` 版本重点:
42
+ `0.2.8` 版本重点:
43
43
 
44
- - 完成 `lint-spec`、`scope-check`、`lint-tasks` 质量门对齐,并引入共享 gate-envelope 工具,明确 clarify/analyze/task-checkpoint 路由
45
- - `scope-check` analyze gate 现在会对空 `pencil-bindings.md` 报告页面可追踪性漂移,并强化 orphan task 检测(支持 planning-anchor 降级为 advisory)
46
- - `lint-tasks` persisted 信号过期时会从当前工件重新派生 clarify/analyze 上游上下文,并保留 clarify bounded-context
47
- - `lint-tasks` freshness 依赖新增 `proposal.md` 与 page-map 链路,避免 task-checkpoint 误信陈旧上游信号
48
- - workflow promotion integrity audit 现在都会稳定展示 clarify bounded context 备注,同时保持 bounded 默认非阻断语义
44
+ - 现在可以通过 `DA_VINCI_TRACE_WORKFLOW_DECISIONS=1` 为 `workflow-status` `next-step` 开启可选的 workflow decision tracing,记录会写入 `.da-vinci/logs/workflow-decisions/YYYY-MM-DD.ndjson`
45
+ - 首批 trace-family allowlist 已覆盖 persisted-state trust、task-group seed fallback、task-group focus override、stale planning-signal fallback、verification freshness downgrade 与 worktree-isolation downgrade
46
+ - workflow decision trace 仍然是 bounded 且 diagnostic-only:不会改变 routing truth,不会在非 eligible command 上发射,也不会因为 trace 持久化失败而阻断正常命令
47
+ - trace record 现在会对非法 schema 组合给出可见诊断,不再静默丢失
48
+ - trace payload 不再为缺失证据伪造 `evidenceRefs`,persisted fallback trace 也不再错误暗示某些未发生的 fingerprint comparison
49
49
 
50
50
  ## Discipline And Orchestration 升级
51
51
 
@@ -97,8 +97,10 @@ These commands do not replace route selection, but they support design execution
97
97
  - generates reviewable TODO scaffold templates with framework-aware shape (`next`/`react`/`vue`/`svelte`/`html`)
98
98
  - keeps known implementation landing extension/route shape when a concrete landing already exists
99
99
  - unknown/ambiguous framework detection falls back to HTML with explicit warning; traversal/output-root safety remains enforced
100
- - `da-vinci task-execution --project <path> --change <id> --task-group <id> --status <DONE|DONE_WITH_CONCERNS|NEEDS_CONTEXT|BLOCKED> --summary <text> [--changed-files <csv>] [--test-evidence <csv>] [--concerns <csv>] [--blockers <csv>] [--json]`
100
+ - `da-vinci task-execution --project <path> --change <id> --task-group <id> --status <DONE|DONE_WITH_CONCERNS|NEEDS_CONTEXT|BLOCKED> --summary <text> [--changed-files <csv>] [--test-evidence <csv> --confirm-test-evidence-executed] [--pending-test-evidence <csv>] [--concerns <csv>] [--blockers <csv>] [--out-of-scope-writes <csv>] [--partial] [--json]`
101
101
  - persists normalized implementer-status envelopes into execution signals
102
+ - `--pending-test-evidence` and `--partial` keep the envelope explicitly non-final; `DONE` is invalid when either is present
103
+ - `--out-of-scope-writes` keeps write-scope drift visible to workflow safety handling
102
104
  - use this to keep resume routing machine-readable when implementation is blocked or concerns remain
103
105
  - `da-vinci task-review --project <path> --change <id> --task-group <id> --stage <spec|quality> --status <PASS|WARN|BLOCK> --summary <text> [--issues <csv>] [--reviewer <name>] [--write-verification] [--json]`
104
106
  - persists ordered two-stage task review evidence (`spec` before `quality`)
@@ -97,8 +97,10 @@ Da Vinci 期望它们遵循工作流状态。
97
97
  - 生成 framework-aware 的 TODO 可审查骨架(`next`/`react`/`vue`/`svelte`/`html`)
98
98
  - 若已存在明确实现落点,会优先保留该落点的扩展名与路由形状
99
99
  - 框架未知或冲突时显式告警并回退 HTML;同时继续严格执行 traversal/output-root 安全约束
100
- - `da-vinci task-execution --project <path> --change <id> --task-group <id> --status <DONE|DONE_WITH_CONCERNS|NEEDS_CONTEXT|BLOCKED> --summary <text> [--changed-files <csv>] [--test-evidence <csv>] [--concerns <csv>] [--blockers <csv>] [--json]`
100
+ - `da-vinci task-execution --project <path> --change <id> --task-group <id> --status <DONE|DONE_WITH_CONCERNS|NEEDS_CONTEXT|BLOCKED> --summary <text> [--changed-files <csv>] [--test-evidence <csv> --confirm-test-evidence-executed] [--pending-test-evidence <csv>] [--concerns <csv>] [--blockers <csv>] [--out-of-scope-writes <csv>] [--partial] [--json]`
101
101
  - 持久化结构化 implementer 执行结果包,作为 task 级执行证据
102
+ - `--pending-test-evidence` 与 `--partial` 会将结果明确标记为非终态;此时不得使用 `DONE`
103
+ - `--out-of-scope-writes` 会把写范围漂移显式暴露给 workflow safety 处理
102
104
  - `da-vinci task-review --project <path> --change <id> --task-group <id> --stage <spec|quality> --status <PASS|WARN|BLOCK> --summary <text> [--issues <csv>] [--reviewer <name>] [--write-verification] [--json]`
103
105
  - 持久化有序两阶段 task review 证据(`spec` 在前,`quality` 在后)
104
106
  - `da-vinci worktree-preflight --project <path> [--change <id>] [--json]`
package/lib/cli.js CHANGED
@@ -125,8 +125,10 @@ const OPTION_FLAGS_WITH_VALUES = new Set([
125
125
  "--task-group",
126
126
  "--changed-files",
127
127
  "--test-evidence",
128
+ "--pending-test-evidence",
128
129
  "--concerns",
129
130
  "--blockers",
131
+ "--out-of-scope-writes",
130
132
  "--issues",
131
133
  "--reviewer",
132
134
  "--source",
@@ -199,8 +201,21 @@ const HELP_OPTION_SPECS = [
199
201
  description: "comma-separated changed files for verify-implementation/verify-structure/verify-coverage/task-execution"
200
202
  },
201
203
  { flag: "--test-evidence <csv>", description: "comma-separated test evidence commands for task-execution" },
204
+ {
205
+ flag: "--pending-test-evidence <csv>",
206
+ description: "comma-separated planned-but-not-executed test commands for task-execution"
207
+ },
208
+ {
209
+ flag: "--confirm-test-evidence-executed",
210
+ description: "required when providing --test-evidence; confirms listed commands actually ran"
211
+ },
202
212
  { flag: "--concerns <csv>", description: "comma-separated concern text for task-execution" },
203
213
  { flag: "--blockers <csv>", description: "comma-separated blocker text for task-execution" },
214
+ {
215
+ flag: "--out-of-scope-writes <csv>",
216
+ description: "comma-separated out-of-scope write paths for task-execution safety visibility"
217
+ },
218
+ { flag: "--partial", description: "mark task-execution payload as non-final progress evidence" },
204
219
  { flag: "--issues <csv>", description: "comma-separated issue text for task-review" },
205
220
  { flag: "--reviewer <name>", description: "reviewer identifier for task-review" },
206
221
  { flag: "--write-verification", description: "append task-review evidence into verification.md" },
@@ -560,7 +575,7 @@ function printHelp() {
560
575
  " da-vinci verify-implementation [--project <path>] [--change <id>] [--changed-files <csv>] [--strict] [--json]",
561
576
  " da-vinci verify-structure [--project <path>] [--change <id>] [--changed-files <csv>] [--strict] [--json]",
562
577
  " da-vinci verify-coverage [--project <path>] [--change <id>] [--changed-files <csv>] [--strict] [--json]",
563
- " da-vinci task-execution --project <path> --change <id> --task-group <id> --status <DONE|DONE_WITH_CONCERNS|NEEDS_CONTEXT|BLOCKED> --summary <text> [--changed-files <csv>] [--test-evidence <csv>] [--concerns <csv>] [--blockers <csv>] [--json]",
578
+ " da-vinci task-execution --project <path> --change <id> --task-group <id> --status <DONE|DONE_WITH_CONCERNS|NEEDS_CONTEXT|BLOCKED> --summary <text> [--changed-files <csv>] [--test-evidence <csv> --confirm-test-evidence-executed] [--pending-test-evidence <csv>] [--concerns <csv>] [--blockers <csv>] [--out-of-scope-writes <csv>] [--partial] [--json]",
564
579
  " da-vinci task-review --project <path> --change <id> --task-group <id> --stage <spec|quality> --status <PASS|WARN|BLOCK> --summary <text> [--issues <csv>] [--reviewer <name>] [--write-verification] [--json]",
565
580
  " da-vinci worktree-preflight --project <path> [--change <id>] [--json]",
566
581
  " da-vinci diff-spec [--project <path>] [--change <id>] [--from <sidecars-dir>] [--json]",
@@ -1082,7 +1097,11 @@ async function runCli(argv) {
1082
1097
  if (command === "workflow-status") {
1083
1098
  const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1084
1099
  const changeId = getOption(argv, "--change");
1085
- const result = deriveWorkflowStatus(projectPath, { changeId });
1100
+ const result = deriveWorkflowStatus(projectPath, {
1101
+ changeId,
1102
+ traceSurface: "workflow-status",
1103
+ env: process.env
1104
+ });
1086
1105
 
1087
1106
  if (argv.includes("--json")) {
1088
1107
  console.log(JSON.stringify(result, null, 2));
@@ -1096,7 +1115,11 @@ async function runCli(argv) {
1096
1115
  if (command === "next-step") {
1097
1116
  const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1098
1117
  const changeId = getOption(argv, "--change");
1099
- const result = deriveWorkflowStatus(projectPath, { changeId });
1118
+ const result = deriveWorkflowStatus(projectPath, {
1119
+ changeId,
1120
+ traceSurface: "next-step",
1121
+ env: process.env
1122
+ });
1100
1123
 
1101
1124
  if (argv.includes("--json")) {
1102
1125
  console.log(
@@ -1109,7 +1132,8 @@ async function runCli(argv) {
1109
1132
  discipline: result.discipline || null,
1110
1133
  executionProfile: result.executionProfile || null,
1111
1134
  worktreePreflight: result.worktreePreflight || null,
1112
- verificationFreshness: result.verificationFreshness || null
1135
+ verificationFreshness: result.verificationFreshness || null,
1136
+ traceDiagnostics: result.traceDiagnostics || null
1113
1137
  },
1114
1138
  null,
1115
1139
  2
@@ -1189,8 +1213,12 @@ async function runCli(argv) {
1189
1213
  summary: getOption(argv, "--summary"),
1190
1214
  changedFiles: getCommaSeparatedOptionValues(argv, "--changed-files"),
1191
1215
  testEvidence: getCommaSeparatedOptionValues(argv, "--test-evidence"),
1216
+ pendingTestEvidence: getCommaSeparatedOptionValues(argv, "--pending-test-evidence"),
1217
+ confirmTestEvidenceExecuted: argv.includes("--confirm-test-evidence-executed"),
1192
1218
  concerns: getCommaSeparatedOptionValues(argv, "--concerns"),
1193
- blockers: getCommaSeparatedOptionValues(argv, "--blockers")
1219
+ blockers: getCommaSeparatedOptionValues(argv, "--blockers"),
1220
+ outOfScopeWrites: getCommaSeparatedOptionValues(argv, "--out-of-scope-writes"),
1221
+ partial: argv.includes("--partial")
1194
1222
  });
1195
1223
  const useJson = argv.includes("--json");
1196
1224
  const output = useJson ? JSON.stringify(result, null, 2) : formatTaskExecutionReport(result);
@@ -0,0 +1,181 @@
1
+ const VALID_IMPLEMENTER_RESULT_STATUSES = new Set([
2
+ "DONE",
3
+ "DONE_WITH_CONCERNS",
4
+ "NEEDS_CONTEXT",
5
+ "BLOCKED"
6
+ ]);
7
+
8
+ const IMPLEMENTER_INPUT_REQUIRED_FIELDS = Object.freeze([
9
+ "changeId",
10
+ "taskGroupId",
11
+ "title",
12
+ "executionIntent",
13
+ "targetFiles",
14
+ "fileReferences",
15
+ "reviewIntent",
16
+ "verificationActions",
17
+ "verificationCommands",
18
+ "canonicalProjectRoot",
19
+ "isolatedWorkspaceRoot"
20
+ ]);
21
+
22
+ const IMPLEMENTER_RESULT_REQUIRED_FIELDS = Object.freeze([
23
+ "changeId",
24
+ "taskGroupId",
25
+ "status",
26
+ "summary",
27
+ "changedFiles",
28
+ "testEvidence",
29
+ "concerns",
30
+ "blockers",
31
+ "outOfScopeWrites",
32
+ "recordedAt"
33
+ ]);
34
+
35
+ const IMPLEMENTER_PROGRESS_REQUIRED_FIELDS = Object.freeze([
36
+ ...IMPLEMENTER_RESULT_REQUIRED_FIELDS,
37
+ "partial"
38
+ ]);
39
+
40
+ function normalizeString(value) {
41
+ return String(value || "").trim();
42
+ }
43
+
44
+ function normalizeList(value) {
45
+ const source = Array.isArray(value)
46
+ ? value
47
+ : String(value || "")
48
+ .split(/[,\n;]/)
49
+ .map((item) => item.trim());
50
+ return Array.from(
51
+ new Set(
52
+ source
53
+ .map((item) => String(item || "").trim())
54
+ .filter(Boolean)
55
+ )
56
+ );
57
+ }
58
+
59
+ function assertRequiredFields(payload, requiredFields, label) {
60
+ const missing = requiredFields.filter((field) => !Object.prototype.hasOwnProperty.call(payload || {}, field));
61
+ if (missing.length > 0) {
62
+ throw new Error(`${label} is missing required fields: ${missing.join(", ")}`);
63
+ }
64
+ }
65
+
66
+ function assertNoUnknownFields(payload, allowedFields, label) {
67
+ const allowed = new Set(allowedFields);
68
+ const unknown = Object.keys(payload || {}).filter((field) => !allowed.has(field));
69
+ if (unknown.length > 0) {
70
+ throw new Error(`${label} contains unsupported fields: ${unknown.join(", ")}`);
71
+ }
72
+ }
73
+
74
+ function normalizeStatus(status) {
75
+ const normalized = normalizeString(status).toUpperCase();
76
+ if (!VALID_IMPLEMENTER_RESULT_STATUSES.has(normalized)) {
77
+ throw new Error(
78
+ `isolated implementer result status must be one of ${Array.from(VALID_IMPLEMENTER_RESULT_STATUSES).join(", ")}.`
79
+ );
80
+ }
81
+ return normalized;
82
+ }
83
+
84
+ function normalizeRecordedAt(value, label) {
85
+ const normalized = normalizeString(value);
86
+ if (!normalized) {
87
+ throw new Error(`${label} requires recordedAt.`);
88
+ }
89
+ const parsed = Date.parse(normalized);
90
+ if (!Number.isFinite(parsed)) {
91
+ throw new Error(`${label} recordedAt must be a valid ISO-8601 timestamp.`);
92
+ }
93
+ return new Date(parsed).toISOString();
94
+ }
95
+
96
+ function normalizeIsolatedImplementerInputPayload(payload = {}) {
97
+ assertRequiredFields(payload, IMPLEMENTER_INPUT_REQUIRED_FIELDS, "isolated implementer input payload");
98
+ assertNoUnknownFields(payload, IMPLEMENTER_INPUT_REQUIRED_FIELDS, "isolated implementer input payload");
99
+
100
+ const changeId = normalizeString(payload.changeId);
101
+ const taskGroupId = normalizeString(payload.taskGroupId);
102
+ const title = normalizeString(payload.title);
103
+ const canonicalProjectRoot = normalizeString(payload.canonicalProjectRoot);
104
+ const isolatedWorkspaceRoot = normalizeString(payload.isolatedWorkspaceRoot);
105
+ if (!changeId || !taskGroupId || !title || !canonicalProjectRoot || !isolatedWorkspaceRoot) {
106
+ throw new Error(
107
+ "isolated implementer input payload requires non-empty changeId, taskGroupId, title, canonicalProjectRoot, and isolatedWorkspaceRoot."
108
+ );
109
+ }
110
+
111
+ return {
112
+ changeId,
113
+ taskGroupId,
114
+ title,
115
+ executionIntent: normalizeList(payload.executionIntent),
116
+ targetFiles: normalizeList(payload.targetFiles),
117
+ fileReferences: normalizeList(payload.fileReferences),
118
+ reviewIntent: payload.reviewIntent === true,
119
+ verificationActions: normalizeList(payload.verificationActions),
120
+ verificationCommands: normalizeList(payload.verificationCommands),
121
+ canonicalProjectRoot,
122
+ isolatedWorkspaceRoot
123
+ };
124
+ }
125
+
126
+ function normalizeIsolatedImplementerResultPayload(payload = {}) {
127
+ assertRequiredFields(payload, IMPLEMENTER_RESULT_REQUIRED_FIELDS, "isolated implementer result payload");
128
+ assertNoUnknownFields(payload, IMPLEMENTER_RESULT_REQUIRED_FIELDS, "isolated implementer result payload");
129
+
130
+ const changeId = normalizeString(payload.changeId);
131
+ const taskGroupId = normalizeString(payload.taskGroupId);
132
+ const summary = normalizeString(payload.summary);
133
+ if (!changeId || !taskGroupId || !summary) {
134
+ throw new Error("isolated implementer result payload requires non-empty changeId, taskGroupId, and summary.");
135
+ }
136
+
137
+ return {
138
+ changeId,
139
+ taskGroupId,
140
+ status: normalizeStatus(payload.status),
141
+ summary,
142
+ changedFiles: normalizeList(payload.changedFiles),
143
+ testEvidence: normalizeList(payload.testEvidence),
144
+ concerns: normalizeList(payload.concerns),
145
+ blockers: normalizeList(payload.blockers),
146
+ outOfScopeWrites: normalizeList(payload.outOfScopeWrites),
147
+ recordedAt: normalizeRecordedAt(payload.recordedAt, "isolated implementer result payload")
148
+ };
149
+ }
150
+
151
+ function normalizeIsolatedImplementerProgressPayload(payload = {}) {
152
+ assertRequiredFields(payload, IMPLEMENTER_PROGRESS_REQUIRED_FIELDS, "isolated implementer progress payload");
153
+ assertNoUnknownFields(payload, IMPLEMENTER_PROGRESS_REQUIRED_FIELDS, "isolated implementer progress payload");
154
+ if (payload.partial !== true) {
155
+ throw new Error("isolated implementer progress payload requires partial=true.");
156
+ }
157
+
158
+ const {
159
+ partial: _partial,
160
+ ...resultShape
161
+ } = payload;
162
+ const normalizedResult = normalizeIsolatedImplementerResultPayload(resultShape);
163
+ if (normalizedResult.status === "DONE") {
164
+ throw new Error("isolated implementer progress payload cannot use status DONE because partial snapshots are non-final.");
165
+ }
166
+
167
+ return {
168
+ ...normalizedResult,
169
+ partial: true
170
+ };
171
+ }
172
+
173
+ module.exports = {
174
+ VALID_IMPLEMENTER_RESULT_STATUSES,
175
+ IMPLEMENTER_INPUT_REQUIRED_FIELDS,
176
+ IMPLEMENTER_RESULT_REQUIRED_FIELDS,
177
+ IMPLEMENTER_PROGRESS_REQUIRED_FIELDS,
178
+ normalizeIsolatedImplementerInputPayload,
179
+ normalizeIsolatedImplementerResultPayload,
180
+ normalizeIsolatedImplementerProgressPayload
181
+ };
@@ -1,7 +1,7 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
  const os = require("os");
4
- const { execFile } = require("child_process");
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
- execFile(command, args, options, (error, stdout, stderr) => {
150
- if (error) {
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
- throw new Error(`Reviewer \`${reviewer}\` completed but produced no output JSON.`);
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"));