@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 +33 -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 +33 -5
- 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-decision-trace.js +335 -0
- package/lib/workflow-state.js +462 -63
- package/package.json +3 -2
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.
|
|
37
|
+
- `@xenonbyte/da-vinci-workflow@0.2.8`
|
|
38
38
|
|
|
39
|
-
Release highlights for `0.2.
|
|
39
|
+
Release highlights for `0.2.8`:
|
|
40
40
|
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
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.
|
|
40
|
+
- `@xenonbyte/da-vinci-workflow@0.2.8`
|
|
41
41
|
|
|
42
|
-
`0.2.
|
|
42
|
+
`0.2.8` 版本重点:
|
|
43
43
|
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
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, {
|
|
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, {
|
|
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
|
+
};
|
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"));
|