@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 +16 -0
- package/README.md +7 -7
- package/README.zh-CN.md +7 -7
- package/docs/dv-command-reference.md +3 -1
- package/docs/zh-CN/dv-command-reference.md +3 -1
- package/lib/cli.js +21 -2
- package/lib/isolated-worker-handoff.js +181 -0
- package/lib/supervisor-review.js +117 -6
- package/lib/task-execution.js +88 -16
- package/lib/task-review.js +12 -7
- package/lib/workflow-state.js +54 -0
- package/package.json +3 -2
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.
|
|
37
|
+
- `@xenonbyte/da-vinci-workflow@0.2.7`
|
|
38
38
|
|
|
39
|
-
Release highlights for `0.2.
|
|
39
|
+
Release highlights for `0.2.7`:
|
|
40
40
|
|
|
41
|
-
-
|
|
42
|
-
- `
|
|
43
|
-
- `
|
|
44
|
-
-
|
|
45
|
-
-
|
|
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.
|
|
40
|
+
- `@xenonbyte/da-vinci-workflow@0.2.7`
|
|
41
41
|
|
|
42
|
-
`0.2.
|
|
42
|
+
`0.2.7` 版本重点:
|
|
43
43
|
|
|
44
|
-
-
|
|
45
|
-
- `
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
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
|
+
};
|
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"));
|
package/lib/task-execution.js
CHANGED
|
@@ -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(
|
|
99
|
-
status:
|
|
142
|
+
surface: buildSurface(envelopeWithIdentity.taskGroupId),
|
|
143
|
+
status: signalStatus,
|
|
100
144
|
advisory: false,
|
|
101
145
|
strict: true,
|
|
102
|
-
failures:
|
|
146
|
+
failures: envelopeWithIdentity.status === "BLOCKED" ? envelopeWithIdentity.blockers : [],
|
|
103
147
|
warnings:
|
|
104
|
-
|
|
105
|
-
?
|
|
106
|
-
: [],
|
|
107
|
-
notes: [
|
|
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:
|
|
175
|
+
status: signalStatus,
|
|
116
176
|
projectRoot,
|
|
117
177
|
changeId: resolved.changeId,
|
|
118
|
-
taskGroupId:
|
|
119
|
-
implementerStatus:
|
|
120
|
-
summary:
|
|
121
|
-
changedFiles:
|
|
122
|
-
testEvidence:
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
package/lib/task-review.js
CHANGED
|
@@ -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(
|
|
137
|
-
status: mapReviewStatusToSignalStatus(
|
|
141
|
+
surface: buildTaskReviewSurface(envelopeWithIdentity.taskGroupId, envelopeWithIdentity.stage),
|
|
142
|
+
status: mapReviewStatusToSignalStatus(envelopeWithIdentity.status),
|
|
138
143
|
advisory: false,
|
|
139
144
|
strict: true,
|
|
140
|
-
failures:
|
|
141
|
-
warnings:
|
|
142
|
-
notes: [
|
|
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),
|
|
158
|
+
const nextVerification = appendTaskReviewEvidence(readTextIfExists(verificationPath), envelopeWithIdentity);
|
|
154
159
|
writeFileAtomic(verificationPath, nextVerification);
|
|
155
160
|
}
|
|
156
161
|
|
package/lib/workflow-state.js
CHANGED
|
@@ -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.
|
|
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"
|