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.
- package/CHANGELOG.md +30 -0
- package/README.md +16 -2
- package/README.zh.md +16 -2
- package/docs/command-reference.md +32 -4
- package/lib/commands/studio.js +862 -28
- package/lib/spec/related-specs.js +10 -2
- package/lib/spec/scene-binding-overrides.js +115 -0
- package/lib/studio/spec-intake-governor.js +328 -5
- package/lib/workspace/takeover-baseline.js +25 -0
- package/package.json +1 -1
- package/template/.sce/config/studio-intake-policy.json +86 -0
package/lib/commands/studio.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
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', '
|
|
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', '
|
|
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', '
|
|
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
|
};
|