@xenonbyte/da-vinci-workflow 0.2.6 → 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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.2.7 - 2026-04-05
4
+
5
+ ### Added
6
+ - `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
7
+ - `lib/isolated-worker-handoff.js` plus phase 1-4 regression tests for worker handoff payload constraints and contract closeout checks
8
+
9
+ ### Changed
10
+ - `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
11
+ - `quality:ci:contracts` now includes bounded worker isolation contract regressions instead of relying only on docs/asset consistency lanes
12
+ - 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
13
+
14
+ ### Fixed
15
+ - reviewer bridge diagnostics now keep `stdout` / `stderr` context when Codex exits without writing the expected structured JSON output
16
+ - supervisor-review CLI and integration smoke fixtures now attach valid exported PNG screenshots so real reviewer runs can execute end to end
17
+ - release docs now reflect the current published version and release highlights
18
+
3
19
  ## v0.2.6 - 2026-04-04
4
20
 
5
21
  ### 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.7`
38
38
 
39
- Release highlights for `0.2.6`:
39
+ Release highlights for `0.2.7`:
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
+ - bounded worker isolation is now formalized as a contract-only OpenSpec change, with explicit task-group ownership, isolated workspace, handoff, sequencing, writeback, and downgrade rules
42
+ - `task-execution`, `task-review`, and `workflow-state` now align with that contract by blocking out-of-scope writes, preserving review ordering, and rejecting `partial=true` + `DONE`
43
+ - the reviewer bridge now runs `codex exec` with closed stdin plus explicit prompt separation, and preserves raw diagnostics when structured reviewer output is missing
44
+ - contract CI now exercises bounded-worker-isolation phase 1-4 tests directly instead of relying only on docs/asset consistency checks
45
+ - supervisor-review smoke fixtures now use valid exported screenshots, and the real reviewer bridge integration passes end to end
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.7`
41
41
 
42
- `0.2.6` 版本重点:
42
+ `0.2.7` 版本重点:
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
+ - `bounded-worker-isolation-contract` 现已作为 contract-only OpenSpec 变更落地,明确 task-group ownership、isolated workspace、handoff、sequencing、writeback 与 downgrade 规则
45
+ - `task-execution`、`task-review`、`workflow-state` 现已与该 contract 对齐:会阻断 out-of-scope writes,保持 review 顺序,并拒绝 `partial=true` `DONE` 组合
46
+ - reviewer bridge 现在会以显式 prompt 分隔并关闭 stdin 的方式运行 `codex exec`,同时在 reviewer 缺失结构化输出时保留原始诊断
47
+ - contract CI 现在会直接跑 bounded-worker-isolation phase 1-4 回归,不再只依赖文档/命令资产一致性检查
48
+ - supervisor-review smoke fixture 现在使用有效截图,真实 reviewer bridge integration 已能端到端通过
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]",
@@ -1189,8 +1204,12 @@ async function runCli(argv) {
1189
1204
  summary: getOption(argv, "--summary"),
1190
1205
  changedFiles: getCommaSeparatedOptionValues(argv, "--changed-files"),
1191
1206
  testEvidence: getCommaSeparatedOptionValues(argv, "--test-evidence"),
1207
+ pendingTestEvidence: getCommaSeparatedOptionValues(argv, "--pending-test-evidence"),
1208
+ confirmTestEvidenceExecuted: argv.includes("--confirm-test-evidence-executed"),
1192
1209
  concerns: getCommaSeparatedOptionValues(argv, "--concerns"),
1193
- blockers: getCommaSeparatedOptionValues(argv, "--blockers")
1210
+ blockers: getCommaSeparatedOptionValues(argv, "--blockers"),
1211
+ outOfScopeWrites: getCommaSeparatedOptionValues(argv, "--out-of-scope-writes"),
1212
+ partial: argv.includes("--partial")
1194
1213
  });
1195
1214
  const useJson = argv.includes("--json");
1196
1215
  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"));
@@ -65,8 +65,37 @@ function normalizeTaskExecutionEnvelope(input = {}) {
65
65
  }
66
66
  const changedFiles = normalizeList(input.changedFiles);
67
67
  const testEvidence = normalizeList(input.testEvidence);
68
+ const pendingTestEvidence = normalizeList(input.pendingTestEvidence);
69
+ const confirmTestEvidenceExecuted = input.confirmTestEvidenceExecuted === true;
68
70
  const concerns = normalizeList(input.concerns);
69
71
  const blockers = normalizeList(input.blockers);
72
+ const outOfScopeWrites = normalizeList(input.outOfScopeWrites);
73
+ const partial = input.partial === true;
74
+
75
+ const overlap = testEvidence.filter((item) => pendingTestEvidence.includes(item));
76
+ if (overlap.length > 0) {
77
+ throw new Error(
78
+ `task-execution cannot mark the same command as both executed and pending: ${overlap.join(", ")}`
79
+ );
80
+ }
81
+
82
+ if (testEvidence.length > 0 && !confirmTestEvidenceExecuted) {
83
+ throw new Error(
84
+ "`task-execution` requires explicit executed-evidence confirmation when `--test-evidence` is provided. Pass `--confirm-test-evidence-executed`."
85
+ );
86
+ }
87
+
88
+ if (pendingTestEvidence.length > 0 && status === "DONE") {
89
+ throw new Error(
90
+ "`task-execution` cannot use status DONE when pending test evidence exists. Use DONE_WITH_CONCERNS, NEEDS_CONTEXT, or BLOCKED."
91
+ );
92
+ }
93
+
94
+ if (partial && status === "DONE") {
95
+ throw new Error(
96
+ "`task-execution` cannot use status DONE when --partial is set. Use DONE_WITH_CONCERNS, NEEDS_CONTEXT, or BLOCKED."
97
+ );
98
+ }
70
99
 
71
100
  return {
72
101
  taskGroupId,
@@ -74,8 +103,12 @@ function normalizeTaskExecutionEnvelope(input = {}) {
74
103
  summary,
75
104
  changedFiles,
76
105
  testEvidence,
106
+ pendingTestEvidence,
107
+ confirmTestEvidenceExecuted,
77
108
  concerns,
78
109
  blockers,
110
+ outOfScopeWrites,
111
+ partial,
79
112
  recordedAt: new Date().toISOString()
80
113
  };
81
114
  }
@@ -93,35 +126,65 @@ function writeTaskExecutionEnvelope(projectPathInput, options = {}) {
93
126
  }
94
127
 
95
128
  const envelope = normalizeTaskExecutionEnvelope(options);
129
+ const envelopeWithIdentity = {
130
+ ...envelope,
131
+ changeId: resolved.changeId
132
+ };
133
+ const signalStatus = mapImplementerStatusToSignal(envelopeWithIdentity.status);
134
+ const outOfScopeWriteWarnings = envelopeWithIdentity.outOfScopeWrites.map(
135
+ (item) => `out-of-scope write: ${item}`
136
+ );
137
+ const pendingTestWarnings = envelopeWithIdentity.pendingTestEvidence.map(
138
+ (item) => `test not executed: ${item}`
139
+ );
96
140
  const signalPath = writeExecutionSignal(projectRoot, {
97
141
  changeId: resolved.changeId,
98
- surface: buildSurface(envelope.taskGroupId),
99
- status: mapImplementerStatusToSignal(envelope.status),
142
+ surface: buildSurface(envelopeWithIdentity.taskGroupId),
143
+ status: signalStatus,
100
144
  advisory: false,
101
145
  strict: true,
102
- failures: envelope.status === "BLOCKED" ? envelope.blockers : [],
146
+ failures: envelopeWithIdentity.status === "BLOCKED" ? envelopeWithIdentity.blockers : [],
103
147
  warnings:
104
- envelope.status === "DONE_WITH_CONCERNS" || envelope.status === "NEEDS_CONTEXT"
105
- ? envelope.concerns
106
- : [],
107
- notes: [envelope.summary, ...envelope.testEvidence.map((item) => `test: ${item}`)],
148
+ envelopeWithIdentity.status === "DONE_WITH_CONCERNS" || envelopeWithIdentity.status === "NEEDS_CONTEXT"
149
+ ? unique([...envelopeWithIdentity.concerns, ...outOfScopeWriteWarnings, ...pendingTestWarnings])
150
+ : unique([...outOfScopeWriteWarnings, ...pendingTestWarnings]),
151
+ notes: [
152
+ envelopeWithIdentity.summary,
153
+ ...envelopeWithIdentity.testEvidence.map((item) => `test: ${item}`)
154
+ ],
108
155
  details: {
109
156
  type: "task_execution",
110
- envelope
157
+ envelope: {
158
+ taskGroupId: envelopeWithIdentity.taskGroupId,
159
+ changeId: envelopeWithIdentity.changeId,
160
+ status: envelopeWithIdentity.status,
161
+ summary: envelopeWithIdentity.summary,
162
+ changedFiles: envelopeWithIdentity.changedFiles,
163
+ testEvidence: envelopeWithIdentity.testEvidence,
164
+ pendingTestEvidence: envelopeWithIdentity.pendingTestEvidence,
165
+ concerns: envelopeWithIdentity.concerns,
166
+ blockers: envelopeWithIdentity.blockers,
167
+ recordedAt: envelopeWithIdentity.recordedAt
168
+ },
169
+ outOfScopeWrites: envelopeWithIdentity.outOfScopeWrites,
170
+ partial: envelopeWithIdentity.partial
111
171
  }
112
172
  });
113
173
 
114
174
  return {
115
- status: mapImplementerStatusToSignal(envelope.status),
175
+ status: signalStatus,
116
176
  projectRoot,
117
177
  changeId: resolved.changeId,
118
- taskGroupId: envelope.taskGroupId,
119
- implementerStatus: envelope.status,
120
- summary: envelope.summary,
121
- changedFiles: envelope.changedFiles,
122
- testEvidence: envelope.testEvidence,
123
- concerns: envelope.concerns,
124
- blockers: envelope.blockers,
178
+ taskGroupId: envelopeWithIdentity.taskGroupId,
179
+ implementerStatus: envelopeWithIdentity.status,
180
+ summary: envelopeWithIdentity.summary,
181
+ changedFiles: envelopeWithIdentity.changedFiles,
182
+ testEvidence: envelopeWithIdentity.testEvidence,
183
+ pendingTestEvidence: envelopeWithIdentity.pendingTestEvidence,
184
+ concerns: envelopeWithIdentity.concerns,
185
+ blockers: envelopeWithIdentity.blockers,
186
+ outOfScopeWrites: envelopeWithIdentity.outOfScopeWrites,
187
+ partial: envelopeWithIdentity.partial,
125
188
  signalPath
126
189
  };
127
190
  }
@@ -143,12 +206,21 @@ function formatTaskExecutionReport(result) {
143
206
  if (result.testEvidence.length > 0) {
144
207
  lines.push(`Test evidence: ${result.testEvidence.join(", ")}`);
145
208
  }
209
+ if (result.pendingTestEvidence.length > 0) {
210
+ lines.push(`Pending test evidence: ${result.pendingTestEvidence.join(", ")}`);
211
+ }
146
212
  if (result.concerns.length > 0) {
147
213
  lines.push(`Concerns: ${result.concerns.join(", ")}`);
148
214
  }
149
215
  if (result.blockers.length > 0) {
150
216
  lines.push(`Blockers: ${result.blockers.join(", ")}`);
151
217
  }
218
+ if (result.outOfScopeWrites.length > 0) {
219
+ lines.push(`Out-of-scope writes: ${result.outOfScopeWrites.join(", ")}`);
220
+ }
221
+ if (result.partial) {
222
+ lines.push("Partial: true");
223
+ }
152
224
  return lines.join("\n");
153
225
  }
154
226
 
@@ -131,18 +131,23 @@ function writeTaskReviewEnvelope(projectPathInput, options = {}) {
131
131
  }
132
132
  }
133
133
 
134
+ const envelopeWithIdentity = {
135
+ ...envelope,
136
+ changeId: resolved.changeId
137
+ };
138
+
134
139
  const signalPath = writeExecutionSignal(projectRoot, {
135
140
  changeId: resolved.changeId,
136
- surface: buildTaskReviewSurface(envelope.taskGroupId, envelope.stage),
137
- status: mapReviewStatusToSignalStatus(envelope.status),
141
+ surface: buildTaskReviewSurface(envelopeWithIdentity.taskGroupId, envelopeWithIdentity.stage),
142
+ status: mapReviewStatusToSignalStatus(envelopeWithIdentity.status),
138
143
  advisory: false,
139
144
  strict: true,
140
- failures: envelope.status === "BLOCK" ? envelope.issues : [],
141
- warnings: envelope.status === "WARN" ? envelope.issues : [],
142
- notes: [envelope.summary, `reviewer: ${envelope.reviewer}`],
145
+ failures: envelopeWithIdentity.status === "BLOCK" ? envelopeWithIdentity.issues : [],
146
+ warnings: envelopeWithIdentity.status === "WARN" ? envelopeWithIdentity.issues : [],
147
+ notes: [envelopeWithIdentity.summary, `reviewer: ${envelopeWithIdentity.reviewer}`],
143
148
  details: {
144
149
  type: "task_review",
145
- envelope
150
+ envelope: envelopeWithIdentity
146
151
  }
147
152
  });
148
153
 
@@ -150,7 +155,7 @@ function writeTaskReviewEnvelope(projectPathInput, options = {}) {
150
155
  if (options.writeVerification === true) {
151
156
  verificationPath = path.join(resolved.changeDir, "verification.md");
152
157
  fs.mkdirSync(path.dirname(verificationPath), { recursive: true });
153
- const nextVerification = appendTaskReviewEvidence(readTextIfExists(verificationPath), envelope);
158
+ const nextVerification = appendTaskReviewEvidence(readTextIfExists(verificationPath), envelopeWithIdentity);
154
159
  writeFileAtomic(verificationPath, nextVerification);
155
160
  }
156
161
 
@@ -436,6 +436,10 @@ function applyTaskExecutionAndReviewFindings(findings, signals) {
436
436
 
437
437
  for (const signal of Object.values(latestTaskExecution)) {
438
438
  const envelope = signal.details && signal.details.envelope ? signal.details.envelope : null;
439
+ const outOfScopeWrites =
440
+ signal.details && Array.isArray(signal.details.outOfScopeWrites)
441
+ ? dedupeMessages(signal.details.outOfScopeWrites.map((item) => String(item || "").trim()).filter(Boolean))
442
+ : [];
439
443
  const taskGroupId =
440
444
  (envelope && envelope.taskGroupId) ||
441
445
  String(signal.surface || "").replace(/^task-execution\./, "") ||
@@ -445,6 +449,11 @@ function applyTaskExecutionAndReviewFindings(findings, signals) {
445
449
  } else if (signal.status === STATUS.WARN) {
446
450
  findings.warnings.push(`Task group ${taskGroupId} has unresolved implementer concerns/context needs.`);
447
451
  }
452
+ if (outOfScopeWrites.length > 0) {
453
+ findings.blockers.push(
454
+ `Task group ${taskGroupId} reported out-of-scope writes: ${outOfScopeWrites.join(", ")}.`
455
+ );
456
+ }
448
457
  if (envelope && envelope.summary) {
449
458
  findings.notes.push(`Implementer summary ${taskGroupId}: ${envelope.summary}`);
450
459
  }
@@ -476,6 +485,12 @@ function applyTaskExecutionAndReviewFindings(findings, signals) {
476
485
  );
477
486
  continue;
478
487
  }
488
+ if (state.quality && state.spec === STATUS.WARN) {
489
+ findings.blockers.push(
490
+ `Task review ordering violation for ${taskGroupId}: quality review was recorded before spec review reached PASS.`
491
+ );
492
+ continue;
493
+ }
479
494
  if (state.quality && state.spec === STATUS.BLOCK) {
480
495
  findings.blockers.push(
481
496
  `Task review ordering violation for ${taskGroupId}: quality review was recorded while spec review is BLOCK.`
@@ -1185,6 +1200,7 @@ function buildTaskGroupImplementerState(taskGroupId, signals, fallbackState) {
1185
1200
  testEvidence: Array.isArray(fallback.testEvidence) ? fallback.testEvidence : [],
1186
1201
  concerns: Array.isArray(fallback.concerns) ? fallback.concerns : [],
1187
1202
  blockers: Array.isArray(fallback.blockers) ? fallback.blockers : [],
1203
+ outOfScopeWrites: Array.isArray(fallback.outOfScopeWrites) ? fallback.outOfScopeWrites : [],
1188
1204
  recordedAt: fallback.recordedAt || null
1189
1205
  };
1190
1206
  }
@@ -1208,6 +1224,12 @@ function buildTaskGroupImplementerState(taskGroupId, signals, fallbackState) {
1208
1224
  : [],
1209
1225
  concerns: summarizeSignalIssues(signal, envelope && envelope.concerns),
1210
1226
  blockers: summarizeSignalIssues(signal, envelope && envelope.blockers),
1227
+ outOfScopeWrites:
1228
+ signal.details && Array.isArray(signal.details.outOfScopeWrites)
1229
+ ? dedupeMessages(signal.details.outOfScopeWrites.map((item) => String(item || "").trim()).filter(Boolean))
1230
+ : Array.isArray(fallback.outOfScopeWrites)
1231
+ ? fallback.outOfScopeWrites
1232
+ : [],
1211
1233
  recordedAt: (envelope && envelope.recordedAt) || signal.timestamp || fallback.recordedAt || null
1212
1234
  };
1213
1235
  }
@@ -1262,6 +1284,21 @@ function buildEffectiveTaskGroupState(group, planned, implementer, review) {
1262
1284
  reason: "planned_checklist"
1263
1285
  };
1264
1286
 
1287
+ if (implementer.present && implementer.outOfScopeWrites.length > 0) {
1288
+ return {
1289
+ status: "blocked",
1290
+ nextAction:
1291
+ `resolve out-of-scope writes for task group ${group.taskGroupId}: ${implementer.outOfScopeWrites.join(", ")}`,
1292
+ resumeCursor: {
1293
+ groupIndex: fallbackCursor.groupIndex,
1294
+ nextUncheckedItem: null,
1295
+ liveFocus: "out_of_scope_write"
1296
+ },
1297
+ source: "implementer",
1298
+ reason: "out_of_scope_write"
1299
+ };
1300
+ }
1301
+
1265
1302
  if (implementer.present && implementer.signalStatus === STATUS.BLOCK) {
1266
1303
  return {
1267
1304
  status: "blocked",
@@ -1288,6 +1325,23 @@ function buildEffectiveTaskGroupState(group, planned, implementer, review) {
1288
1325
  const reviewContextReady = review.required && (reviewSignalsPresent || reviewHardDue || implementer.present);
1289
1326
 
1290
1327
  if (reviewContextReady) {
1328
+ if (
1329
+ review.quality.present &&
1330
+ (!review.spec.present || review.spec.status === "missing" || review.spec.status === STATUS.WARN)
1331
+ ) {
1332
+ return {
1333
+ status: "blocked",
1334
+ nextAction:
1335
+ `remove or rerun out-of-order quality review for task group ${group.taskGroupId} after spec review PASS`,
1336
+ resumeCursor: {
1337
+ groupIndex: fallbackCursor.groupIndex,
1338
+ nextUncheckedItem: null,
1339
+ liveFocus: "review_ordering_violation"
1340
+ },
1341
+ source: "review",
1342
+ reason: "review_ordering_violation"
1343
+ };
1344
+ }
1291
1345
  if (review.spec.status === STATUS.BLOCK) {
1292
1346
  return {
1293
1347
  status: "blocked",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenonbyte/da-vinci-workflow",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Requirement-to-design-to-code workflow skill for Codex, Claude, and Gemini",
5
5
  "bin": {
6
6
  "da-vinci": "bin/da-vinci.js",
@@ -53,8 +53,9 @@
53
53
  "test:workflow-persisted-state": "node scripts/test-workflow-persisted-state.js",
54
54
  "test:install": "node scripts/test-install.js",
55
55
  "test:package-contents": "node scripts/test-package-contents.js",
56
+ "test:bounded-worker-isolation:contracts": "node scripts/test-bounded-worker-isolation-phase1.js && node scripts/test-bounded-worker-isolation-phase2.js && node scripts/test-bounded-worker-isolation-phase3.js && node scripts/test-bounded-worker-isolation-phase4.js",
56
57
  "quality:ci:core": "npm run test",
57
- "quality:ci:contracts": "npm run test:mode-consistency && npm run test:command-assets",
58
+ "quality:ci:contracts": "npm run test:mode-consistency && npm run test:command-assets && npm run test:bounded-worker-isolation:contracts",
58
59
  "quality:ci:e2e": "npm run test:lint-planning && npm run test:sidecars-diff && npm run test:verify-scaffold && npm run test:workflow-persisted-state && npm run test:audit-execution-signals",
59
60
  "quality:reviewer-bridge-smoke": "npm run test:supervisor-review-integration",
60
61
  "quality:ci": "npm run quality:ci:core && npm run quality:ci:contracts && npm run quality:ci:e2e"