scene-capability-engine 3.5.1 → 3.5.2
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 +15 -0
- package/README.md +12 -1
- package/README.zh.md +12 -1
- package/docs/command-reference.md +14 -2
- package/lib/commands/studio.js +793 -24
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.5.2] - 2026-03-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Studio task-stream contract in command JSON payloads:
|
|
14
|
+
- root IDs: `sessionId`, `sceneId`, `specId`, `taskId`, `eventId`
|
|
15
|
+
- normalized task payload: `task.goal`, `task.status`, `task.summary`, `task.handoff`, `task.next_action`
|
|
16
|
+
- UI-ready arrays: `task.file_changes[]`, `task.commands[]`, `task.errors[]`, `task.evidence[]`
|
|
17
|
+
- raw audit passthrough: `event[]` (with `studio events` keeping `events[]` compatibility)
|
|
18
|
+
- OpenHands bridge for task stream:
|
|
19
|
+
- `sce studio events --openhands-events <path>`
|
|
20
|
+
- supports `.json`/`.jsonl` raw OpenHands events ingestion and maps to SCE task contract fields
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- `studio verify/release` now persist `gate_steps` in stage metadata so command execution details can be surfaced in `task.commands[]` and `task.errors[]`.
|
|
24
|
+
|
|
10
25
|
## [3.5.1] - 2026-03-03
|
|
11
26
|
|
|
12
27
|
### Added
|
package/README.md
CHANGED
|
@@ -128,10 +128,21 @@ SCE is tool-agnostic and works with Codex, Claude Code, Cursor, Windsurf, VS Cod
|
|
|
128
128
|
- `sce workspace takeover-audit --json`
|
|
129
129
|
- `sce workspace takeover-apply --json`
|
|
130
130
|
|
|
131
|
+
Studio task-stream output contract (default):
|
|
132
|
+
- IDs: `sessionId`, `sceneId`, `specId`, `taskId`, `eventId`
|
|
133
|
+
- Task: `task.goal`, `task.status`, `task.summary` (3-line), `task.handoff`, `task.next_action`
|
|
134
|
+
- File refs: `task.file_changes[]` with `path`, `line`, `diffRef`
|
|
135
|
+
- Command logs: `task.commands[]` with `cmd`, `exit_code`, `stdout`, `stderr`, `log_path`
|
|
136
|
+
- Errors: `task.errors[]` with `message`, `error_bundle` (copy-ready)
|
|
137
|
+
- Evidence: `task.evidence[]`
|
|
138
|
+
- Raw audit stream: `event[]` (and `studio events` keeps `events[]` compatibility field)
|
|
139
|
+
- OpenHands bridge: `sce studio events --openhands-events <path>` maps OpenHands raw events into the same task contract (`source_stream=openhands`)
|
|
140
|
+
|
|
131
141
|
---
|
|
132
142
|
|
|
133
143
|
## Important Version Changes
|
|
134
144
|
|
|
145
|
+
- `3.5.2`: Introduced task-stream output contract for Studio commands (`sessionId/sceneId/specId/taskId/eventId`, structured `task.*` fields, `event[]` audit stream) and added OpenHands raw-event bridge via `sce studio events --openhands-events <path>`.
|
|
135
146
|
- `3.5.1`: Enforced stricter Studio intake defaults (`--manual-spec` and `--no-spec-governance` blocked unless policy override), added historical spec scene backfill command (`sce studio backfill-spec-scenes`) and persisted override mapping (`.sce/spec-governance/spec-scene-overrides.json`) for portfolio/related-spec alignment.
|
|
136
147
|
- `3.5.0`: Added Studio automatic goal intake + scene spec portfolio governance (`sce studio intake`, `sce studio portfolio`), including default intake policy baseline and governance artifacts for bounded scene spec growth.
|
|
137
148
|
- `3.4.6`: Added default `problem-closure-gate` + `problem-contract` baseline and strengthened mandatory problem evaluation dimensions (`problem_contract`/`ontology_alignment`/`convergence`) for verify/release convergence control.
|
|
@@ -180,5 +191,5 @@ MIT. See [LICENSE](LICENSE).
|
|
|
180
191
|
|
|
181
192
|
---
|
|
182
193
|
|
|
183
|
-
**Version**: 3.5.
|
|
194
|
+
**Version**: 3.5.2
|
|
184
195
|
**Last Updated**: 2026-03-03
|
package/README.zh.md
CHANGED
|
@@ -128,10 +128,21 @@ SCE 对工具无锁定,可接入 Codex、Claude Code、Cursor、Windsurf、VS
|
|
|
128
128
|
- `sce workspace takeover-audit --json`
|
|
129
129
|
- `sce workspace takeover-apply --json`
|
|
130
130
|
|
|
131
|
+
Studio 任务流输出契约(默认):
|
|
132
|
+
- ID 字段:`sessionId`、`sceneId`、`specId`、`taskId`、`eventId`
|
|
133
|
+
- 任务主体:`task.goal`、`task.status`、`task.summary`(固定三行)、`task.handoff`、`task.next_action`
|
|
134
|
+
- 文件引用:`task.file_changes[]`(`path`、`line`、`diffRef`)
|
|
135
|
+
- 命令执行:`task.commands[]`(`cmd`、`exit_code`、`stdout`、`stderr`、`log_path`)
|
|
136
|
+
- 错误复制:`task.errors[]`(`message`、`error_bundle`,可直接复制给 AI 修复)
|
|
137
|
+
- 证据引用:`task.evidence[]`
|
|
138
|
+
- 原始审计流:`event[]`(`studio events` 同时保留 `events[]` 兼容字段)
|
|
139
|
+
- OpenHands 桥接:`sce studio events --openhands-events <path>` 可将 OpenHands 原始事件映射为同一 task 契约(`source_stream=openhands`)
|
|
140
|
+
|
|
131
141
|
---
|
|
132
142
|
|
|
133
143
|
## 重要版本变更
|
|
134
144
|
|
|
145
|
+
- `3.5.2`:新增 Studio 任务流输出契约(`sessionId/sceneId/specId/taskId/eventId`、结构化 `task.*` 字段、`event[]` 审计流),并新增 OpenHands 原始事件桥接能力:`sce studio events --openhands-events <path>`。
|
|
135
146
|
- `3.5.1`:默认强化 Studio intake 治理(`--manual-spec`、`--no-spec-governance` 在未显式放开策略时会被阻断),新增历史 spec 场景回填命令 `sce studio backfill-spec-scenes`,并写入 `.sce/spec-governance/spec-scene-overrides.json` 以统一 portfolio 与 related-spec 的场景映射。
|
|
136
147
|
- `3.5.0`:新增 Studio 目标自动 intake + 场景 spec 组合治理(`sce studio intake`、`sce studio portfolio`),并默认启用 intake 策略基线与治理快照产物,控制场景内 spec 无序增长。
|
|
137
148
|
- `3.4.6`:新增默认 `problem-closure-gate` + `problem-contract` 基线,并强化问题评估强制维度(`problem_contract`/`ontology_alignment`/`convergence`),提升 verify/release 收敛控制。
|
|
@@ -180,5 +191,5 @@ MIT,见 [LICENSE](LICENSE)。
|
|
|
180
191
|
|
|
181
192
|
---
|
|
182
193
|
|
|
183
|
-
**版本**:3.5.
|
|
194
|
+
**版本**:3.5.2
|
|
184
195
|
**最后更新**:2026-03-03
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
> Quick reference for all `sce` commands
|
|
4
4
|
|
|
5
|
-
**Version**: 3.5.
|
|
6
|
-
**Last Updated**: 2026-03-
|
|
5
|
+
**Version**: 3.5.2
|
|
6
|
+
**Last Updated**: 2026-03-03
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -550,6 +550,8 @@ sce studio resume --job <job-id> --json
|
|
|
550
550
|
|
|
551
551
|
# Inspect recent stage events
|
|
552
552
|
sce studio events --job <job-id> --limit 50 --json
|
|
553
|
+
# Map OpenHands raw events into the same task-stream contract
|
|
554
|
+
sce studio events --job <job-id> --openhands-events ./openhands-events.json --json
|
|
553
555
|
|
|
554
556
|
# Rollback a job after apply/release
|
|
555
557
|
sce studio rollback --job <job-id> --reason "manual-check-failed" --json
|
|
@@ -567,6 +569,16 @@ sce studio backfill-spec-scenes --scene scene.unassigned --limit 20 --apply --js
|
|
|
567
569
|
SCE_STUDIO_REQUIRE_AUTH=1 SCE_STUDIO_AUTH_PASSWORD=top-secret sce studio apply --job <job-id> --auth-password top-secret --json
|
|
568
570
|
```
|
|
569
571
|
|
|
572
|
+
Studio JSON output now includes a stable UI-oriented task stream contract (in addition to existing `job_*` fields):
|
|
573
|
+
- root IDs: `sessionId`, `sceneId`, `specId`, `taskId`, `eventId`
|
|
574
|
+
- `task.goal`, `task.status`, `task.summary` (fixed 3-line summary), `task.handoff`, `task.next_action`
|
|
575
|
+
- `task.file_changes[]`: `path`, `line`, `diffRef`
|
|
576
|
+
- `task.commands[]`: `cmd`, `exit_code`, `stdout`, `stderr`, `log_path`
|
|
577
|
+
- `task.errors[]`: `message`, `error_bundle` (copy-ready diagnostic bundle)
|
|
578
|
+
- `task.evidence[]`: structured evidence references
|
|
579
|
+
- `event[]`: raw audit event stream (`studio events` also keeps legacy `events[]` for compatibility)
|
|
580
|
+
- `studio events --openhands-events <path>` switches `source_stream=openhands` and maps OpenHands raw events to the same task contract fields.
|
|
581
|
+
|
|
570
582
|
Stage guardrails are enforced by default:
|
|
571
583
|
- `plan` requires `--scene`; SCE binds one active primary session per scene
|
|
572
584
|
- `plan` runs auto intake by default (`.sce/config/studio-intake-policy.json`):
|
package/lib/commands/studio.js
CHANGED
|
@@ -643,6 +643,7 @@ async function appendStudioEvent(paths, job, eventType, metadata = {}, fileSyste
|
|
|
643
643
|
const eventLine = `${JSON.stringify(event)}\n`;
|
|
644
644
|
const eventFile = getEventLogFilePath(paths, job.job_id);
|
|
645
645
|
await fileSystem.appendFile(eventFile, eventLine, 'utf8');
|
|
646
|
+
return event;
|
|
646
647
|
}
|
|
647
648
|
|
|
648
649
|
async function readStudioEvents(paths, jobId, options = {}, fileSystem = fs) {
|
|
@@ -730,6 +731,697 @@ function resolveNextAction(job) {
|
|
|
730
731
|
return 'complete';
|
|
731
732
|
}
|
|
732
733
|
|
|
734
|
+
function resolveTaskStage(mode, job, explicitStage = '') {
|
|
735
|
+
const normalizedExplicit = normalizeString(explicitStage);
|
|
736
|
+
if (normalizedExplicit) {
|
|
737
|
+
return normalizedExplicit;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const modeSuffix = normalizeString(mode).replace(/^studio-/, '');
|
|
741
|
+
if (modeSuffix && (STAGE_ORDER.includes(modeSuffix) || modeSuffix === 'rollback')) {
|
|
742
|
+
return modeSuffix;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (normalizeString(job && job.status) === 'rolled_back') {
|
|
746
|
+
return 'rollback';
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
for (let i = STAGE_ORDER.length - 1; i >= 0; i -= 1) {
|
|
750
|
+
const stageName = STAGE_ORDER[i];
|
|
751
|
+
const stage = job && job.stages ? job.stages[stageName] : null;
|
|
752
|
+
const status = normalizeString(stage && stage.status);
|
|
753
|
+
if (status && status !== 'pending') {
|
|
754
|
+
return stageName;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return 'plan';
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function normalizeTaskFileChanges(fileChanges = []) {
|
|
762
|
+
const entries = Array.isArray(fileChanges) ? fileChanges : [];
|
|
763
|
+
const seen = new Set();
|
|
764
|
+
const normalized = [];
|
|
765
|
+
for (const entry of entries) {
|
|
766
|
+
const pathRef = normalizeString(entry && entry.path);
|
|
767
|
+
if (!pathRef) {
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
const line = Number.parseInt(`${entry && entry.line != null ? entry.line : 1}`, 10);
|
|
771
|
+
const normalizedLine = Number.isFinite(line) && line > 0 ? line : 1;
|
|
772
|
+
const diffRef = normalizeString(entry && entry.diffRef) || `${pathRef}:${normalizedLine}`;
|
|
773
|
+
if (seen.has(diffRef)) {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
seen.add(diffRef);
|
|
777
|
+
normalized.push({
|
|
778
|
+
path: pathRef,
|
|
779
|
+
line: normalizedLine,
|
|
780
|
+
diffRef
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
return normalized;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function normalizeTaskCommands(commands = []) {
|
|
787
|
+
const entries = Array.isArray(commands) ? commands : [];
|
|
788
|
+
const normalized = [];
|
|
789
|
+
for (const entry of entries) {
|
|
790
|
+
const cmd = normalizeString(entry && entry.cmd);
|
|
791
|
+
const stdout = typeof (entry && entry.stdout) === 'string' ? entry.stdout : '';
|
|
792
|
+
const stderr = typeof (entry && entry.stderr) === 'string' ? entry.stderr : '';
|
|
793
|
+
const exitCodeRaw = entry && entry.exit_code;
|
|
794
|
+
const exitCode = Number.isFinite(Number(exitCodeRaw))
|
|
795
|
+
? Number(exitCodeRaw)
|
|
796
|
+
: null;
|
|
797
|
+
const logPath = normalizeString(entry && entry.log_path) || null;
|
|
798
|
+
if (!cmd && !stdout && !stderr && exitCode == null && !logPath) {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
normalized.push({
|
|
802
|
+
cmd: cmd || 'n/a',
|
|
803
|
+
exit_code: exitCode,
|
|
804
|
+
stdout,
|
|
805
|
+
stderr,
|
|
806
|
+
log_path: logPath
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
return normalized;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function buildErrorBundle(entry = {}) {
|
|
813
|
+
const lines = [];
|
|
814
|
+
const pushLine = (key, value) => {
|
|
815
|
+
const normalized = typeof value === 'string' ? value.trim() : `${value || ''}`.trim();
|
|
816
|
+
if (!normalized) {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
lines.push(`${key}: ${normalized}`);
|
|
820
|
+
};
|
|
821
|
+
pushLine('message', entry.message);
|
|
822
|
+
pushLine('step', entry.step_id || entry.step);
|
|
823
|
+
pushLine('cmd', entry.cmd);
|
|
824
|
+
if (entry.exit_code != null && `${entry.exit_code}`.trim()) {
|
|
825
|
+
lines.push(`exit_code: ${entry.exit_code}`);
|
|
826
|
+
}
|
|
827
|
+
pushLine('skip_reason', entry.skip_reason);
|
|
828
|
+
pushLine('stderr', entry.stderr);
|
|
829
|
+
pushLine('stdout', entry.stdout);
|
|
830
|
+
return lines.join('\n');
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function normalizeTaskErrors(errors = []) {
|
|
834
|
+
const entries = Array.isArray(errors) ? errors : [];
|
|
835
|
+
const normalized = [];
|
|
836
|
+
for (const entry of entries) {
|
|
837
|
+
const message = normalizeString(entry && entry.message);
|
|
838
|
+
if (!message) {
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
const errorBundle = normalizeString(entry && entry.error_bundle) || buildErrorBundle(entry);
|
|
842
|
+
normalized.push({
|
|
843
|
+
message,
|
|
844
|
+
error_bundle: errorBundle
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
return normalized;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function normalizeTaskEvidence(evidence = []) {
|
|
851
|
+
const entries = Array.isArray(evidence) ? evidence : [];
|
|
852
|
+
const normalized = [];
|
|
853
|
+
const seen = new Set();
|
|
854
|
+
for (const entry of entries) {
|
|
855
|
+
if (typeof entry === 'string') {
|
|
856
|
+
const ref = normalizeString(entry);
|
|
857
|
+
if (!ref || seen.has(ref)) {
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
seen.add(ref);
|
|
861
|
+
normalized.push({
|
|
862
|
+
type: 'reference',
|
|
863
|
+
ref,
|
|
864
|
+
detail: null
|
|
865
|
+
});
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const type = normalizeString(entry && entry.type) || 'reference';
|
|
870
|
+
const ref = normalizeString(entry && entry.ref);
|
|
871
|
+
const detail = normalizeString(entry && entry.detail) || null;
|
|
872
|
+
const key = `${type}:${ref}:${detail || ''}`;
|
|
873
|
+
if ((!ref && !detail) || seen.has(key)) {
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
seen.add(key);
|
|
877
|
+
normalized.push({
|
|
878
|
+
type,
|
|
879
|
+
ref: ref || null,
|
|
880
|
+
detail
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
return normalized;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function pickFirstString(values = []) {
|
|
887
|
+
for (const value of values) {
|
|
888
|
+
if (typeof value === 'string' && value.trim()) {
|
|
889
|
+
return value.trim();
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return '';
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function pickFirstNumber(values = []) {
|
|
896
|
+
for (const value of values) {
|
|
897
|
+
if (value == null || value === '') {
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
const numberValue = Number(value);
|
|
901
|
+
if (Number.isFinite(numberValue)) {
|
|
902
|
+
return numberValue;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function extractEventArrayFromPayload(payload) {
|
|
909
|
+
if (Array.isArray(payload)) {
|
|
910
|
+
return payload;
|
|
911
|
+
}
|
|
912
|
+
if (!payload || typeof payload !== 'object') {
|
|
913
|
+
return [];
|
|
914
|
+
}
|
|
915
|
+
const candidateKeys = ['events', 'items', 'data', 'records', 'entries'];
|
|
916
|
+
for (const key of candidateKeys) {
|
|
917
|
+
if (Array.isArray(payload[key])) {
|
|
918
|
+
return payload[key];
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return [payload];
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
async function readOpenHandsEventsFile(openhandsEventsPath, fileSystem = fs) {
|
|
925
|
+
const normalizedPath = normalizeString(openhandsEventsPath);
|
|
926
|
+
if (!normalizedPath) {
|
|
927
|
+
return [];
|
|
928
|
+
}
|
|
929
|
+
const exists = await fileSystem.pathExists(normalizedPath);
|
|
930
|
+
if (!exists) {
|
|
931
|
+
throw new Error(`OpenHands events file not found: ${normalizedPath}`);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const content = await fileSystem.readFile(normalizedPath, 'utf8');
|
|
935
|
+
const trimmed = `${content || ''}`.trim();
|
|
936
|
+
if (!trimmed) {
|
|
937
|
+
return [];
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
try {
|
|
941
|
+
const parsed = JSON.parse(trimmed);
|
|
942
|
+
return extractEventArrayFromPayload(parsed);
|
|
943
|
+
} catch (_error) {
|
|
944
|
+
const lines = trimmed
|
|
945
|
+
.split(/\r?\n/)
|
|
946
|
+
.map((line) => line.trim())
|
|
947
|
+
.filter(Boolean);
|
|
948
|
+
const parsedLines = [];
|
|
949
|
+
for (const line of lines) {
|
|
950
|
+
try {
|
|
951
|
+
parsedLines.push(JSON.parse(line));
|
|
952
|
+
} catch (_innerError) {
|
|
953
|
+
// Skip malformed JSONL lines to keep ingestion robust.
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return parsedLines;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function normalizeOpenHandsEventRecord(rawEvent, index, jobId) {
|
|
961
|
+
const raw = rawEvent && typeof rawEvent === 'object'
|
|
962
|
+
? rawEvent
|
|
963
|
+
: { value: rawEvent };
|
|
964
|
+
const eventId = pickFirstString([
|
|
965
|
+
raw.event_id,
|
|
966
|
+
raw.eventId,
|
|
967
|
+
raw.id,
|
|
968
|
+
raw.uuid
|
|
969
|
+
]) || `oh-evt-${index + 1}`;
|
|
970
|
+
const eventType = pickFirstString([
|
|
971
|
+
raw.event_type,
|
|
972
|
+
raw.eventType,
|
|
973
|
+
raw.type,
|
|
974
|
+
raw.kind,
|
|
975
|
+
raw.action
|
|
976
|
+
]) || 'unknown';
|
|
977
|
+
const timestamp = pickFirstString([
|
|
978
|
+
raw.timestamp,
|
|
979
|
+
raw.time,
|
|
980
|
+
raw.created_at,
|
|
981
|
+
raw.createdAt,
|
|
982
|
+
raw.ts
|
|
983
|
+
]) || nowIso();
|
|
984
|
+
return {
|
|
985
|
+
api_version: STUDIO_EVENT_API_VERSION,
|
|
986
|
+
event_id: eventId,
|
|
987
|
+
job_id: jobId,
|
|
988
|
+
event_type: `openhands.${eventType}`,
|
|
989
|
+
timestamp,
|
|
990
|
+
metadata: {
|
|
991
|
+
source: 'openhands',
|
|
992
|
+
raw
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function tryParseJsonText(value) {
|
|
998
|
+
if (typeof value !== 'string') {
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
const trimmed = value.trim();
|
|
1002
|
+
if (!trimmed) {
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
if (!(trimmed.startsWith('{') || trimmed.startsWith('['))) {
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
try {
|
|
1009
|
+
return JSON.parse(trimmed);
|
|
1010
|
+
} catch (_error) {
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function stringifyCompact(value) {
|
|
1016
|
+
if (value == null) {
|
|
1017
|
+
return '';
|
|
1018
|
+
}
|
|
1019
|
+
if (typeof value === 'string') {
|
|
1020
|
+
return value;
|
|
1021
|
+
}
|
|
1022
|
+
try {
|
|
1023
|
+
return JSON.stringify(value);
|
|
1024
|
+
} catch (_error) {
|
|
1025
|
+
return `${value}`;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function extractOpenHandsCommandCandidates(raw = {}) {
|
|
1030
|
+
const command = pickFirstString([
|
|
1031
|
+
raw.command,
|
|
1032
|
+
raw.cmd,
|
|
1033
|
+
raw.input && raw.input.command,
|
|
1034
|
+
raw.action && raw.action.command,
|
|
1035
|
+
raw.observation && raw.observation.command,
|
|
1036
|
+
raw.tool_input && raw.tool_input.command
|
|
1037
|
+
]);
|
|
1038
|
+
|
|
1039
|
+
const toolName = pickFirstString([
|
|
1040
|
+
raw.tool_name,
|
|
1041
|
+
raw.toolName,
|
|
1042
|
+
raw.tool && raw.tool.name,
|
|
1043
|
+
raw.name
|
|
1044
|
+
]);
|
|
1045
|
+
const toolArgs = raw.arguments || raw.tool_input || raw.input || null;
|
|
1046
|
+
|
|
1047
|
+
const cmd = command || (toolName
|
|
1048
|
+
? `tool:${toolName}${toolArgs ? ` ${stringifyCompact(toolArgs)}` : ''}`
|
|
1049
|
+
: '');
|
|
1050
|
+
|
|
1051
|
+
return {
|
|
1052
|
+
cmd,
|
|
1053
|
+
exit_code: pickFirstNumber([
|
|
1054
|
+
raw.exit_code,
|
|
1055
|
+
raw.exitCode,
|
|
1056
|
+
raw.result && (raw.result.exit_code ?? raw.result.exitCode)
|
|
1057
|
+
]),
|
|
1058
|
+
stdout: pickFirstString([
|
|
1059
|
+
raw.stdout,
|
|
1060
|
+
raw.output,
|
|
1061
|
+
raw.result && raw.result.stdout,
|
|
1062
|
+
raw.observation && raw.observation.stdout
|
|
1063
|
+
]),
|
|
1064
|
+
stderr: pickFirstString([
|
|
1065
|
+
raw.stderr,
|
|
1066
|
+
raw.result && raw.result.stderr,
|
|
1067
|
+
raw.error && raw.error.message,
|
|
1068
|
+
typeof raw.error === 'string' ? raw.error : ''
|
|
1069
|
+
]),
|
|
1070
|
+
log_path: pickFirstString([
|
|
1071
|
+
raw.log_path,
|
|
1072
|
+
raw.logPath,
|
|
1073
|
+
raw.log && raw.log.path
|
|
1074
|
+
])
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function collectOpenHandsFileChangesFromRaw(raw = {}) {
|
|
1079
|
+
const changes = [];
|
|
1080
|
+
const addPath = (pathValue, lineValue, diffRefValue = '') => {
|
|
1081
|
+
const pathRef = normalizeString(pathValue);
|
|
1082
|
+
if (!pathRef) {
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
const line = Number.parseInt(`${lineValue != null ? lineValue : 1}`, 10);
|
|
1086
|
+
const normalizedLine = Number.isFinite(line) && line > 0 ? line : 1;
|
|
1087
|
+
const diffRef = normalizeString(diffRefValue) || `${pathRef}:${normalizedLine}`;
|
|
1088
|
+
changes.push({
|
|
1089
|
+
path: pathRef,
|
|
1090
|
+
line: normalizedLine,
|
|
1091
|
+
diffRef
|
|
1092
|
+
});
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
if (typeof raw.path === 'string') {
|
|
1096
|
+
addPath(raw.path, raw.line || raw.line_number, raw.diff_ref);
|
|
1097
|
+
}
|
|
1098
|
+
if (raw.file && typeof raw.file === 'object') {
|
|
1099
|
+
addPath(raw.file.path || raw.file.name, raw.file.line || raw.file.line_number, raw.file.diffRef || raw.file.diff_ref);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
const arrayKeys = ['files', 'changed_files', 'modified_files', 'created_files', 'deleted_files'];
|
|
1103
|
+
for (const key of arrayKeys) {
|
|
1104
|
+
const values = Array.isArray(raw[key]) ? raw[key] : [];
|
|
1105
|
+
for (const value of values) {
|
|
1106
|
+
if (typeof value === 'string') {
|
|
1107
|
+
addPath(value, 1);
|
|
1108
|
+
} else if (value && typeof value === 'object') {
|
|
1109
|
+
addPath(value.path || value.file || value.name, value.line || value.line_number, value.diffRef || value.diff_ref);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const patchText = pickFirstString([raw.patch, raw.diff]);
|
|
1115
|
+
if (patchText) {
|
|
1116
|
+
const parsedJson = tryParseJsonText(patchText);
|
|
1117
|
+
if (parsedJson && typeof parsedJson === 'object') {
|
|
1118
|
+
const nested = collectOpenHandsFileChangesFromRaw(parsedJson);
|
|
1119
|
+
changes.push(...nested);
|
|
1120
|
+
} else {
|
|
1121
|
+
const lines = patchText.split(/\r?\n/);
|
|
1122
|
+
for (const line of lines) {
|
|
1123
|
+
const match = line.match(/^\+\+\+\s+b\/(.+)$/);
|
|
1124
|
+
if (match && match[1]) {
|
|
1125
|
+
addPath(match[1], 1);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
return changes;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function mapOpenHandsEventsToTaskSignals(openhandsEvents = [], context = {}) {
|
|
1135
|
+
const normalizedEvents = openhandsEvents.map((item, index) =>
|
|
1136
|
+
normalizeOpenHandsEventRecord(item, index, context.jobId)
|
|
1137
|
+
);
|
|
1138
|
+
|
|
1139
|
+
const commands = [];
|
|
1140
|
+
const fileChanges = [];
|
|
1141
|
+
const errors = [];
|
|
1142
|
+
const evidence = [];
|
|
1143
|
+
|
|
1144
|
+
for (const event of normalizedEvents) {
|
|
1145
|
+
const raw = event && event.metadata ? event.metadata.raw || {} : {};
|
|
1146
|
+
const commandCandidate = extractOpenHandsCommandCandidates(raw);
|
|
1147
|
+
if (commandCandidate.cmd || commandCandidate.stdout || commandCandidate.stderr || commandCandidate.exit_code != null) {
|
|
1148
|
+
commands.push(commandCandidate);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
fileChanges.push(...collectOpenHandsFileChangesFromRaw(raw));
|
|
1152
|
+
|
|
1153
|
+
const eventType = normalizeString(event.event_type).toLowerCase();
|
|
1154
|
+
const failedByType = eventType.includes('error') || eventType.includes('fail');
|
|
1155
|
+
const failedByExit = commandCandidate.exit_code != null && Number(commandCandidate.exit_code) !== 0;
|
|
1156
|
+
const failedByStderr = commandCandidate.stderr.length > 0 && !commandCandidate.stdout;
|
|
1157
|
+
if (failedByType || failedByExit || failedByStderr) {
|
|
1158
|
+
const message = pickFirstString([
|
|
1159
|
+
raw.message,
|
|
1160
|
+
raw.error && raw.error.message,
|
|
1161
|
+
typeof raw.error === 'string' ? raw.error : '',
|
|
1162
|
+
failedByExit ? `OpenHands command failed (exit ${commandCandidate.exit_code})` : '',
|
|
1163
|
+
'OpenHands event indicates failure'
|
|
1164
|
+
]);
|
|
1165
|
+
errors.push({
|
|
1166
|
+
message,
|
|
1167
|
+
step_id: event.event_type,
|
|
1168
|
+
cmd: commandCandidate.cmd,
|
|
1169
|
+
exit_code: commandCandidate.exit_code,
|
|
1170
|
+
stderr: commandCandidate.stderr,
|
|
1171
|
+
stdout: commandCandidate.stdout
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
evidence.push({
|
|
1176
|
+
type: 'openhands-event',
|
|
1177
|
+
ref: event.event_id,
|
|
1178
|
+
detail: event.event_type
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
return {
|
|
1183
|
+
event: normalizedEvents,
|
|
1184
|
+
commands: normalizeTaskCommands(commands),
|
|
1185
|
+
file_changes: normalizeTaskFileChanges(fileChanges),
|
|
1186
|
+
errors: normalizeTaskErrors(errors),
|
|
1187
|
+
evidence: normalizeTaskEvidence(evidence)
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function extractCommandsFromStageMetadata(stageMetadata = {}) {
|
|
1192
|
+
const gateSteps = Array.isArray(stageMetadata && stageMetadata.gate_steps)
|
|
1193
|
+
? stageMetadata.gate_steps
|
|
1194
|
+
: [];
|
|
1195
|
+
return gateSteps.map((step) => ({
|
|
1196
|
+
cmd: normalizeString(step && step.command) || normalizeString(step && step.id) || 'n/a',
|
|
1197
|
+
exit_code: Number.isFinite(Number(step && step.exit_code))
|
|
1198
|
+
? Number(step.exit_code)
|
|
1199
|
+
: null,
|
|
1200
|
+
stdout: normalizeString(step?.output?.stdout),
|
|
1201
|
+
stderr: normalizeString(step?.output?.stderr),
|
|
1202
|
+
log_path: normalizeString(stageMetadata.report) || null
|
|
1203
|
+
}));
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function extractErrorsFromStageMetadata(stageState = {}, stageMetadata = {}) {
|
|
1207
|
+
const directErrors = Array.isArray(stageMetadata && stageMetadata.errors)
|
|
1208
|
+
? stageMetadata.errors
|
|
1209
|
+
: [];
|
|
1210
|
+
const gateStepErrors = [];
|
|
1211
|
+
const gateSteps = Array.isArray(stageMetadata && stageMetadata.gate_steps)
|
|
1212
|
+
? stageMetadata.gate_steps
|
|
1213
|
+
: [];
|
|
1214
|
+
|
|
1215
|
+
for (const step of gateSteps) {
|
|
1216
|
+
const status = normalizeString(step && step.status);
|
|
1217
|
+
if (status !== 'failed') {
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
const stepId = normalizeString(step && step.id) || normalizeString(step && step.name) || 'unknown-step';
|
|
1221
|
+
const stepMessage = `Gate step failed: ${stepId}${step && step.exit_code != null ? ` (exit ${step.exit_code})` : ''}`;
|
|
1222
|
+
gateStepErrors.push({
|
|
1223
|
+
message: stepMessage,
|
|
1224
|
+
step_id: stepId,
|
|
1225
|
+
cmd: normalizeString(step && step.command),
|
|
1226
|
+
exit_code: step && step.exit_code != null ? step.exit_code : null,
|
|
1227
|
+
skip_reason: normalizeString(step && step.skip_reason),
|
|
1228
|
+
stdout: normalizeString(step?.output?.stdout),
|
|
1229
|
+
stderr: normalizeString(step?.output?.stderr)
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (normalizeString(stageState && stageState.status) === 'blocked') {
|
|
1234
|
+
const blockers = Array.isArray(stageMetadata?.problem_evaluation?.blockers)
|
|
1235
|
+
? stageMetadata.problem_evaluation.blockers
|
|
1236
|
+
: [];
|
|
1237
|
+
if (blockers.length > 0) {
|
|
1238
|
+
gateStepErrors.push({
|
|
1239
|
+
message: `Problem evaluation blocked stage: ${blockers.join(', ')}`,
|
|
1240
|
+
cmd: 'problem-evaluation-policy'
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
return [...directErrors, ...gateStepErrors];
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function collectTaskFileChanges(job = {}, stageName = '', stageMetadata = {}) {
|
|
1249
|
+
const fileChanges = [];
|
|
1250
|
+
if (Array.isArray(stageMetadata && stageMetadata.file_changes)) {
|
|
1251
|
+
fileChanges.push(...stageMetadata.file_changes);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
const createdSpec = job?.source?.intake?.created_spec;
|
|
1255
|
+
const createdSpecId = createdSpec && createdSpec.created
|
|
1256
|
+
? normalizeString(createdSpec.spec_id)
|
|
1257
|
+
: '';
|
|
1258
|
+
if (stageName === 'plan' && createdSpecId) {
|
|
1259
|
+
fileChanges.push(
|
|
1260
|
+
{ path: `.sce/specs/${createdSpecId}/requirements.md`, line: 1 },
|
|
1261
|
+
{ path: `.sce/specs/${createdSpecId}/design.md`, line: 1 },
|
|
1262
|
+
{ path: `.sce/specs/${createdSpecId}/tasks.md`, line: 1 },
|
|
1263
|
+
{ path: `.sce/specs/${createdSpecId}/custom/problem-domain-chain.json`, line: 1 },
|
|
1264
|
+
{ path: `.sce/specs/${createdSpecId}/custom/problem-contract.json`, line: 1 }
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
const artifacts = job && job.artifacts ? job.artifacts : {};
|
|
1269
|
+
if (stageName === 'plan') {
|
|
1270
|
+
if (normalizeString(artifacts.spec_portfolio_report)) {
|
|
1271
|
+
fileChanges.push({ path: artifacts.spec_portfolio_report, line: 1 });
|
|
1272
|
+
}
|
|
1273
|
+
if (normalizeString(artifacts.spec_scene_index)) {
|
|
1274
|
+
fileChanges.push({ path: artifacts.spec_scene_index, line: 1 });
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
if (stageName === 'generate' && normalizeString(artifacts.generate_report)) {
|
|
1278
|
+
fileChanges.push({ path: artifacts.generate_report, line: 1 });
|
|
1279
|
+
}
|
|
1280
|
+
if (stageName === 'verify' && normalizeString(artifacts.verify_report)) {
|
|
1281
|
+
fileChanges.push({ path: artifacts.verify_report, line: 1 });
|
|
1282
|
+
}
|
|
1283
|
+
if (stageName === 'release' && normalizeString(artifacts.release_report)) {
|
|
1284
|
+
fileChanges.push({ path: artifacts.release_report, line: 1 });
|
|
1285
|
+
}
|
|
1286
|
+
return normalizeTaskFileChanges(fileChanges);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function collectTaskEvidence(job = {}, stageName = '', stageMetadata = {}) {
|
|
1290
|
+
const evidence = [];
|
|
1291
|
+
const stageReport = normalizeString(stageMetadata && stageMetadata.report);
|
|
1292
|
+
if (stageReport) {
|
|
1293
|
+
evidence.push({ type: 'stage-report', ref: stageReport, detail: stageName });
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
const artifacts = job && job.artifacts ? job.artifacts : {};
|
|
1297
|
+
if (stageName && artifacts.problem_eval_reports && artifacts.problem_eval_reports[stageName]) {
|
|
1298
|
+
evidence.push({
|
|
1299
|
+
type: 'problem-evaluation-report',
|
|
1300
|
+
ref: artifacts.problem_eval_reports[stageName],
|
|
1301
|
+
detail: stageName
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
if (normalizeString(job?.source?.domain_chain?.chain_path)) {
|
|
1306
|
+
evidence.push({
|
|
1307
|
+
type: 'domain-chain',
|
|
1308
|
+
ref: job.source.domain_chain.chain_path,
|
|
1309
|
+
detail: stageName
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
if (normalizeString(job?.source?.problem_contract_path)) {
|
|
1313
|
+
evidence.push({
|
|
1314
|
+
type: 'problem-contract',
|
|
1315
|
+
ref: job.source.problem_contract_path,
|
|
1316
|
+
detail: stageName
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
if (Array.isArray(stageMetadata && stageMetadata.auto_errorbook_records)) {
|
|
1320
|
+
for (const item of stageMetadata.auto_errorbook_records) {
|
|
1321
|
+
const entryId = normalizeString(item && item.entry_id);
|
|
1322
|
+
if (!entryId) {
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
evidence.push({
|
|
1326
|
+
type: 'errorbook-entry',
|
|
1327
|
+
ref: entryId,
|
|
1328
|
+
detail: normalizeString(item && item.step_id) || null
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
if (normalizeString(job && job.job_id)) {
|
|
1334
|
+
evidence.push({
|
|
1335
|
+
type: 'event-log',
|
|
1336
|
+
ref: `.sce/studio/events/${job.job_id}.jsonl`,
|
|
1337
|
+
detail: 'raw-audit-stream'
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
return normalizeTaskEvidence(evidence);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function buildTaskSummaryLines(job = {}, stageName = '', taskStatus = '', nextAction = '') {
|
|
1345
|
+
const sceneId = normalizeString(job?.scene?.id) || 'scene.n/a';
|
|
1346
|
+
const specId = normalizeString(job?.scene?.spec_id) || normalizeString(job?.source?.spec_id) || 'spec.n/a';
|
|
1347
|
+
const progress = buildProgress(job);
|
|
1348
|
+
return [
|
|
1349
|
+
`Stage: ${stageName || 'plan'} | Status: ${taskStatus || 'unknown'}`,
|
|
1350
|
+
`Scene: ${sceneId} | Spec: ${specId} | Progress: ${progress.completed}/${progress.total}`,
|
|
1351
|
+
`Next: ${nextAction || 'n/a'}`
|
|
1352
|
+
];
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function buildTaskEnvelope(mode, job, options = {}) {
|
|
1356
|
+
const stageName = resolveTaskStage(mode, job, options.stageName);
|
|
1357
|
+
const stageState = stageName && job && job.stages && job.stages[stageName]
|
|
1358
|
+
? job.stages[stageName]
|
|
1359
|
+
: {};
|
|
1360
|
+
const stageMetadata = stageState && typeof stageState.metadata === 'object' && stageState.metadata
|
|
1361
|
+
? stageState.metadata
|
|
1362
|
+
: {};
|
|
1363
|
+
const nextAction = resolveNextAction(job);
|
|
1364
|
+
|
|
1365
|
+
const events = Array.isArray(options.events)
|
|
1366
|
+
? options.events
|
|
1367
|
+
: (options.event ? [options.event] : []);
|
|
1368
|
+
const latestEvent = events.length > 0 ? events[events.length - 1] : null;
|
|
1369
|
+
|
|
1370
|
+
const taskStatus = normalizeString(stageState && stageState.status)
|
|
1371
|
+
|| (stageName === 'rollback' && normalizeString(job && job.status) === 'rolled_back'
|
|
1372
|
+
? 'completed'
|
|
1373
|
+
: normalizeString(job && job.status) || 'unknown');
|
|
1374
|
+
const taskId = normalizeString(options.taskId)
|
|
1375
|
+
|| (normalizeString(job && job.job_id)
|
|
1376
|
+
? `${job.job_id}:${stageName || 'task'}`
|
|
1377
|
+
: null);
|
|
1378
|
+
const goal = normalizeString(job?.source?.goal)
|
|
1379
|
+
|| `Studio ${stageName || 'task'} execution`;
|
|
1380
|
+
const sessionId = normalizeString(job?.session?.scene_session_id) || null;
|
|
1381
|
+
const sceneId = normalizeString(job?.scene?.id) || null;
|
|
1382
|
+
const specId = normalizeString(job?.scene?.spec_id) || normalizeString(job?.source?.spec_id) || null;
|
|
1383
|
+
|
|
1384
|
+
const commands = normalizeTaskCommands([
|
|
1385
|
+
...(Array.isArray(stageMetadata.commands) ? stageMetadata.commands : []),
|
|
1386
|
+
...extractCommandsFromStageMetadata(stageMetadata)
|
|
1387
|
+
]);
|
|
1388
|
+
const errors = normalizeTaskErrors(
|
|
1389
|
+
extractErrorsFromStageMetadata(stageState, stageMetadata)
|
|
1390
|
+
);
|
|
1391
|
+
const fileChanges = collectTaskFileChanges(job, stageName, stageMetadata);
|
|
1392
|
+
const evidence = collectTaskEvidence(job, stageName, stageMetadata);
|
|
1393
|
+
|
|
1394
|
+
const handoff = stageMetadata.handoff && typeof stageMetadata.handoff === 'object'
|
|
1395
|
+
? stageMetadata.handoff
|
|
1396
|
+
: {
|
|
1397
|
+
stage: stageName,
|
|
1398
|
+
status: taskStatus,
|
|
1399
|
+
completed_at: normalizeString(stageState && stageState.completed_at) || null,
|
|
1400
|
+
report: normalizeString(stageMetadata.report) || null,
|
|
1401
|
+
release_ref: normalizeString(stageMetadata.release_ref) || normalizeString(job?.artifacts?.release_ref) || null
|
|
1402
|
+
};
|
|
1403
|
+
|
|
1404
|
+
return {
|
|
1405
|
+
sessionId,
|
|
1406
|
+
sceneId,
|
|
1407
|
+
specId,
|
|
1408
|
+
taskId,
|
|
1409
|
+
eventId: normalizeString(latestEvent && latestEvent.event_id) || null,
|
|
1410
|
+
task: {
|
|
1411
|
+
goal,
|
|
1412
|
+
status: taskStatus,
|
|
1413
|
+
summary: buildTaskSummaryLines(job, stageName, taskStatus, nextAction),
|
|
1414
|
+
handoff,
|
|
1415
|
+
next_action: nextAction,
|
|
1416
|
+
file_changes: fileChanges,
|
|
1417
|
+
commands,
|
|
1418
|
+
errors,
|
|
1419
|
+
evidence
|
|
1420
|
+
},
|
|
1421
|
+
event: events
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
|
|
733
1425
|
function toRelativePosix(projectPath, absolutePath) {
|
|
734
1426
|
return path.relative(projectPath, absolutePath).replace(/\\/g, '/');
|
|
735
1427
|
}
|
|
@@ -1170,8 +1862,8 @@ function ensureNotRolledBack(job, stageName) {
|
|
|
1170
1862
|
}
|
|
1171
1863
|
}
|
|
1172
1864
|
|
|
1173
|
-
function buildCommandPayload(mode, job) {
|
|
1174
|
-
|
|
1865
|
+
function buildCommandPayload(mode, job, options = {}) {
|
|
1866
|
+
const base = {
|
|
1175
1867
|
mode,
|
|
1176
1868
|
success: true,
|
|
1177
1869
|
job_id: job.job_id,
|
|
@@ -1180,6 +1872,10 @@ function buildCommandPayload(mode, job) {
|
|
|
1180
1872
|
next_action: resolveNextAction(job),
|
|
1181
1873
|
artifacts: { ...job.artifacts }
|
|
1182
1874
|
};
|
|
1875
|
+
return {
|
|
1876
|
+
...base,
|
|
1877
|
+
...buildTaskEnvelope(mode, job, options)
|
|
1878
|
+
};
|
|
1183
1879
|
}
|
|
1184
1880
|
|
|
1185
1881
|
function buildJobDomainChainMetadata(job = {}) {
|
|
@@ -1645,7 +2341,7 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
|
|
|
1645
2341
|
};
|
|
1646
2342
|
|
|
1647
2343
|
await saveJob(paths, job, fileSystem);
|
|
1648
|
-
await appendStudioEvent(paths, job, 'stage.plan.completed', {
|
|
2344
|
+
const planEvent = await appendStudioEvent(paths, job, 'stage.plan.completed', {
|
|
1649
2345
|
from_chat: fromChat,
|
|
1650
2346
|
scene_id: sceneId,
|
|
1651
2347
|
spec_id: effectiveSpecId,
|
|
@@ -1671,7 +2367,10 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
|
|
|
1671
2367
|
}, fileSystem);
|
|
1672
2368
|
await writeLatestJob(paths, jobId, fileSystem);
|
|
1673
2369
|
|
|
1674
|
-
const payload = buildCommandPayload('studio-plan', job
|
|
2370
|
+
const payload = buildCommandPayload('studio-plan', job, {
|
|
2371
|
+
stageName: 'plan',
|
|
2372
|
+
event: planEvent
|
|
2373
|
+
});
|
|
1675
2374
|
payload.scene = {
|
|
1676
2375
|
id: sceneId,
|
|
1677
2376
|
spec_id: effectiveSpecId
|
|
@@ -1756,7 +2455,7 @@ async function runStudioGenerateCommand(options = {}, dependencies = {}) {
|
|
|
1756
2455
|
});
|
|
1757
2456
|
|
|
1758
2457
|
await saveJob(paths, job, fileSystem);
|
|
1759
|
-
await appendStudioEvent(paths, job, 'stage.generate.completed', {
|
|
2458
|
+
const generateEvent = await appendStudioEvent(paths, job, 'stage.generate.completed', {
|
|
1760
2459
|
scene_id: sceneId,
|
|
1761
2460
|
target: job.target,
|
|
1762
2461
|
patch_bundle_id: patchBundleId,
|
|
@@ -1766,7 +2465,10 @@ async function runStudioGenerateCommand(options = {}, dependencies = {}) {
|
|
|
1766
2465
|
}, fileSystem);
|
|
1767
2466
|
await writeLatestJob(paths, jobId, fileSystem);
|
|
1768
2467
|
|
|
1769
|
-
const payload = buildCommandPayload('studio-generate', job
|
|
2468
|
+
const payload = buildCommandPayload('studio-generate', job, {
|
|
2469
|
+
stageName: 'generate',
|
|
2470
|
+
event: generateEvent
|
|
2471
|
+
});
|
|
1770
2472
|
printStudioPayload(payload, options);
|
|
1771
2473
|
return payload;
|
|
1772
2474
|
}
|
|
@@ -1826,14 +2528,17 @@ async function runStudioApplyCommand(options = {}, dependencies = {}) {
|
|
|
1826
2528
|
});
|
|
1827
2529
|
|
|
1828
2530
|
await saveJob(paths, job, fileSystem);
|
|
1829
|
-
await appendStudioEvent(paths, job, 'stage.apply.completed', {
|
|
2531
|
+
const applyEvent = await appendStudioEvent(paths, job, 'stage.apply.completed', {
|
|
1830
2532
|
patch_bundle_id: patchBundleId,
|
|
1831
2533
|
auth_required: authResult.required,
|
|
1832
2534
|
problem_evaluation: summarizeProblemEvaluation(applyProblemEvaluation)
|
|
1833
2535
|
}, fileSystem);
|
|
1834
2536
|
await writeLatestJob(paths, jobId, fileSystem);
|
|
1835
2537
|
|
|
1836
|
-
const payload = buildCommandPayload('studio-apply', job
|
|
2538
|
+
const payload = buildCommandPayload('studio-apply', job, {
|
|
2539
|
+
stageName: 'apply',
|
|
2540
|
+
event: applyEvent
|
|
2541
|
+
});
|
|
1837
2542
|
printStudioPayload(payload, options);
|
|
1838
2543
|
return payload;
|
|
1839
2544
|
}
|
|
@@ -1940,6 +2645,7 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
|
|
|
1940
2645
|
profile,
|
|
1941
2646
|
passed: false,
|
|
1942
2647
|
report: verifyReportPath,
|
|
2648
|
+
gate_steps: gateResult.steps,
|
|
1943
2649
|
problem_evaluation: summarizeProblemEvaluation(verifyProblemEvaluation),
|
|
1944
2650
|
domain_chain: domainChainMetadata,
|
|
1945
2651
|
auto_errorbook_records: autoErrorbookRecords
|
|
@@ -1962,13 +2668,14 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
|
|
|
1962
2668
|
profile,
|
|
1963
2669
|
passed: true,
|
|
1964
2670
|
report: verifyReportPath,
|
|
2671
|
+
gate_steps: gateResult.steps,
|
|
1965
2672
|
problem_evaluation: summarizeProblemEvaluation(verifyProblemEvaluation),
|
|
1966
2673
|
domain_chain: domainChainMetadata,
|
|
1967
2674
|
auto_errorbook_records: autoErrorbookRecords
|
|
1968
2675
|
});
|
|
1969
2676
|
|
|
1970
2677
|
await saveJob(paths, job, fileSystem);
|
|
1971
|
-
await appendStudioEvent(paths, job, 'stage.verify.completed', {
|
|
2678
|
+
const verifyEvent = await appendStudioEvent(paths, job, 'stage.verify.completed', {
|
|
1972
2679
|
profile,
|
|
1973
2680
|
passed: true,
|
|
1974
2681
|
report: verifyReportPath,
|
|
@@ -1978,7 +2685,10 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
|
|
|
1978
2685
|
}, fileSystem);
|
|
1979
2686
|
await writeLatestJob(paths, jobId, fileSystem);
|
|
1980
2687
|
|
|
1981
|
-
const payload = buildCommandPayload('studio-verify', job
|
|
2688
|
+
const payload = buildCommandPayload('studio-verify', job, {
|
|
2689
|
+
stageName: 'verify',
|
|
2690
|
+
event: verifyEvent
|
|
2691
|
+
});
|
|
1982
2692
|
printStudioPayload(payload, options);
|
|
1983
2693
|
return payload;
|
|
1984
2694
|
}
|
|
@@ -2118,6 +2828,7 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
|
|
|
2118
2828
|
release_ref: releaseRef,
|
|
2119
2829
|
passed: false,
|
|
2120
2830
|
report: releaseReportPath,
|
|
2831
|
+
gate_steps: gateResult.steps,
|
|
2121
2832
|
auth_required: authResult.required,
|
|
2122
2833
|
problem_evaluation: summarizeProblemEvaluation(releaseProblemEvaluation),
|
|
2123
2834
|
domain_chain: domainChainMetadata,
|
|
@@ -2143,6 +2854,7 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
|
|
|
2143
2854
|
channel,
|
|
2144
2855
|
release_ref: releaseRef,
|
|
2145
2856
|
report: releaseReportPath,
|
|
2857
|
+
gate_steps: gateResult.steps,
|
|
2146
2858
|
auth_required: authResult.required,
|
|
2147
2859
|
problem_evaluation: summarizeProblemEvaluation(releaseProblemEvaluation),
|
|
2148
2860
|
domain_chain: domainChainMetadata,
|
|
@@ -2171,7 +2883,7 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
|
|
|
2171
2883
|
}
|
|
2172
2884
|
|
|
2173
2885
|
await saveJob(paths, job, fileSystem);
|
|
2174
|
-
await appendStudioEvent(paths, job, 'stage.release.completed', {
|
|
2886
|
+
const releaseEvent = await appendStudioEvent(paths, job, 'stage.release.completed', {
|
|
2175
2887
|
channel,
|
|
2176
2888
|
release_ref: releaseRef,
|
|
2177
2889
|
report: releaseReportPath,
|
|
@@ -2182,7 +2894,10 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
|
|
|
2182
2894
|
}, fileSystem);
|
|
2183
2895
|
await writeLatestJob(paths, jobId, fileSystem);
|
|
2184
2896
|
|
|
2185
|
-
const payload = buildCommandPayload('studio-release', job
|
|
2897
|
+
const payload = buildCommandPayload('studio-release', job, {
|
|
2898
|
+
stageName: 'release',
|
|
2899
|
+
event: releaseEvent
|
|
2900
|
+
});
|
|
2186
2901
|
printStudioPayload(payload, options);
|
|
2187
2902
|
return payload;
|
|
2188
2903
|
}
|
|
@@ -2200,7 +2915,10 @@ async function runStudioResumeCommand(options = {}, dependencies = {}) {
|
|
|
2200
2915
|
}
|
|
2201
2916
|
|
|
2202
2917
|
const job = await loadJob(paths, jobId, fileSystem);
|
|
2203
|
-
const
|
|
2918
|
+
const events = await readStudioEvents(paths, jobId, { limit: 20 }, fileSystem);
|
|
2919
|
+
const payload = buildCommandPayload('studio-resume', job, {
|
|
2920
|
+
events
|
|
2921
|
+
});
|
|
2204
2922
|
payload.success = true;
|
|
2205
2923
|
printStudioPayload(payload, options);
|
|
2206
2924
|
return payload;
|
|
@@ -2252,12 +2970,15 @@ async function runStudioRollbackCommand(options = {}, dependencies = {}) {
|
|
|
2252
2970
|
}
|
|
2253
2971
|
|
|
2254
2972
|
await saveJob(paths, job, fileSystem);
|
|
2255
|
-
await appendStudioEvent(paths, job, 'job.rolled_back', {
|
|
2973
|
+
const rollbackEvent = await appendStudioEvent(paths, job, 'job.rolled_back', {
|
|
2256
2974
|
reason
|
|
2257
2975
|
}, fileSystem);
|
|
2258
2976
|
await writeLatestJob(paths, jobId, fileSystem);
|
|
2259
2977
|
|
|
2260
|
-
const payload = buildCommandPayload('studio-rollback', job
|
|
2978
|
+
const payload = buildCommandPayload('studio-rollback', job, {
|
|
2979
|
+
stageName: 'rollback',
|
|
2980
|
+
event: rollbackEvent
|
|
2981
|
+
});
|
|
2261
2982
|
payload.rollback = { ...job.rollback };
|
|
2262
2983
|
printStudioPayload(payload, options);
|
|
2263
2984
|
return payload;
|
|
@@ -2297,15 +3018,62 @@ async function runStudioEventsCommand(options = {}, dependencies = {}) {
|
|
|
2297
3018
|
}
|
|
2298
3019
|
|
|
2299
3020
|
const limit = normalizePositiveInteger(options.limit, 50);
|
|
2300
|
-
const
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
3021
|
+
const job = await loadJob(paths, jobId, fileSystem);
|
|
3022
|
+
const openhandsEventsPath = normalizeString(options.openhandsEvents);
|
|
3023
|
+
let sourceStream = 'studio';
|
|
3024
|
+
let events = await readStudioEvents(paths, jobId, { limit }, fileSystem);
|
|
3025
|
+
let openhandsSignals = null;
|
|
3026
|
+
if (openhandsEventsPath) {
|
|
3027
|
+
const absoluteOpenhandsPath = path.isAbsolute(openhandsEventsPath)
|
|
3028
|
+
? openhandsEventsPath
|
|
3029
|
+
: path.join(projectPath, openhandsEventsPath);
|
|
3030
|
+
const rawOpenhandsEvents = await readOpenHandsEventsFile(absoluteOpenhandsPath, fileSystem);
|
|
3031
|
+
openhandsSignals = mapOpenHandsEventsToTaskSignals(rawOpenhandsEvents, {
|
|
3032
|
+
jobId: job.job_id
|
|
3033
|
+
});
|
|
3034
|
+
events = openhandsSignals.event;
|
|
3035
|
+
sourceStream = 'openhands';
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
const payload = buildCommandPayload('studio-events', job, { events });
|
|
3039
|
+
payload.limit = limit;
|
|
3040
|
+
payload.source_stream = sourceStream;
|
|
3041
|
+
if (sourceStream === 'openhands') {
|
|
3042
|
+
payload.openhands_events_file = path.relative(projectPath, path.isAbsolute(openhandsEventsPath)
|
|
3043
|
+
? openhandsEventsPath
|
|
3044
|
+
: path.join(projectPath, openhandsEventsPath)).replace(/\\/g, '/');
|
|
3045
|
+
payload.task = {
|
|
3046
|
+
...payload.task,
|
|
3047
|
+
summary: [
|
|
3048
|
+
`OpenHands events: ${events.length} | commands: ${openhandsSignals.commands.length} | errors: ${openhandsSignals.errors.length}`,
|
|
3049
|
+
`File changes: ${openhandsSignals.file_changes.length} | source: ${payload.openhands_events_file}`,
|
|
3050
|
+
`Next: ${payload.task.next_action}`
|
|
3051
|
+
],
|
|
3052
|
+
handoff: {
|
|
3053
|
+
...(payload.task.handoff || {}),
|
|
3054
|
+
source_stream: 'openhands',
|
|
3055
|
+
openhands_event_count: events.length,
|
|
3056
|
+
openhands_command_count: openhandsSignals.commands.length,
|
|
3057
|
+
openhands_error_count: openhandsSignals.errors.length
|
|
3058
|
+
},
|
|
3059
|
+
commands: openhandsSignals.commands,
|
|
3060
|
+
file_changes: openhandsSignals.file_changes,
|
|
3061
|
+
errors: openhandsSignals.errors,
|
|
3062
|
+
evidence: normalizeTaskEvidence([
|
|
3063
|
+
...(Array.isArray(payload.task.evidence) ? payload.task.evidence : []),
|
|
3064
|
+
...openhandsSignals.evidence,
|
|
3065
|
+
{
|
|
3066
|
+
type: 'openhands-event-file',
|
|
3067
|
+
ref: payload.openhands_events_file,
|
|
3068
|
+
detail: 'mapped'
|
|
3069
|
+
}
|
|
3070
|
+
])
|
|
3071
|
+
};
|
|
3072
|
+
payload.eventId = events.length > 0
|
|
3073
|
+
? events[events.length - 1].event_id
|
|
3074
|
+
: null;
|
|
3075
|
+
}
|
|
3076
|
+
payload.events = events;
|
|
2309
3077
|
printStudioEventsPayload(payload, options);
|
|
2310
3078
|
return payload;
|
|
2311
3079
|
}
|
|
@@ -2591,6 +3359,7 @@ function registerStudioCommands(program) {
|
|
|
2591
3359
|
.description('Show studio job event stream')
|
|
2592
3360
|
.option('--job <job-id>', 'Studio job id (defaults to latest)')
|
|
2593
3361
|
.option('--limit <number>', 'Maximum number of recent events to return', '50')
|
|
3362
|
+
.option('--openhands-events <path>', 'Optional OpenHands raw events file (.json/.jsonl) mapped to task stream')
|
|
2594
3363
|
.option('--json', 'Print machine-readable JSON output')
|
|
2595
3364
|
.action(async (options) => runStudioCommand(runStudioEventsCommand, options, 'events'));
|
|
2596
3365
|
|
package/package.json
CHANGED