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 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.1
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.1
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.0
6
- **Last Updated**: 2026-03-02
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`):
@@ -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
- return {
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 payload = buildCommandPayload('studio-resume', job);
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 events = await readStudioEvents(paths, jobId, { limit }, fileSystem);
2301
-
2302
- const payload = {
2303
- mode: 'studio-events',
2304
- success: true,
2305
- job_id: jobId,
2306
- limit,
2307
- events
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scene-capability-engine",
3
- "version": "3.5.1",
3
+ "version": "3.5.2",
4
4
  "description": "SCE (Scene Capability Engine) - A CLI tool and npm package for spec-driven development with AI coding assistants.",
5
5
  "main": "index.js",
6
6
  "bin": {