scene-capability-engine 3.6.60 → 3.6.62

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.
@@ -921,273 +921,6 @@ function extractEventArrayFromPayload(payload) {
921
921
  return [payload];
922
922
  }
923
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
924
  function extractCommandsFromStageMetadata(stageMetadata = {}) {
1192
925
  const gateSteps = Array.isArray(stageMetadata && stageMetadata.gate_steps)
1193
926
  ? stageMetadata.gate_steps
@@ -2919,21 +2652,7 @@ async function runStudioEventsCommand(options = {}, dependencies = {}) {
2919
2652
 
2920
2653
  const limit = normalizePositiveInteger(options.limit, 50);
2921
2654
  const job = await loadJob(paths, jobId, fileSystem);
2922
- const openhandsEventsPath = normalizeString(options.openhandsEvents);
2923
- let sourceStream = 'studio';
2924
2655
  let events = await readStudioEvents(paths, jobId, { limit }, fileSystem);
2925
- let openhandsSignals = null;
2926
- if (openhandsEventsPath) {
2927
- const absoluteOpenhandsPath = path.isAbsolute(openhandsEventsPath)
2928
- ? openhandsEventsPath
2929
- : path.join(projectPath, openhandsEventsPath);
2930
- const rawOpenhandsEvents = await readOpenHandsEventsFile(absoluteOpenhandsPath, fileSystem);
2931
- openhandsSignals = mapOpenHandsEventsToTaskSignals(rawOpenhandsEvents, {
2932
- jobId: job.job_id
2933
- });
2934
- events = openhandsSignals.event;
2935
- sourceStream = 'openhands';
2936
- }
2937
2656
 
2938
2657
  const payload = await buildCommandPayload('studio-events', job, {
2939
2658
  events,
@@ -2941,47 +2660,11 @@ async function runStudioEventsCommand(options = {}, dependencies = {}) {
2941
2660
  fileSystem
2942
2661
  });
2943
2662
  payload.limit = limit;
2944
- payload.source_stream = sourceStream;
2945
- if (sourceStream === 'openhands') {
2946
- payload.openhands_events_file = path.relative(projectPath, path.isAbsolute(openhandsEventsPath)
2947
- ? openhandsEventsPath
2948
- : path.join(projectPath, openhandsEventsPath)).replace(/\\/g, '/');
2949
- payload.task = {
2950
- ...payload.task,
2951
- summary: [
2952
- `OpenHands events: ${events.length} | commands: ${openhandsSignals.commands.length} | errors: ${openhandsSignals.errors.length}`,
2953
- `File changes: ${openhandsSignals.file_changes.length} | source: ${payload.openhands_events_file}`,
2954
- `Next: ${payload.task.next_action}`
2955
- ],
2956
- handoff: {
2957
- ...(payload.task.handoff || {}),
2958
- source_stream: 'openhands',
2959
- openhands_event_count: events.length,
2960
- openhands_command_count: openhandsSignals.commands.length,
2961
- openhands_error_count: openhandsSignals.errors.length
2962
- },
2963
- commands: openhandsSignals.commands,
2964
- file_changes: openhandsSignals.file_changes,
2965
- errors: openhandsSignals.errors,
2966
- evidence: normalizeTaskEvidence([
2967
- ...(Array.isArray(payload.task.evidence) ? payload.task.evidence : []),
2968
- ...openhandsSignals.evidence,
2969
- {
2970
- type: 'openhands-event-file',
2971
- ref: payload.openhands_events_file,
2972
- detail: 'mapped'
2973
- }
2974
- ])
2975
- };
2976
- payload.eventId = events.length > 0
2977
- ? events[events.length - 1].event_id
2978
- : null;
2979
- }
2663
+ payload.source_stream = 'studio';
2980
2664
  payload.events = events;
2981
2665
  const normalizedPayload = attachTaskFeedbackModel(payload);
2982
2666
  printStudioEventsPayload(normalizedPayload, options);
2983
2667
  return normalizedPayload;
2984
- return payload;
2985
2668
  }
2986
2669
 
2987
2670
  function printStudioIntakePayload(payload, options = {}) {
@@ -3267,7 +2950,6 @@ function registerStudioCommands(program) {
3267
2950
  .description('Show studio job event stream')
3268
2951
  .option('--job <job-id>', 'Studio job id (defaults to latest)')
3269
2952
  .option('--limit <number>', 'Maximum number of recent events to return', '50')
3270
- .option('--openhands-events <path>', 'Optional OpenHands raw events file (.json/.jsonl) mapped to task stream')
3271
2953
  .option('--json', 'Print machine-readable JSON output')
3272
2954
  .action(async (options) => runStudioCommand(runStudioEventsCommand, options, 'events'));
3273
2955
 
@@ -0,0 +1,216 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+ const WorkspaceStateManager = require('../workspace/multi/workspace-state-manager');
4
+ const {
5
+ buildLocalProjectId,
6
+ buildWorkspaceProjectId
7
+ } = require('./portfolio-projection-service');
8
+
9
+ const PROJECT_CANDIDATE_REASON_CODES = {
10
+ ROOT_ACCESSIBLE: 'project.root.accessible',
11
+ ROOT_INACCESSIBLE: 'project.root.inaccessible',
12
+ ROOT_INVALID_TYPE: 'project.root.invalid_type',
13
+ WORKSPACE_REGISTERED: 'project.workspace.registered',
14
+ SCE_PRESENT: 'project.sce.present',
15
+ ROOT_NOT_INITIALIZED: 'project.root.not_initialized',
16
+ INVALID_PROJECT_METADATA: 'project.metadata.invalid',
17
+ UNREGISTERED_PROJECT: 'project.sce.unregistered'
18
+ };
19
+
20
+ function normalizeString(value) {
21
+ if (typeof value !== 'string') {
22
+ return '';
23
+ }
24
+ return value.trim();
25
+ }
26
+
27
+ function normalizePath(value) {
28
+ return normalizeString(value).replace(/\\/g, '/');
29
+ }
30
+
31
+ function buildProjectName(rootDir) {
32
+ return path.basename(rootDir) || 'project';
33
+ }
34
+
35
+ async function resolveExactWorkspaceByRoot(rootDir, stateManager) {
36
+ const workspaces = await stateManager.listWorkspaces();
37
+ const normalizedRoot = normalizePath(rootDir);
38
+ return workspaces.find((workspace) => normalizePath(workspace && workspace.path) === normalizedRoot) || null;
39
+ }
40
+
41
+ async function inspectSceMetadata(rootDir, fileSystem = fs) {
42
+ const sceRoot = path.join(rootDir, '.sce');
43
+ if (!await fileSystem.pathExists(sceRoot)) {
44
+ return {
45
+ scePresent: false,
46
+ metadataValid: true,
47
+ metadataReasonCodes: []
48
+ };
49
+ }
50
+
51
+ try {
52
+ const sceStats = await fileSystem.stat(sceRoot);
53
+ if (!sceStats.isDirectory()) {
54
+ return {
55
+ scePresent: false,
56
+ metadataValid: false,
57
+ metadataReasonCodes: [PROJECT_CANDIDATE_REASON_CODES.ROOT_INVALID_TYPE]
58
+ };
59
+ }
60
+ } catch (_error) {
61
+ return {
62
+ scePresent: false,
63
+ metadataValid: false,
64
+ metadataReasonCodes: [PROJECT_CANDIDATE_REASON_CODES.ROOT_INACCESSIBLE]
65
+ };
66
+ }
67
+
68
+ const versionPath = path.join(sceRoot, 'version.json');
69
+ if (!await fileSystem.pathExists(versionPath)) {
70
+ return {
71
+ scePresent: true,
72
+ metadataValid: true,
73
+ metadataReasonCodes: [PROJECT_CANDIDATE_REASON_CODES.SCE_PRESENT]
74
+ };
75
+ }
76
+
77
+ try {
78
+ await fileSystem.readJson(versionPath);
79
+ return {
80
+ scePresent: true,
81
+ metadataValid: true,
82
+ metadataReasonCodes: [PROJECT_CANDIDATE_REASON_CODES.SCE_PRESENT]
83
+ };
84
+ } catch (_error) {
85
+ return {
86
+ scePresent: true,
87
+ metadataValid: false,
88
+ metadataReasonCodes: [
89
+ PROJECT_CANDIDATE_REASON_CODES.SCE_PRESENT,
90
+ PROJECT_CANDIDATE_REASON_CODES.INVALID_PROJECT_METADATA
91
+ ]
92
+ };
93
+ }
94
+ }
95
+
96
+ function dedupeReasonCodes(reasonCodes = []) {
97
+ return Array.from(new Set(reasonCodes.filter(Boolean)));
98
+ }
99
+
100
+ async function inspectProjectCandidate(options = {}, dependencies = {}) {
101
+ const fileSystem = dependencies.fileSystem || fs;
102
+ const stateManager = dependencies.stateManager || new WorkspaceStateManager(dependencies.workspaceStatePath);
103
+ const requestedRoot = normalizeString(options.root || options.rootDir);
104
+ if (!requestedRoot) {
105
+ throw new Error('--root is required');
106
+ }
107
+
108
+ const absoluteRoot = path.resolve(requestedRoot);
109
+ const rootDir = normalizePath(absoluteRoot);
110
+ const inspectedAt = new Date().toISOString();
111
+ const projectName = buildProjectName(rootDir);
112
+
113
+ if (!await fileSystem.pathExists(absoluteRoot)) {
114
+ return {
115
+ inspectedAt,
116
+ rootDir,
117
+ kind: 'invalid',
118
+ projectName,
119
+ readiness: 'blocked',
120
+ availability: 'inaccessible',
121
+ localCandidate: false,
122
+ reasonCodes: [PROJECT_CANDIDATE_REASON_CODES.ROOT_INACCESSIBLE]
123
+ };
124
+ }
125
+
126
+ let rootStats = null;
127
+ try {
128
+ rootStats = await fileSystem.stat(absoluteRoot);
129
+ } catch (_error) {
130
+ return {
131
+ inspectedAt,
132
+ rootDir,
133
+ kind: 'invalid',
134
+ projectName,
135
+ readiness: 'blocked',
136
+ availability: 'inaccessible',
137
+ localCandidate: false,
138
+ reasonCodes: [PROJECT_CANDIDATE_REASON_CODES.ROOT_INACCESSIBLE]
139
+ };
140
+ }
141
+
142
+ if (!rootStats.isDirectory()) {
143
+ return {
144
+ inspectedAt,
145
+ rootDir,
146
+ kind: 'invalid',
147
+ projectName,
148
+ readiness: 'blocked',
149
+ availability: 'degraded',
150
+ localCandidate: false,
151
+ reasonCodes: [PROJECT_CANDIDATE_REASON_CODES.ROOT_INVALID_TYPE]
152
+ };
153
+ }
154
+
155
+ const workspace = await resolveExactWorkspaceByRoot(absoluteRoot, stateManager);
156
+ const sceInspection = await inspectSceMetadata(absoluteRoot, fileSystem);
157
+ const metadataBlocked = sceInspection.scePresent && !sceInspection.metadataValid;
158
+
159
+ if (workspace) {
160
+ const workspaceBlocked = !sceInspection.scePresent || metadataBlocked;
161
+ return {
162
+ inspectedAt,
163
+ rootDir,
164
+ kind: 'workspace-backed',
165
+ projectId: buildWorkspaceProjectId(workspace.name),
166
+ workspaceId: workspace.name,
167
+ projectName,
168
+ readiness: workspaceBlocked ? 'blocked' : 'ready',
169
+ availability: workspaceBlocked ? 'degraded' : 'accessible',
170
+ localCandidate: false,
171
+ reasonCodes: dedupeReasonCodes([
172
+ PROJECT_CANDIDATE_REASON_CODES.WORKSPACE_REGISTERED,
173
+ PROJECT_CANDIDATE_REASON_CODES.ROOT_ACCESSIBLE,
174
+ ...(!sceInspection.scePresent ? [PROJECT_CANDIDATE_REASON_CODES.ROOT_NOT_INITIALIZED] : []),
175
+ ...sceInspection.metadataReasonCodes
176
+ ])
177
+ };
178
+ }
179
+
180
+ if (sceInspection.scePresent) {
181
+ return {
182
+ inspectedAt,
183
+ rootDir,
184
+ kind: 'local-sce-candidate',
185
+ projectId: buildLocalProjectId(rootDir),
186
+ projectName,
187
+ readiness: metadataBlocked ? 'blocked' : 'partial',
188
+ availability: metadataBlocked ? 'degraded' : 'degraded',
189
+ localCandidate: true,
190
+ reasonCodes: dedupeReasonCodes([
191
+ PROJECT_CANDIDATE_REASON_CODES.ROOT_ACCESSIBLE,
192
+ PROJECT_CANDIDATE_REASON_CODES.UNREGISTERED_PROJECT,
193
+ ...sceInspection.metadataReasonCodes
194
+ ])
195
+ };
196
+ }
197
+
198
+ return {
199
+ inspectedAt,
200
+ rootDir,
201
+ kind: 'directory-candidate',
202
+ projectName,
203
+ readiness: 'pending',
204
+ availability: 'accessible',
205
+ localCandidate: true,
206
+ reasonCodes: [
207
+ PROJECT_CANDIDATE_REASON_CODES.ROOT_ACCESSIBLE,
208
+ PROJECT_CANDIDATE_REASON_CODES.ROOT_NOT_INITIALIZED
209
+ ]
210
+ };
211
+ }
212
+
213
+ module.exports = {
214
+ PROJECT_CANDIDATE_REASON_CODES,
215
+ inspectProjectCandidate
216
+ };