scene-capability-engine 3.5.0 → 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.
@@ -12,8 +12,10 @@ const { findRelatedSpecs } = require('../spec/related-specs');
12
12
  const { captureTimelineCheckpoint } = require('../runtime/project-timeline');
13
13
  const { runProblemEvaluation } = require('../problem/problem-evaluator');
14
14
  const {
15
+ loadStudioIntakePolicy,
15
16
  runStudioAutoIntake,
16
- runStudioSpecGovernance
17
+ runStudioSpecGovernance,
18
+ runStudioSceneBackfill
17
19
  } = require('../studio/spec-intake-governor');
18
20
 
19
21
  const STUDIO_JOB_API_VERSION = 'sce.studio.job/v0.1';
@@ -641,6 +643,7 @@ async function appendStudioEvent(paths, job, eventType, metadata = {}, fileSyste
641
643
  const eventLine = `${JSON.stringify(event)}\n`;
642
644
  const eventFile = getEventLogFilePath(paths, job.job_id);
643
645
  await fileSystem.appendFile(eventFile, eventLine, 'utf8');
646
+ return event;
644
647
  }
645
648
 
646
649
  async function readStudioEvents(paths, jobId, options = {}, fileSystem = fs) {
@@ -728,6 +731,697 @@ function resolveNextAction(job) {
728
731
  return 'complete';
729
732
  }
730
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
+
731
1425
  function toRelativePosix(projectPath, absolutePath) {
732
1426
  return path.relative(projectPath, absolutePath).replace(/\\/g, '/');
733
1427
  }
@@ -1168,8 +1862,8 @@ function ensureNotRolledBack(job, stageName) {
1168
1862
  }
1169
1863
  }
1170
1864
 
1171
- function buildCommandPayload(mode, job) {
1172
- return {
1865
+ function buildCommandPayload(mode, job, options = {}) {
1866
+ const base = {
1173
1867
  mode,
1174
1868
  success: true,
1175
1869
  job_id: job.job_id,
@@ -1178,6 +1872,10 @@ function buildCommandPayload(mode, job) {
1178
1872
  next_action: resolveNextAction(job),
1179
1873
  artifacts: { ...job.artifacts }
1180
1874
  };
1875
+ return {
1876
+ ...base,
1877
+ ...buildTaskEnvelope(mode, job, options)
1878
+ };
1181
1879
  }
1182
1880
 
1183
1881
  function buildJobDomainChainMetadata(job = {}) {
@@ -1358,6 +2056,20 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1358
2056
  throw new Error('--scene is required');
1359
2057
  }
1360
2058
 
2059
+ const intakePolicyBundle = await loadStudioIntakePolicy(projectPath, fileSystem);
2060
+ const intakePolicy = intakePolicyBundle.policy || {};
2061
+ const governancePolicy = intakePolicy.governance || {};
2062
+ if (manualSpecMode && intakePolicy.allow_manual_spec_override !== true) {
2063
+ throw new Error(
2064
+ '--manual-spec is disabled by studio intake policy (allow_manual_spec_override=false)'
2065
+ );
2066
+ }
2067
+ if (skipSpecGovernance && governancePolicy.require_auto_on_plan !== false) {
2068
+ throw new Error(
2069
+ '--no-spec-governance is disabled by studio intake policy (governance.require_auto_on_plan=true)'
2070
+ );
2071
+ }
2072
+
1361
2073
  let domainChainBinding = await resolveDomainChainBinding({
1362
2074
  sceneId,
1363
2075
  specId,
@@ -1629,7 +2341,7 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1629
2341
  };
1630
2342
 
1631
2343
  await saveJob(paths, job, fileSystem);
1632
- await appendStudioEvent(paths, job, 'stage.plan.completed', {
2344
+ const planEvent = await appendStudioEvent(paths, job, 'stage.plan.completed', {
1633
2345
  from_chat: fromChat,
1634
2346
  scene_id: sceneId,
1635
2347
  spec_id: effectiveSpecId,
@@ -1655,7 +2367,10 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1655
2367
  }, fileSystem);
1656
2368
  await writeLatestJob(paths, jobId, fileSystem);
1657
2369
 
1658
- const payload = buildCommandPayload('studio-plan', job);
2370
+ const payload = buildCommandPayload('studio-plan', job, {
2371
+ stageName: 'plan',
2372
+ event: planEvent
2373
+ });
1659
2374
  payload.scene = {
1660
2375
  id: sceneId,
1661
2376
  spec_id: effectiveSpecId
@@ -1740,7 +2455,7 @@ async function runStudioGenerateCommand(options = {}, dependencies = {}) {
1740
2455
  });
1741
2456
 
1742
2457
  await saveJob(paths, job, fileSystem);
1743
- await appendStudioEvent(paths, job, 'stage.generate.completed', {
2458
+ const generateEvent = await appendStudioEvent(paths, job, 'stage.generate.completed', {
1744
2459
  scene_id: sceneId,
1745
2460
  target: job.target,
1746
2461
  patch_bundle_id: patchBundleId,
@@ -1750,7 +2465,10 @@ async function runStudioGenerateCommand(options = {}, dependencies = {}) {
1750
2465
  }, fileSystem);
1751
2466
  await writeLatestJob(paths, jobId, fileSystem);
1752
2467
 
1753
- const payload = buildCommandPayload('studio-generate', job);
2468
+ const payload = buildCommandPayload('studio-generate', job, {
2469
+ stageName: 'generate',
2470
+ event: generateEvent
2471
+ });
1754
2472
  printStudioPayload(payload, options);
1755
2473
  return payload;
1756
2474
  }
@@ -1810,14 +2528,17 @@ async function runStudioApplyCommand(options = {}, dependencies = {}) {
1810
2528
  });
1811
2529
 
1812
2530
  await saveJob(paths, job, fileSystem);
1813
- await appendStudioEvent(paths, job, 'stage.apply.completed', {
2531
+ const applyEvent = await appendStudioEvent(paths, job, 'stage.apply.completed', {
1814
2532
  patch_bundle_id: patchBundleId,
1815
2533
  auth_required: authResult.required,
1816
2534
  problem_evaluation: summarizeProblemEvaluation(applyProblemEvaluation)
1817
2535
  }, fileSystem);
1818
2536
  await writeLatestJob(paths, jobId, fileSystem);
1819
2537
 
1820
- const payload = buildCommandPayload('studio-apply', job);
2538
+ const payload = buildCommandPayload('studio-apply', job, {
2539
+ stageName: 'apply',
2540
+ event: applyEvent
2541
+ });
1821
2542
  printStudioPayload(payload, options);
1822
2543
  return payload;
1823
2544
  }
@@ -1924,6 +2645,7 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
1924
2645
  profile,
1925
2646
  passed: false,
1926
2647
  report: verifyReportPath,
2648
+ gate_steps: gateResult.steps,
1927
2649
  problem_evaluation: summarizeProblemEvaluation(verifyProblemEvaluation),
1928
2650
  domain_chain: domainChainMetadata,
1929
2651
  auto_errorbook_records: autoErrorbookRecords
@@ -1946,13 +2668,14 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
1946
2668
  profile,
1947
2669
  passed: true,
1948
2670
  report: verifyReportPath,
2671
+ gate_steps: gateResult.steps,
1949
2672
  problem_evaluation: summarizeProblemEvaluation(verifyProblemEvaluation),
1950
2673
  domain_chain: domainChainMetadata,
1951
2674
  auto_errorbook_records: autoErrorbookRecords
1952
2675
  });
1953
2676
 
1954
2677
  await saveJob(paths, job, fileSystem);
1955
- await appendStudioEvent(paths, job, 'stage.verify.completed', {
2678
+ const verifyEvent = await appendStudioEvent(paths, job, 'stage.verify.completed', {
1956
2679
  profile,
1957
2680
  passed: true,
1958
2681
  report: verifyReportPath,
@@ -1962,7 +2685,10 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
1962
2685
  }, fileSystem);
1963
2686
  await writeLatestJob(paths, jobId, fileSystem);
1964
2687
 
1965
- const payload = buildCommandPayload('studio-verify', job);
2688
+ const payload = buildCommandPayload('studio-verify', job, {
2689
+ stageName: 'verify',
2690
+ event: verifyEvent
2691
+ });
1966
2692
  printStudioPayload(payload, options);
1967
2693
  return payload;
1968
2694
  }
@@ -2102,6 +2828,7 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
2102
2828
  release_ref: releaseRef,
2103
2829
  passed: false,
2104
2830
  report: releaseReportPath,
2831
+ gate_steps: gateResult.steps,
2105
2832
  auth_required: authResult.required,
2106
2833
  problem_evaluation: summarizeProblemEvaluation(releaseProblemEvaluation),
2107
2834
  domain_chain: domainChainMetadata,
@@ -2127,6 +2854,7 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
2127
2854
  channel,
2128
2855
  release_ref: releaseRef,
2129
2856
  report: releaseReportPath,
2857
+ gate_steps: gateResult.steps,
2130
2858
  auth_required: authResult.required,
2131
2859
  problem_evaluation: summarizeProblemEvaluation(releaseProblemEvaluation),
2132
2860
  domain_chain: domainChainMetadata,
@@ -2155,7 +2883,7 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
2155
2883
  }
2156
2884
 
2157
2885
  await saveJob(paths, job, fileSystem);
2158
- await appendStudioEvent(paths, job, 'stage.release.completed', {
2886
+ const releaseEvent = await appendStudioEvent(paths, job, 'stage.release.completed', {
2159
2887
  channel,
2160
2888
  release_ref: releaseRef,
2161
2889
  report: releaseReportPath,
@@ -2166,7 +2894,10 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
2166
2894
  }, fileSystem);
2167
2895
  await writeLatestJob(paths, jobId, fileSystem);
2168
2896
 
2169
- const payload = buildCommandPayload('studio-release', job);
2897
+ const payload = buildCommandPayload('studio-release', job, {
2898
+ stageName: 'release',
2899
+ event: releaseEvent
2900
+ });
2170
2901
  printStudioPayload(payload, options);
2171
2902
  return payload;
2172
2903
  }
@@ -2184,7 +2915,10 @@ async function runStudioResumeCommand(options = {}, dependencies = {}) {
2184
2915
  }
2185
2916
 
2186
2917
  const job = await loadJob(paths, jobId, fileSystem);
2187
- 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
+ });
2188
2922
  payload.success = true;
2189
2923
  printStudioPayload(payload, options);
2190
2924
  return payload;
@@ -2236,12 +2970,15 @@ async function runStudioRollbackCommand(options = {}, dependencies = {}) {
2236
2970
  }
2237
2971
 
2238
2972
  await saveJob(paths, job, fileSystem);
2239
- await appendStudioEvent(paths, job, 'job.rolled_back', {
2973
+ const rollbackEvent = await appendStudioEvent(paths, job, 'job.rolled_back', {
2240
2974
  reason
2241
2975
  }, fileSystem);
2242
2976
  await writeLatestJob(paths, jobId, fileSystem);
2243
2977
 
2244
- const payload = buildCommandPayload('studio-rollback', job);
2978
+ const payload = buildCommandPayload('studio-rollback', job, {
2979
+ stageName: 'rollback',
2980
+ event: rollbackEvent
2981
+ });
2245
2982
  payload.rollback = { ...job.rollback };
2246
2983
  printStudioPayload(payload, options);
2247
2984
  return payload;
@@ -2281,15 +3018,62 @@ async function runStudioEventsCommand(options = {}, dependencies = {}) {
2281
3018
  }
2282
3019
 
2283
3020
  const limit = normalizePositiveInteger(options.limit, 50);
2284
- const events = await readStudioEvents(paths, jobId, { limit }, fileSystem);
2285
-
2286
- const payload = {
2287
- mode: 'studio-events',
2288
- success: true,
2289
- job_id: jobId,
2290
- limit,
2291
- events
2292
- };
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;
2293
3077
  printStudioEventsPayload(payload, options);
2294
3078
  return payload;
2295
3079
  }
@@ -2383,6 +3167,19 @@ function printStudioPortfolioPayload(payload, options = {}) {
2383
3167
  console.log(` Overflow scenes: ${summary.overflow_scenes || 0}`);
2384
3168
  }
2385
3169
 
3170
+ function printStudioBackfillPayload(payload, options = {}) {
3171
+ if (options.json) {
3172
+ console.log(JSON.stringify(payload, null, 2));
3173
+ return;
3174
+ }
3175
+ console.log(chalk.blue('Studio scene backfill'));
3176
+ console.log(` Source scene: ${payload.source_scene || 'scene.unassigned'}`);
3177
+ console.log(` Candidates: ${payload.summary ? payload.summary.candidate_count : 0}`);
3178
+ console.log(` Changed: ${payload.summary ? payload.summary.changed_count : 0}`);
3179
+ console.log(` Apply: ${payload.apply ? 'yes' : 'no'}`);
3180
+ console.log(` Override file: ${payload.override_file || 'n/a'}`);
3181
+ }
3182
+
2386
3183
  async function runStudioPortfolioCommand(options = {}, dependencies = {}) {
2387
3184
  const projectPath = dependencies.projectPath || process.cwd();
2388
3185
  const fileSystem = dependencies.fileSystem || fs;
@@ -2404,6 +3201,29 @@ async function runStudioPortfolioCommand(options = {}, dependencies = {}) {
2404
3201
  return payload;
2405
3202
  }
2406
3203
 
3204
+ async function runStudioBackfillSpecScenesCommand(options = {}, dependencies = {}) {
3205
+ const projectPath = dependencies.projectPath || process.cwd();
3206
+ const fileSystem = dependencies.fileSystem || fs;
3207
+ const backfillOptions = {
3208
+ scene: normalizeString(options.scene),
3209
+ all: options.all === true,
3210
+ limit: options.limit,
3211
+ apply: options.apply === true,
3212
+ refresh_governance: options.refreshGovernance !== false
3213
+ };
3214
+ if (options.activeOnly === true) {
3215
+ backfillOptions.active_only = true;
3216
+ }
3217
+
3218
+ const payload = await runStudioSceneBackfill(backfillOptions, {
3219
+ projectPath,
3220
+ fileSystem
3221
+ });
3222
+
3223
+ printStudioBackfillPayload(payload, options);
3224
+ return payload;
3225
+ }
3226
+
2407
3227
  async function runStudioCommand(handler, options, stageName = '') {
2408
3228
  try {
2409
3229
  const stage = normalizeString(stageName) || 'unknown';
@@ -2447,9 +3267,9 @@ function registerStudioCommands(program) {
2447
3267
  .requiredOption('--from-chat <session>', 'Chat session identifier or transcript reference')
2448
3268
  .option('--spec <spec-id>', 'Optional spec binding for domain-chain context ingestion')
2449
3269
  .option('--goal <goal>', 'Optional goal summary')
2450
- .option('--manual-spec', 'Disable auto intake and only use explicit --spec or existing scene binding')
3270
+ .option('--manual-spec', 'Legacy bypass flag (disabled by default policy)')
2451
3271
  .option('--target <target>', 'Target integration profile', 'default')
2452
- .option('--no-spec-governance', 'Skip auto portfolio governance snapshot on plan stage')
3272
+ .option('--no-spec-governance', 'Legacy bypass flag (disabled by default policy)')
2453
3273
  .option('--job <job-id>', 'Reuse an explicit studio job id')
2454
3274
  .option('--json', 'Print machine-readable JSON output')
2455
3275
  .action(async (options) => runStudioCommand(runStudioPlanCommand, options, 'plan'));
@@ -2462,7 +3282,7 @@ function registerStudioCommands(program) {
2462
3282
  .option('--spec <spec-id>', 'Optional explicit spec id')
2463
3283
  .option('--goal <goal>', 'Goal text used for intent classification')
2464
3284
  .option('--apply', 'Create spec when decision is create_spec')
2465
- .option('--manual-spec', 'Disable auto intake and keep explicit/manual binding only')
3285
+ .option('--manual-spec', 'Legacy bypass flag (disabled by default policy)')
2466
3286
  .option('--json', 'Print machine-readable JSON output')
2467
3287
  .action(async (options) => runStudioCommand(runStudioIntakeCommand, options, 'intake'));
2468
3288
 
@@ -2475,6 +3295,18 @@ function registerStudioCommands(program) {
2475
3295
  .option('--json', 'Print machine-readable JSON output')
2476
3296
  .action(async (options) => runStudioCommand(runStudioPortfolioCommand, options, 'portfolio'));
2477
3297
 
3298
+ studio
3299
+ .command('backfill-spec-scenes')
3300
+ .description('Backfill scene bindings for historical specs (writes override mapping when --apply)')
3301
+ .option('--scene <scene-id>', 'Source scene filter (default: scene.unassigned)')
3302
+ .option('--all', 'Include completed/stale specs (default uses active-only policy)')
3303
+ .option('--active-only', 'Force active-only filtering')
3304
+ .option('--limit <n>', 'Maximum number of specs to process')
3305
+ .option('--apply', 'Write mapping to .sce/spec-governance/spec-scene-overrides.json')
3306
+ .option('--no-refresh-governance', 'Skip portfolio refresh after apply')
3307
+ .option('--json', 'Print machine-readable JSON output')
3308
+ .action(async (options) => runStudioCommand(runStudioBackfillSpecScenesCommand, options, 'backfill-spec-scenes'));
3309
+
2478
3310
  studio
2479
3311
  .command('generate')
2480
3312
  .description('Generate patch bundle metadata for a planned studio job (scene inherited from plan)')
@@ -2527,6 +3359,7 @@ function registerStudioCommands(program) {
2527
3359
  .description('Show studio job event stream')
2528
3360
  .option('--job <job-id>', 'Studio job id (defaults to latest)')
2529
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')
2530
3363
  .option('--json', 'Print machine-readable JSON output')
2531
3364
  .action(async (options) => runStudioCommand(runStudioEventsCommand, options, 'events'));
2532
3365
 
@@ -2567,6 +3400,7 @@ module.exports = {
2567
3400
  runStudioRollbackCommand,
2568
3401
  runStudioEventsCommand,
2569
3402
  runStudioPortfolioCommand,
3403
+ runStudioBackfillSpecScenesCommand,
2570
3404
  runStudioResumeCommand,
2571
3405
  registerStudioCommands
2572
3406
  };