scene-capability-engine 3.5.1 → 3.6.0
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 +34 -0
- package/README.md +22 -2
- package/README.zh.md +22 -2
- package/bin/scene-capability-engine.js +2 -0
- package/docs/command-reference.md +34 -3
- package/lib/commands/studio.js +882 -58
- package/lib/commands/task.js +630 -68
- package/lib/state/sce-state-store.js +594 -0
- package/lib/task/task-ref-registry.js +139 -0
- package/package.json +1 -1
package/lib/commands/studio.js
CHANGED
|
@@ -11,6 +11,8 @@ const {
|
|
|
11
11
|
const { findRelatedSpecs } = require('../spec/related-specs');
|
|
12
12
|
const { captureTimelineCheckpoint } = require('../runtime/project-timeline');
|
|
13
13
|
const { runProblemEvaluation } = require('../problem/problem-evaluator');
|
|
14
|
+
const { TaskRefRegistry } = require('../task/task-ref-registry');
|
|
15
|
+
const { getSceStateStore } = require('../state/sce-state-store');
|
|
14
16
|
const {
|
|
15
17
|
loadStudioIntakePolicy,
|
|
16
18
|
runStudioAutoIntake,
|
|
@@ -40,8 +42,7 @@ function resolveStudioPaths(projectPath = process.cwd()) {
|
|
|
40
42
|
projectPath,
|
|
41
43
|
studioDir,
|
|
42
44
|
jobsDir: path.join(studioDir, 'jobs'),
|
|
43
|
-
latestFile: path.join(studioDir, 'latest-job.json')
|
|
44
|
-
eventsDir: path.join(studioDir, 'events')
|
|
45
|
+
latestFile: path.join(studioDir, 'latest-job.json')
|
|
45
46
|
};
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -597,7 +598,6 @@ async function writeStudioReport(projectPath, relativePath, payload, fileSystem
|
|
|
597
598
|
|
|
598
599
|
async function ensureStudioDirectories(paths, fileSystem = fs) {
|
|
599
600
|
await fileSystem.ensureDir(paths.jobsDir);
|
|
600
|
-
await fileSystem.ensureDir(paths.eventsDir);
|
|
601
601
|
}
|
|
602
602
|
|
|
603
603
|
async function writeLatestJob(paths, jobId, fileSystem = fs) {
|
|
@@ -622,10 +622,6 @@ function getJobFilePath(paths, jobId) {
|
|
|
622
622
|
return path.join(paths.jobsDir, `${jobId}.json`);
|
|
623
623
|
}
|
|
624
624
|
|
|
625
|
-
function getEventLogFilePath(paths, jobId) {
|
|
626
|
-
return path.join(paths.eventsDir, `${jobId}.jsonl`);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
625
|
async function saveJob(paths, job, fileSystem = fs) {
|
|
630
626
|
const jobFile = getJobFilePath(paths, job.job_id);
|
|
631
627
|
await fileSystem.writeJson(jobFile, job, { spaces: 2 });
|
|
@@ -640,39 +636,28 @@ async function appendStudioEvent(paths, job, eventType, metadata = {}, fileSyste
|
|
|
640
636
|
timestamp: nowIso(),
|
|
641
637
|
metadata
|
|
642
638
|
};
|
|
643
|
-
const
|
|
644
|
-
const
|
|
645
|
-
|
|
639
|
+
const sceneId = normalizeString(job?.scene?.id) || null;
|
|
640
|
+
const specId = normalizeString(job?.scene?.spec_id) || normalizeString(job?.source?.spec_id) || null;
|
|
641
|
+
const stateStore = getSceStateStore(paths.projectPath, { fileSystem });
|
|
642
|
+
const persisted = await stateStore.appendStudioEvent({
|
|
643
|
+
...event,
|
|
644
|
+
scene_id: sceneId,
|
|
645
|
+
spec_id: specId
|
|
646
|
+
});
|
|
647
|
+
if (!persisted) {
|
|
648
|
+
throw new Error('Failed to persist studio event into sqlite state store');
|
|
649
|
+
}
|
|
650
|
+
return event;
|
|
646
651
|
}
|
|
647
652
|
|
|
648
653
|
async function readStudioEvents(paths, jobId, options = {}, fileSystem = fs) {
|
|
649
654
|
const { limit = 50 } = options;
|
|
650
|
-
const
|
|
651
|
-
const
|
|
652
|
-
if (
|
|
653
|
-
|
|
655
|
+
const stateStore = getSceStateStore(paths.projectPath, { fileSystem });
|
|
656
|
+
const events = await stateStore.listStudioEvents(jobId, { limit });
|
|
657
|
+
if (events === null) {
|
|
658
|
+
throw new Error('SQLite state backend unavailable while reading studio events');
|
|
654
659
|
}
|
|
655
|
-
|
|
656
|
-
const content = await fileSystem.readFile(eventFile, 'utf8');
|
|
657
|
-
const lines = content
|
|
658
|
-
.split(/\r?\n/)
|
|
659
|
-
.map((line) => line.trim())
|
|
660
|
-
.filter(Boolean);
|
|
661
|
-
|
|
662
|
-
const parsed = [];
|
|
663
|
-
for (const line of lines) {
|
|
664
|
-
try {
|
|
665
|
-
const payload = JSON.parse(line);
|
|
666
|
-
parsed.push(payload);
|
|
667
|
-
} catch (_error) {
|
|
668
|
-
// Ignore malformed lines to keep event stream robust.
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
if (limit <= 0) {
|
|
673
|
-
return parsed;
|
|
674
|
-
}
|
|
675
|
-
return parsed.slice(-limit);
|
|
660
|
+
return events;
|
|
676
661
|
}
|
|
677
662
|
|
|
678
663
|
async function loadJob(paths, jobId, fileSystem = fs) {
|
|
@@ -730,6 +715,705 @@ function resolveNextAction(job) {
|
|
|
730
715
|
return 'complete';
|
|
731
716
|
}
|
|
732
717
|
|
|
718
|
+
function resolveTaskStage(mode, job, explicitStage = '') {
|
|
719
|
+
const normalizedExplicit = normalizeString(explicitStage);
|
|
720
|
+
if (normalizedExplicit) {
|
|
721
|
+
return normalizedExplicit;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const modeSuffix = normalizeString(mode).replace(/^studio-/, '');
|
|
725
|
+
if (modeSuffix && (STAGE_ORDER.includes(modeSuffix) || modeSuffix === 'rollback')) {
|
|
726
|
+
return modeSuffix;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (normalizeString(job && job.status) === 'rolled_back') {
|
|
730
|
+
return 'rollback';
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
for (let i = STAGE_ORDER.length - 1; i >= 0; i -= 1) {
|
|
734
|
+
const stageName = STAGE_ORDER[i];
|
|
735
|
+
const stage = job && job.stages ? job.stages[stageName] : null;
|
|
736
|
+
const status = normalizeString(stage && stage.status);
|
|
737
|
+
if (status && status !== 'pending') {
|
|
738
|
+
return stageName;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return 'plan';
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function normalizeTaskFileChanges(fileChanges = []) {
|
|
746
|
+
const entries = Array.isArray(fileChanges) ? fileChanges : [];
|
|
747
|
+
const seen = new Set();
|
|
748
|
+
const normalized = [];
|
|
749
|
+
for (const entry of entries) {
|
|
750
|
+
const pathRef = normalizeString(entry && entry.path);
|
|
751
|
+
if (!pathRef) {
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
const line = Number.parseInt(`${entry && entry.line != null ? entry.line : 1}`, 10);
|
|
755
|
+
const normalizedLine = Number.isFinite(line) && line > 0 ? line : 1;
|
|
756
|
+
const diffRef = normalizeString(entry && entry.diffRef) || `${pathRef}:${normalizedLine}`;
|
|
757
|
+
if (seen.has(diffRef)) {
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
seen.add(diffRef);
|
|
761
|
+
normalized.push({
|
|
762
|
+
path: pathRef,
|
|
763
|
+
line: normalizedLine,
|
|
764
|
+
diffRef
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
return normalized;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function normalizeTaskCommands(commands = []) {
|
|
771
|
+
const entries = Array.isArray(commands) ? commands : [];
|
|
772
|
+
const normalized = [];
|
|
773
|
+
for (const entry of entries) {
|
|
774
|
+
const cmd = normalizeString(entry && entry.cmd);
|
|
775
|
+
const stdout = typeof (entry && entry.stdout) === 'string' ? entry.stdout : '';
|
|
776
|
+
const stderr = typeof (entry && entry.stderr) === 'string' ? entry.stderr : '';
|
|
777
|
+
const exitCodeRaw = entry && entry.exit_code;
|
|
778
|
+
const exitCode = Number.isFinite(Number(exitCodeRaw))
|
|
779
|
+
? Number(exitCodeRaw)
|
|
780
|
+
: null;
|
|
781
|
+
const logPath = normalizeString(entry && entry.log_path) || null;
|
|
782
|
+
if (!cmd && !stdout && !stderr && exitCode == null && !logPath) {
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
normalized.push({
|
|
786
|
+
cmd: cmd || 'n/a',
|
|
787
|
+
exit_code: exitCode,
|
|
788
|
+
stdout,
|
|
789
|
+
stderr,
|
|
790
|
+
log_path: logPath
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
return normalized;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function buildErrorBundle(entry = {}) {
|
|
797
|
+
const lines = [];
|
|
798
|
+
const pushLine = (key, value) => {
|
|
799
|
+
const normalized = typeof value === 'string' ? value.trim() : `${value || ''}`.trim();
|
|
800
|
+
if (!normalized) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
lines.push(`${key}: ${normalized}`);
|
|
804
|
+
};
|
|
805
|
+
pushLine('message', entry.message);
|
|
806
|
+
pushLine('step', entry.step_id || entry.step);
|
|
807
|
+
pushLine('cmd', entry.cmd);
|
|
808
|
+
if (entry.exit_code != null && `${entry.exit_code}`.trim()) {
|
|
809
|
+
lines.push(`exit_code: ${entry.exit_code}`);
|
|
810
|
+
}
|
|
811
|
+
pushLine('skip_reason', entry.skip_reason);
|
|
812
|
+
pushLine('stderr', entry.stderr);
|
|
813
|
+
pushLine('stdout', entry.stdout);
|
|
814
|
+
return lines.join('\n');
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function normalizeTaskErrors(errors = []) {
|
|
818
|
+
const entries = Array.isArray(errors) ? errors : [];
|
|
819
|
+
const normalized = [];
|
|
820
|
+
for (const entry of entries) {
|
|
821
|
+
const message = normalizeString(entry && entry.message);
|
|
822
|
+
if (!message) {
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
const errorBundle = normalizeString(entry && entry.error_bundle) || buildErrorBundle(entry);
|
|
826
|
+
normalized.push({
|
|
827
|
+
message,
|
|
828
|
+
error_bundle: errorBundle
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
return normalized;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function normalizeTaskEvidence(evidence = []) {
|
|
835
|
+
const entries = Array.isArray(evidence) ? evidence : [];
|
|
836
|
+
const normalized = [];
|
|
837
|
+
const seen = new Set();
|
|
838
|
+
for (const entry of entries) {
|
|
839
|
+
if (typeof entry === 'string') {
|
|
840
|
+
const ref = normalizeString(entry);
|
|
841
|
+
if (!ref || seen.has(ref)) {
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
seen.add(ref);
|
|
845
|
+
normalized.push({
|
|
846
|
+
type: 'reference',
|
|
847
|
+
ref,
|
|
848
|
+
detail: null
|
|
849
|
+
});
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const type = normalizeString(entry && entry.type) || 'reference';
|
|
854
|
+
const ref = normalizeString(entry && entry.ref);
|
|
855
|
+
const detail = normalizeString(entry && entry.detail) || null;
|
|
856
|
+
const key = `${type}:${ref}:${detail || ''}`;
|
|
857
|
+
if ((!ref && !detail) || seen.has(key)) {
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
seen.add(key);
|
|
861
|
+
normalized.push({
|
|
862
|
+
type,
|
|
863
|
+
ref: ref || null,
|
|
864
|
+
detail
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
return normalized;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function pickFirstString(values = []) {
|
|
871
|
+
for (const value of values) {
|
|
872
|
+
if (typeof value === 'string' && value.trim()) {
|
|
873
|
+
return value.trim();
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return '';
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function pickFirstNumber(values = []) {
|
|
880
|
+
for (const value of values) {
|
|
881
|
+
if (value == null || value === '') {
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
const numberValue = Number(value);
|
|
885
|
+
if (Number.isFinite(numberValue)) {
|
|
886
|
+
return numberValue;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function extractEventArrayFromPayload(payload) {
|
|
893
|
+
if (Array.isArray(payload)) {
|
|
894
|
+
return payload;
|
|
895
|
+
}
|
|
896
|
+
if (!payload || typeof payload !== 'object') {
|
|
897
|
+
return [];
|
|
898
|
+
}
|
|
899
|
+
const candidateKeys = ['events', 'items', 'data', 'records', 'entries'];
|
|
900
|
+
for (const key of candidateKeys) {
|
|
901
|
+
if (Array.isArray(payload[key])) {
|
|
902
|
+
return payload[key];
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return [payload];
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
async function readOpenHandsEventsFile(openhandsEventsPath, fileSystem = fs) {
|
|
909
|
+
const normalizedPath = normalizeString(openhandsEventsPath);
|
|
910
|
+
if (!normalizedPath) {
|
|
911
|
+
return [];
|
|
912
|
+
}
|
|
913
|
+
const exists = await fileSystem.pathExists(normalizedPath);
|
|
914
|
+
if (!exists) {
|
|
915
|
+
throw new Error(`OpenHands events file not found: ${normalizedPath}`);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const content = await fileSystem.readFile(normalizedPath, 'utf8');
|
|
919
|
+
const trimmed = `${content || ''}`.trim();
|
|
920
|
+
if (!trimmed) {
|
|
921
|
+
return [];
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
try {
|
|
925
|
+
const parsed = JSON.parse(trimmed);
|
|
926
|
+
return extractEventArrayFromPayload(parsed);
|
|
927
|
+
} catch (_error) {
|
|
928
|
+
const lines = trimmed
|
|
929
|
+
.split(/\r?\n/)
|
|
930
|
+
.map((line) => line.trim())
|
|
931
|
+
.filter(Boolean);
|
|
932
|
+
const parsedLines = [];
|
|
933
|
+
for (const line of lines) {
|
|
934
|
+
try {
|
|
935
|
+
parsedLines.push(JSON.parse(line));
|
|
936
|
+
} catch (_innerError) {
|
|
937
|
+
// Skip malformed JSONL lines to keep ingestion robust.
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return parsedLines;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function normalizeOpenHandsEventRecord(rawEvent, index, jobId) {
|
|
945
|
+
const raw = rawEvent && typeof rawEvent === 'object'
|
|
946
|
+
? rawEvent
|
|
947
|
+
: { value: rawEvent };
|
|
948
|
+
const eventId = pickFirstString([
|
|
949
|
+
raw.event_id,
|
|
950
|
+
raw.eventId,
|
|
951
|
+
raw.id,
|
|
952
|
+
raw.uuid
|
|
953
|
+
]) || `oh-evt-${index + 1}`;
|
|
954
|
+
const eventType = pickFirstString([
|
|
955
|
+
raw.event_type,
|
|
956
|
+
raw.eventType,
|
|
957
|
+
raw.type,
|
|
958
|
+
raw.kind,
|
|
959
|
+
raw.action
|
|
960
|
+
]) || 'unknown';
|
|
961
|
+
const timestamp = pickFirstString([
|
|
962
|
+
raw.timestamp,
|
|
963
|
+
raw.time,
|
|
964
|
+
raw.created_at,
|
|
965
|
+
raw.createdAt,
|
|
966
|
+
raw.ts
|
|
967
|
+
]) || nowIso();
|
|
968
|
+
return {
|
|
969
|
+
api_version: STUDIO_EVENT_API_VERSION,
|
|
970
|
+
event_id: eventId,
|
|
971
|
+
job_id: jobId,
|
|
972
|
+
event_type: `openhands.${eventType}`,
|
|
973
|
+
timestamp,
|
|
974
|
+
metadata: {
|
|
975
|
+
source: 'openhands',
|
|
976
|
+
raw
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function tryParseJsonText(value) {
|
|
982
|
+
if (typeof value !== 'string') {
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
const trimmed = value.trim();
|
|
986
|
+
if (!trimmed) {
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
if (!(trimmed.startsWith('{') || trimmed.startsWith('['))) {
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
try {
|
|
993
|
+
return JSON.parse(trimmed);
|
|
994
|
+
} catch (_error) {
|
|
995
|
+
return null;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function stringifyCompact(value) {
|
|
1000
|
+
if (value == null) {
|
|
1001
|
+
return '';
|
|
1002
|
+
}
|
|
1003
|
+
if (typeof value === 'string') {
|
|
1004
|
+
return value;
|
|
1005
|
+
}
|
|
1006
|
+
try {
|
|
1007
|
+
return JSON.stringify(value);
|
|
1008
|
+
} catch (_error) {
|
|
1009
|
+
return `${value}`;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function extractOpenHandsCommandCandidates(raw = {}) {
|
|
1014
|
+
const command = pickFirstString([
|
|
1015
|
+
raw.command,
|
|
1016
|
+
raw.cmd,
|
|
1017
|
+
raw.input && raw.input.command,
|
|
1018
|
+
raw.action && raw.action.command,
|
|
1019
|
+
raw.observation && raw.observation.command,
|
|
1020
|
+
raw.tool_input && raw.tool_input.command
|
|
1021
|
+
]);
|
|
1022
|
+
|
|
1023
|
+
const toolName = pickFirstString([
|
|
1024
|
+
raw.tool_name,
|
|
1025
|
+
raw.toolName,
|
|
1026
|
+
raw.tool && raw.tool.name,
|
|
1027
|
+
raw.name
|
|
1028
|
+
]);
|
|
1029
|
+
const toolArgs = raw.arguments || raw.tool_input || raw.input || null;
|
|
1030
|
+
|
|
1031
|
+
const cmd = command || (toolName
|
|
1032
|
+
? `tool:${toolName}${toolArgs ? ` ${stringifyCompact(toolArgs)}` : ''}`
|
|
1033
|
+
: '');
|
|
1034
|
+
|
|
1035
|
+
return {
|
|
1036
|
+
cmd,
|
|
1037
|
+
exit_code: pickFirstNumber([
|
|
1038
|
+
raw.exit_code,
|
|
1039
|
+
raw.exitCode,
|
|
1040
|
+
raw.result && (raw.result.exit_code ?? raw.result.exitCode)
|
|
1041
|
+
]),
|
|
1042
|
+
stdout: pickFirstString([
|
|
1043
|
+
raw.stdout,
|
|
1044
|
+
raw.output,
|
|
1045
|
+
raw.result && raw.result.stdout,
|
|
1046
|
+
raw.observation && raw.observation.stdout
|
|
1047
|
+
]),
|
|
1048
|
+
stderr: pickFirstString([
|
|
1049
|
+
raw.stderr,
|
|
1050
|
+
raw.result && raw.result.stderr,
|
|
1051
|
+
raw.error && raw.error.message,
|
|
1052
|
+
typeof raw.error === 'string' ? raw.error : ''
|
|
1053
|
+
]),
|
|
1054
|
+
log_path: pickFirstString([
|
|
1055
|
+
raw.log_path,
|
|
1056
|
+
raw.logPath,
|
|
1057
|
+
raw.log && raw.log.path
|
|
1058
|
+
])
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function collectOpenHandsFileChangesFromRaw(raw = {}) {
|
|
1063
|
+
const changes = [];
|
|
1064
|
+
const addPath = (pathValue, lineValue, diffRefValue = '') => {
|
|
1065
|
+
const pathRef = normalizeString(pathValue);
|
|
1066
|
+
if (!pathRef) {
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
const line = Number.parseInt(`${lineValue != null ? lineValue : 1}`, 10);
|
|
1070
|
+
const normalizedLine = Number.isFinite(line) && line > 0 ? line : 1;
|
|
1071
|
+
const diffRef = normalizeString(diffRefValue) || `${pathRef}:${normalizedLine}`;
|
|
1072
|
+
changes.push({
|
|
1073
|
+
path: pathRef,
|
|
1074
|
+
line: normalizedLine,
|
|
1075
|
+
diffRef
|
|
1076
|
+
});
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
if (typeof raw.path === 'string') {
|
|
1080
|
+
addPath(raw.path, raw.line || raw.line_number, raw.diff_ref);
|
|
1081
|
+
}
|
|
1082
|
+
if (raw.file && typeof raw.file === 'object') {
|
|
1083
|
+
addPath(raw.file.path || raw.file.name, raw.file.line || raw.file.line_number, raw.file.diffRef || raw.file.diff_ref);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const arrayKeys = ['files', 'changed_files', 'modified_files', 'created_files', 'deleted_files'];
|
|
1087
|
+
for (const key of arrayKeys) {
|
|
1088
|
+
const values = Array.isArray(raw[key]) ? raw[key] : [];
|
|
1089
|
+
for (const value of values) {
|
|
1090
|
+
if (typeof value === 'string') {
|
|
1091
|
+
addPath(value, 1);
|
|
1092
|
+
} else if (value && typeof value === 'object') {
|
|
1093
|
+
addPath(value.path || value.file || value.name, value.line || value.line_number, value.diffRef || value.diff_ref);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
const patchText = pickFirstString([raw.patch, raw.diff]);
|
|
1099
|
+
if (patchText) {
|
|
1100
|
+
const parsedJson = tryParseJsonText(patchText);
|
|
1101
|
+
if (parsedJson && typeof parsedJson === 'object') {
|
|
1102
|
+
const nested = collectOpenHandsFileChangesFromRaw(parsedJson);
|
|
1103
|
+
changes.push(...nested);
|
|
1104
|
+
} else {
|
|
1105
|
+
const lines = patchText.split(/\r?\n/);
|
|
1106
|
+
for (const line of lines) {
|
|
1107
|
+
const match = line.match(/^\+\+\+\s+b\/(.+)$/);
|
|
1108
|
+
if (match && match[1]) {
|
|
1109
|
+
addPath(match[1], 1);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
return changes;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function mapOpenHandsEventsToTaskSignals(openhandsEvents = [], context = {}) {
|
|
1119
|
+
const normalizedEvents = openhandsEvents.map((item, index) =>
|
|
1120
|
+
normalizeOpenHandsEventRecord(item, index, context.jobId)
|
|
1121
|
+
);
|
|
1122
|
+
|
|
1123
|
+
const commands = [];
|
|
1124
|
+
const fileChanges = [];
|
|
1125
|
+
const errors = [];
|
|
1126
|
+
const evidence = [];
|
|
1127
|
+
|
|
1128
|
+
for (const event of normalizedEvents) {
|
|
1129
|
+
const raw = event && event.metadata ? event.metadata.raw || {} : {};
|
|
1130
|
+
const commandCandidate = extractOpenHandsCommandCandidates(raw);
|
|
1131
|
+
if (commandCandidate.cmd || commandCandidate.stdout || commandCandidate.stderr || commandCandidate.exit_code != null) {
|
|
1132
|
+
commands.push(commandCandidate);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
fileChanges.push(...collectOpenHandsFileChangesFromRaw(raw));
|
|
1136
|
+
|
|
1137
|
+
const eventType = normalizeString(event.event_type).toLowerCase();
|
|
1138
|
+
const failedByType = eventType.includes('error') || eventType.includes('fail');
|
|
1139
|
+
const failedByExit = commandCandidate.exit_code != null && Number(commandCandidate.exit_code) !== 0;
|
|
1140
|
+
const failedByStderr = commandCandidate.stderr.length > 0 && !commandCandidate.stdout;
|
|
1141
|
+
if (failedByType || failedByExit || failedByStderr) {
|
|
1142
|
+
const message = pickFirstString([
|
|
1143
|
+
raw.message,
|
|
1144
|
+
raw.error && raw.error.message,
|
|
1145
|
+
typeof raw.error === 'string' ? raw.error : '',
|
|
1146
|
+
failedByExit ? `OpenHands command failed (exit ${commandCandidate.exit_code})` : '',
|
|
1147
|
+
'OpenHands event indicates failure'
|
|
1148
|
+
]);
|
|
1149
|
+
errors.push({
|
|
1150
|
+
message,
|
|
1151
|
+
step_id: event.event_type,
|
|
1152
|
+
cmd: commandCandidate.cmd,
|
|
1153
|
+
exit_code: commandCandidate.exit_code,
|
|
1154
|
+
stderr: commandCandidate.stderr,
|
|
1155
|
+
stdout: commandCandidate.stdout
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
evidence.push({
|
|
1160
|
+
type: 'openhands-event',
|
|
1161
|
+
ref: event.event_id,
|
|
1162
|
+
detail: event.event_type
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
return {
|
|
1167
|
+
event: normalizedEvents,
|
|
1168
|
+
commands: normalizeTaskCommands(commands),
|
|
1169
|
+
file_changes: normalizeTaskFileChanges(fileChanges),
|
|
1170
|
+
errors: normalizeTaskErrors(errors),
|
|
1171
|
+
evidence: normalizeTaskEvidence(evidence)
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function extractCommandsFromStageMetadata(stageMetadata = {}) {
|
|
1176
|
+
const gateSteps = Array.isArray(stageMetadata && stageMetadata.gate_steps)
|
|
1177
|
+
? stageMetadata.gate_steps
|
|
1178
|
+
: [];
|
|
1179
|
+
return gateSteps.map((step) => ({
|
|
1180
|
+
cmd: normalizeString(step && step.command) || normalizeString(step && step.id) || 'n/a',
|
|
1181
|
+
exit_code: Number.isFinite(Number(step && step.exit_code))
|
|
1182
|
+
? Number(step.exit_code)
|
|
1183
|
+
: null,
|
|
1184
|
+
stdout: normalizeString(step?.output?.stdout),
|
|
1185
|
+
stderr: normalizeString(step?.output?.stderr),
|
|
1186
|
+
log_path: normalizeString(stageMetadata.report) || null
|
|
1187
|
+
}));
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function extractErrorsFromStageMetadata(stageState = {}, stageMetadata = {}) {
|
|
1191
|
+
const directErrors = Array.isArray(stageMetadata && stageMetadata.errors)
|
|
1192
|
+
? stageMetadata.errors
|
|
1193
|
+
: [];
|
|
1194
|
+
const gateStepErrors = [];
|
|
1195
|
+
const gateSteps = Array.isArray(stageMetadata && stageMetadata.gate_steps)
|
|
1196
|
+
? stageMetadata.gate_steps
|
|
1197
|
+
: [];
|
|
1198
|
+
|
|
1199
|
+
for (const step of gateSteps) {
|
|
1200
|
+
const status = normalizeString(step && step.status);
|
|
1201
|
+
if (status !== 'failed') {
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
const stepId = normalizeString(step && step.id) || normalizeString(step && step.name) || 'unknown-step';
|
|
1205
|
+
const stepMessage = `Gate step failed: ${stepId}${step && step.exit_code != null ? ` (exit ${step.exit_code})` : ''}`;
|
|
1206
|
+
gateStepErrors.push({
|
|
1207
|
+
message: stepMessage,
|
|
1208
|
+
step_id: stepId,
|
|
1209
|
+
cmd: normalizeString(step && step.command),
|
|
1210
|
+
exit_code: step && step.exit_code != null ? step.exit_code : null,
|
|
1211
|
+
skip_reason: normalizeString(step && step.skip_reason),
|
|
1212
|
+
stdout: normalizeString(step?.output?.stdout),
|
|
1213
|
+
stderr: normalizeString(step?.output?.stderr)
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
if (normalizeString(stageState && stageState.status) === 'blocked') {
|
|
1218
|
+
const blockers = Array.isArray(stageMetadata?.problem_evaluation?.blockers)
|
|
1219
|
+
? stageMetadata.problem_evaluation.blockers
|
|
1220
|
+
: [];
|
|
1221
|
+
if (blockers.length > 0) {
|
|
1222
|
+
gateStepErrors.push({
|
|
1223
|
+
message: `Problem evaluation blocked stage: ${blockers.join(', ')}`,
|
|
1224
|
+
cmd: 'problem-evaluation-policy'
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
return [...directErrors, ...gateStepErrors];
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function collectTaskFileChanges(job = {}, stageName = '', stageMetadata = {}) {
|
|
1233
|
+
const fileChanges = [];
|
|
1234
|
+
if (Array.isArray(stageMetadata && stageMetadata.file_changes)) {
|
|
1235
|
+
fileChanges.push(...stageMetadata.file_changes);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const createdSpec = job?.source?.intake?.created_spec;
|
|
1239
|
+
const createdSpecId = createdSpec && createdSpec.created
|
|
1240
|
+
? normalizeString(createdSpec.spec_id)
|
|
1241
|
+
: '';
|
|
1242
|
+
if (stageName === 'plan' && createdSpecId) {
|
|
1243
|
+
fileChanges.push(
|
|
1244
|
+
{ path: `.sce/specs/${createdSpecId}/requirements.md`, line: 1 },
|
|
1245
|
+
{ path: `.sce/specs/${createdSpecId}/design.md`, line: 1 },
|
|
1246
|
+
{ path: `.sce/specs/${createdSpecId}/tasks.md`, line: 1 },
|
|
1247
|
+
{ path: `.sce/specs/${createdSpecId}/custom/problem-domain-chain.json`, line: 1 },
|
|
1248
|
+
{ path: `.sce/specs/${createdSpecId}/custom/problem-contract.json`, line: 1 }
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
const artifacts = job && job.artifacts ? job.artifacts : {};
|
|
1253
|
+
if (stageName === 'plan') {
|
|
1254
|
+
if (normalizeString(artifacts.spec_portfolio_report)) {
|
|
1255
|
+
fileChanges.push({ path: artifacts.spec_portfolio_report, line: 1 });
|
|
1256
|
+
}
|
|
1257
|
+
if (normalizeString(artifacts.spec_scene_index)) {
|
|
1258
|
+
fileChanges.push({ path: artifacts.spec_scene_index, line: 1 });
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
if (stageName === 'generate' && normalizeString(artifacts.generate_report)) {
|
|
1262
|
+
fileChanges.push({ path: artifacts.generate_report, line: 1 });
|
|
1263
|
+
}
|
|
1264
|
+
if (stageName === 'verify' && normalizeString(artifacts.verify_report)) {
|
|
1265
|
+
fileChanges.push({ path: artifacts.verify_report, line: 1 });
|
|
1266
|
+
}
|
|
1267
|
+
if (stageName === 'release' && normalizeString(artifacts.release_report)) {
|
|
1268
|
+
fileChanges.push({ path: artifacts.release_report, line: 1 });
|
|
1269
|
+
}
|
|
1270
|
+
return normalizeTaskFileChanges(fileChanges);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function collectTaskEvidence(job = {}, stageName = '', stageMetadata = {}) {
|
|
1274
|
+
const evidence = [];
|
|
1275
|
+
const stageReport = normalizeString(stageMetadata && stageMetadata.report);
|
|
1276
|
+
if (stageReport) {
|
|
1277
|
+
evidence.push({ type: 'stage-report', ref: stageReport, detail: stageName });
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
const artifacts = job && job.artifacts ? job.artifacts : {};
|
|
1281
|
+
if (stageName && artifacts.problem_eval_reports && artifacts.problem_eval_reports[stageName]) {
|
|
1282
|
+
evidence.push({
|
|
1283
|
+
type: 'problem-evaluation-report',
|
|
1284
|
+
ref: artifacts.problem_eval_reports[stageName],
|
|
1285
|
+
detail: stageName
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
if (normalizeString(job?.source?.domain_chain?.chain_path)) {
|
|
1290
|
+
evidence.push({
|
|
1291
|
+
type: 'domain-chain',
|
|
1292
|
+
ref: job.source.domain_chain.chain_path,
|
|
1293
|
+
detail: stageName
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
if (normalizeString(job?.source?.problem_contract_path)) {
|
|
1297
|
+
evidence.push({
|
|
1298
|
+
type: 'problem-contract',
|
|
1299
|
+
ref: job.source.problem_contract_path,
|
|
1300
|
+
detail: stageName
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
if (Array.isArray(stageMetadata && stageMetadata.auto_errorbook_records)) {
|
|
1304
|
+
for (const item of stageMetadata.auto_errorbook_records) {
|
|
1305
|
+
const entryId = normalizeString(item && item.entry_id);
|
|
1306
|
+
if (!entryId) {
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
evidence.push({
|
|
1310
|
+
type: 'errorbook-entry',
|
|
1311
|
+
ref: entryId,
|
|
1312
|
+
detail: normalizeString(item && item.step_id) || null
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
if (normalizeString(job && job.job_id)) {
|
|
1318
|
+
evidence.push({
|
|
1319
|
+
type: 'event-log',
|
|
1320
|
+
ref: '.sce/state/sce-state.sqlite',
|
|
1321
|
+
detail: `studio_event_stream:job_id=${job.job_id}`
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
return normalizeTaskEvidence(evidence);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function buildTaskSummaryLines(job = {}, stageName = '', taskStatus = '', nextAction = '', taskRef = '') {
|
|
1329
|
+
const sceneId = normalizeString(job?.scene?.id) || 'scene.n/a';
|
|
1330
|
+
const specId = normalizeString(job?.scene?.spec_id) || normalizeString(job?.source?.spec_id) || 'spec.n/a';
|
|
1331
|
+
const progress = buildProgress(job);
|
|
1332
|
+
return [
|
|
1333
|
+
`Stage: ${stageName || 'plan'} | Status: ${taskStatus || 'unknown'}${taskRef ? ` | Ref: ${taskRef}` : ''}`,
|
|
1334
|
+
`Scene: ${sceneId} | Spec: ${specId} | Progress: ${progress.completed}/${progress.total}`,
|
|
1335
|
+
`Next: ${nextAction || 'n/a'}`
|
|
1336
|
+
];
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function buildTaskEnvelope(mode, job, options = {}) {
|
|
1340
|
+
const stageName = resolveTaskStage(mode, job, options.stageName);
|
|
1341
|
+
const stageState = stageName && job && job.stages && job.stages[stageName]
|
|
1342
|
+
? job.stages[stageName]
|
|
1343
|
+
: {};
|
|
1344
|
+
const stageMetadata = stageState && typeof stageState.metadata === 'object' && stageState.metadata
|
|
1345
|
+
? stageState.metadata
|
|
1346
|
+
: {};
|
|
1347
|
+
const nextAction = resolveNextAction(job);
|
|
1348
|
+
|
|
1349
|
+
const events = Array.isArray(options.events)
|
|
1350
|
+
? options.events
|
|
1351
|
+
: (options.event ? [options.event] : []);
|
|
1352
|
+
const latestEvent = events.length > 0 ? events[events.length - 1] : null;
|
|
1353
|
+
|
|
1354
|
+
const taskStatus = normalizeString(stageState && stageState.status)
|
|
1355
|
+
|| (stageName === 'rollback' && normalizeString(job && job.status) === 'rolled_back'
|
|
1356
|
+
? 'completed'
|
|
1357
|
+
: normalizeString(job && job.status) || 'unknown');
|
|
1358
|
+
const taskId = normalizeString(options.taskId)
|
|
1359
|
+
|| (normalizeString(job && job.job_id)
|
|
1360
|
+
? `${job.job_id}:${stageName || 'task'}`
|
|
1361
|
+
: null);
|
|
1362
|
+
const goal = normalizeString(job?.source?.goal)
|
|
1363
|
+
|| `Studio ${stageName || 'task'} execution`;
|
|
1364
|
+
const sessionId = normalizeString(job?.session?.scene_session_id) || null;
|
|
1365
|
+
const sceneId = normalizeString(job?.scene?.id) || null;
|
|
1366
|
+
const specId = normalizeString(job?.scene?.spec_id) || normalizeString(job?.source?.spec_id) || null;
|
|
1367
|
+
const taskRef = normalizeString(options.taskRef) || null;
|
|
1368
|
+
|
|
1369
|
+
const commands = normalizeTaskCommands([
|
|
1370
|
+
...(Array.isArray(stageMetadata.commands) ? stageMetadata.commands : []),
|
|
1371
|
+
...extractCommandsFromStageMetadata(stageMetadata)
|
|
1372
|
+
]);
|
|
1373
|
+
const errors = normalizeTaskErrors(
|
|
1374
|
+
extractErrorsFromStageMetadata(stageState, stageMetadata)
|
|
1375
|
+
);
|
|
1376
|
+
const fileChanges = collectTaskFileChanges(job, stageName, stageMetadata);
|
|
1377
|
+
const evidence = collectTaskEvidence(job, stageName, stageMetadata);
|
|
1378
|
+
|
|
1379
|
+
const handoff = stageMetadata.handoff && typeof stageMetadata.handoff === 'object'
|
|
1380
|
+
? stageMetadata.handoff
|
|
1381
|
+
: {
|
|
1382
|
+
stage: stageName,
|
|
1383
|
+
status: taskStatus,
|
|
1384
|
+
completed_at: normalizeString(stageState && stageState.completed_at) || null,
|
|
1385
|
+
report: normalizeString(stageMetadata.report) || null,
|
|
1386
|
+
release_ref: normalizeString(stageMetadata.release_ref) || normalizeString(job?.artifacts?.release_ref) || null
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1389
|
+
const normalizedHandoff = {
|
|
1390
|
+
...handoff,
|
|
1391
|
+
task_ref: taskRef
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
return {
|
|
1395
|
+
sessionId,
|
|
1396
|
+
sceneId,
|
|
1397
|
+
specId,
|
|
1398
|
+
taskId,
|
|
1399
|
+
taskRef,
|
|
1400
|
+
eventId: normalizeString(latestEvent && latestEvent.event_id) || null,
|
|
1401
|
+
task: {
|
|
1402
|
+
ref: taskRef,
|
|
1403
|
+
goal,
|
|
1404
|
+
status: taskStatus,
|
|
1405
|
+
summary: buildTaskSummaryLines(job, stageName, taskStatus, nextAction, taskRef),
|
|
1406
|
+
handoff: normalizedHandoff,
|
|
1407
|
+
next_action: nextAction,
|
|
1408
|
+
file_changes: fileChanges,
|
|
1409
|
+
commands,
|
|
1410
|
+
errors,
|
|
1411
|
+
evidence
|
|
1412
|
+
},
|
|
1413
|
+
event: events
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
|
|
733
1417
|
function toRelativePosix(projectPath, absolutePath) {
|
|
734
1418
|
return path.relative(projectPath, absolutePath).replace(/\\/g, '/');
|
|
735
1419
|
}
|
|
@@ -1170,8 +1854,50 @@ function ensureNotRolledBack(job, stageName) {
|
|
|
1170
1854
|
}
|
|
1171
1855
|
}
|
|
1172
1856
|
|
|
1173
|
-
function
|
|
1174
|
-
|
|
1857
|
+
function buildStudioTaskKey(stageName = '') {
|
|
1858
|
+
const normalizedStage = normalizeString(stageName) || 'task';
|
|
1859
|
+
return `studio:${normalizedStage}`;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
async function resolveTaskReference(mode, job, options = {}) {
|
|
1863
|
+
const explicitTaskRef = normalizeString(options.taskRef);
|
|
1864
|
+
if (explicitTaskRef) {
|
|
1865
|
+
return explicitTaskRef;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
const sceneId = normalizeString(job?.scene?.id);
|
|
1869
|
+
const specId = normalizeString(job?.scene?.spec_id) || normalizeString(job?.source?.spec_id);
|
|
1870
|
+
if (!sceneId || !specId) {
|
|
1871
|
+
return null;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
const stageName = resolveTaskStage(mode, job, options.stageName);
|
|
1875
|
+
const taskKey = normalizeString(options.taskKey) || buildStudioTaskKey(stageName);
|
|
1876
|
+
const projectPath = normalizeString(options.projectPath) || process.cwd();
|
|
1877
|
+
const fileSystem = options.fileSystem || fs;
|
|
1878
|
+
const taskRefRegistry = options.taskRefRegistry || new TaskRefRegistry(projectPath, { fileSystem });
|
|
1879
|
+
|
|
1880
|
+
try {
|
|
1881
|
+
const taskRef = await taskRefRegistry.resolveOrCreateRef({
|
|
1882
|
+
sceneId,
|
|
1883
|
+
specId,
|
|
1884
|
+
taskKey,
|
|
1885
|
+
source: 'studio-stage',
|
|
1886
|
+
metadata: {
|
|
1887
|
+
mode: normalizeString(mode) || null,
|
|
1888
|
+
stage: stageName || null,
|
|
1889
|
+
job_id: normalizeString(job?.job_id) || null
|
|
1890
|
+
}
|
|
1891
|
+
});
|
|
1892
|
+
return taskRef.task_ref;
|
|
1893
|
+
} catch (_error) {
|
|
1894
|
+
return null;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
async function buildCommandPayload(mode, job, options = {}) {
|
|
1899
|
+
const taskRef = await resolveTaskReference(mode, job, options);
|
|
1900
|
+
const base = {
|
|
1175
1901
|
mode,
|
|
1176
1902
|
success: true,
|
|
1177
1903
|
job_id: job.job_id,
|
|
@@ -1180,6 +1906,13 @@ function buildCommandPayload(mode, job) {
|
|
|
1180
1906
|
next_action: resolveNextAction(job),
|
|
1181
1907
|
artifacts: { ...job.artifacts }
|
|
1182
1908
|
};
|
|
1909
|
+
return {
|
|
1910
|
+
...base,
|
|
1911
|
+
...buildTaskEnvelope(mode, job, {
|
|
1912
|
+
...options,
|
|
1913
|
+
taskRef
|
|
1914
|
+
})
|
|
1915
|
+
};
|
|
1183
1916
|
}
|
|
1184
1917
|
|
|
1185
1918
|
function buildJobDomainChainMetadata(job = {}) {
|
|
@@ -1645,7 +2378,7 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
|
|
|
1645
2378
|
};
|
|
1646
2379
|
|
|
1647
2380
|
await saveJob(paths, job, fileSystem);
|
|
1648
|
-
await appendStudioEvent(paths, job, 'stage.plan.completed', {
|
|
2381
|
+
const planEvent = await appendStudioEvent(paths, job, 'stage.plan.completed', {
|
|
1649
2382
|
from_chat: fromChat,
|
|
1650
2383
|
scene_id: sceneId,
|
|
1651
2384
|
spec_id: effectiveSpecId,
|
|
@@ -1671,7 +2404,12 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
|
|
|
1671
2404
|
}, fileSystem);
|
|
1672
2405
|
await writeLatestJob(paths, jobId, fileSystem);
|
|
1673
2406
|
|
|
1674
|
-
const payload = buildCommandPayload('studio-plan', job
|
|
2407
|
+
const payload = await buildCommandPayload('studio-plan', job, {
|
|
2408
|
+
stageName: 'plan',
|
|
2409
|
+
event: planEvent,
|
|
2410
|
+
projectPath,
|
|
2411
|
+
fileSystem
|
|
2412
|
+
});
|
|
1675
2413
|
payload.scene = {
|
|
1676
2414
|
id: sceneId,
|
|
1677
2415
|
spec_id: effectiveSpecId
|
|
@@ -1756,7 +2494,7 @@ async function runStudioGenerateCommand(options = {}, dependencies = {}) {
|
|
|
1756
2494
|
});
|
|
1757
2495
|
|
|
1758
2496
|
await saveJob(paths, job, fileSystem);
|
|
1759
|
-
await appendStudioEvent(paths, job, 'stage.generate.completed', {
|
|
2497
|
+
const generateEvent = await appendStudioEvent(paths, job, 'stage.generate.completed', {
|
|
1760
2498
|
scene_id: sceneId,
|
|
1761
2499
|
target: job.target,
|
|
1762
2500
|
patch_bundle_id: patchBundleId,
|
|
@@ -1766,7 +2504,12 @@ async function runStudioGenerateCommand(options = {}, dependencies = {}) {
|
|
|
1766
2504
|
}, fileSystem);
|
|
1767
2505
|
await writeLatestJob(paths, jobId, fileSystem);
|
|
1768
2506
|
|
|
1769
|
-
const payload = buildCommandPayload('studio-generate', job
|
|
2507
|
+
const payload = await buildCommandPayload('studio-generate', job, {
|
|
2508
|
+
stageName: 'generate',
|
|
2509
|
+
event: generateEvent,
|
|
2510
|
+
projectPath,
|
|
2511
|
+
fileSystem
|
|
2512
|
+
});
|
|
1770
2513
|
printStudioPayload(payload, options);
|
|
1771
2514
|
return payload;
|
|
1772
2515
|
}
|
|
@@ -1826,14 +2569,19 @@ async function runStudioApplyCommand(options = {}, dependencies = {}) {
|
|
|
1826
2569
|
});
|
|
1827
2570
|
|
|
1828
2571
|
await saveJob(paths, job, fileSystem);
|
|
1829
|
-
await appendStudioEvent(paths, job, 'stage.apply.completed', {
|
|
2572
|
+
const applyEvent = await appendStudioEvent(paths, job, 'stage.apply.completed', {
|
|
1830
2573
|
patch_bundle_id: patchBundleId,
|
|
1831
2574
|
auth_required: authResult.required,
|
|
1832
2575
|
problem_evaluation: summarizeProblemEvaluation(applyProblemEvaluation)
|
|
1833
2576
|
}, fileSystem);
|
|
1834
2577
|
await writeLatestJob(paths, jobId, fileSystem);
|
|
1835
2578
|
|
|
1836
|
-
const payload = buildCommandPayload('studio-apply', job
|
|
2579
|
+
const payload = await buildCommandPayload('studio-apply', job, {
|
|
2580
|
+
stageName: 'apply',
|
|
2581
|
+
event: applyEvent,
|
|
2582
|
+
projectPath,
|
|
2583
|
+
fileSystem
|
|
2584
|
+
});
|
|
1837
2585
|
printStudioPayload(payload, options);
|
|
1838
2586
|
return payload;
|
|
1839
2587
|
}
|
|
@@ -1940,6 +2688,7 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
|
|
|
1940
2688
|
profile,
|
|
1941
2689
|
passed: false,
|
|
1942
2690
|
report: verifyReportPath,
|
|
2691
|
+
gate_steps: gateResult.steps,
|
|
1943
2692
|
problem_evaluation: summarizeProblemEvaluation(verifyProblemEvaluation),
|
|
1944
2693
|
domain_chain: domainChainMetadata,
|
|
1945
2694
|
auto_errorbook_records: autoErrorbookRecords
|
|
@@ -1962,13 +2711,14 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
|
|
|
1962
2711
|
profile,
|
|
1963
2712
|
passed: true,
|
|
1964
2713
|
report: verifyReportPath,
|
|
2714
|
+
gate_steps: gateResult.steps,
|
|
1965
2715
|
problem_evaluation: summarizeProblemEvaluation(verifyProblemEvaluation),
|
|
1966
2716
|
domain_chain: domainChainMetadata,
|
|
1967
2717
|
auto_errorbook_records: autoErrorbookRecords
|
|
1968
2718
|
});
|
|
1969
2719
|
|
|
1970
2720
|
await saveJob(paths, job, fileSystem);
|
|
1971
|
-
await appendStudioEvent(paths, job, 'stage.verify.completed', {
|
|
2721
|
+
const verifyEvent = await appendStudioEvent(paths, job, 'stage.verify.completed', {
|
|
1972
2722
|
profile,
|
|
1973
2723
|
passed: true,
|
|
1974
2724
|
report: verifyReportPath,
|
|
@@ -1978,7 +2728,12 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
|
|
|
1978
2728
|
}, fileSystem);
|
|
1979
2729
|
await writeLatestJob(paths, jobId, fileSystem);
|
|
1980
2730
|
|
|
1981
|
-
const payload = buildCommandPayload('studio-verify', job
|
|
2731
|
+
const payload = await buildCommandPayload('studio-verify', job, {
|
|
2732
|
+
stageName: 'verify',
|
|
2733
|
+
event: verifyEvent,
|
|
2734
|
+
projectPath,
|
|
2735
|
+
fileSystem
|
|
2736
|
+
});
|
|
1982
2737
|
printStudioPayload(payload, options);
|
|
1983
2738
|
return payload;
|
|
1984
2739
|
}
|
|
@@ -2118,6 +2873,7 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
|
|
|
2118
2873
|
release_ref: releaseRef,
|
|
2119
2874
|
passed: false,
|
|
2120
2875
|
report: releaseReportPath,
|
|
2876
|
+
gate_steps: gateResult.steps,
|
|
2121
2877
|
auth_required: authResult.required,
|
|
2122
2878
|
problem_evaluation: summarizeProblemEvaluation(releaseProblemEvaluation),
|
|
2123
2879
|
domain_chain: domainChainMetadata,
|
|
@@ -2143,6 +2899,7 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
|
|
|
2143
2899
|
channel,
|
|
2144
2900
|
release_ref: releaseRef,
|
|
2145
2901
|
report: releaseReportPath,
|
|
2902
|
+
gate_steps: gateResult.steps,
|
|
2146
2903
|
auth_required: authResult.required,
|
|
2147
2904
|
problem_evaluation: summarizeProblemEvaluation(releaseProblemEvaluation),
|
|
2148
2905
|
domain_chain: domainChainMetadata,
|
|
@@ -2171,7 +2928,7 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
|
|
|
2171
2928
|
}
|
|
2172
2929
|
|
|
2173
2930
|
await saveJob(paths, job, fileSystem);
|
|
2174
|
-
await appendStudioEvent(paths, job, 'stage.release.completed', {
|
|
2931
|
+
const releaseEvent = await appendStudioEvent(paths, job, 'stage.release.completed', {
|
|
2175
2932
|
channel,
|
|
2176
2933
|
release_ref: releaseRef,
|
|
2177
2934
|
report: releaseReportPath,
|
|
@@ -2182,7 +2939,12 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
|
|
|
2182
2939
|
}, fileSystem);
|
|
2183
2940
|
await writeLatestJob(paths, jobId, fileSystem);
|
|
2184
2941
|
|
|
2185
|
-
const payload = buildCommandPayload('studio-release', job
|
|
2942
|
+
const payload = await buildCommandPayload('studio-release', job, {
|
|
2943
|
+
stageName: 'release',
|
|
2944
|
+
event: releaseEvent,
|
|
2945
|
+
projectPath,
|
|
2946
|
+
fileSystem
|
|
2947
|
+
});
|
|
2186
2948
|
printStudioPayload(payload, options);
|
|
2187
2949
|
return payload;
|
|
2188
2950
|
}
|
|
@@ -2200,7 +2962,12 @@ async function runStudioResumeCommand(options = {}, dependencies = {}) {
|
|
|
2200
2962
|
}
|
|
2201
2963
|
|
|
2202
2964
|
const job = await loadJob(paths, jobId, fileSystem);
|
|
2203
|
-
const
|
|
2965
|
+
const events = await readStudioEvents(paths, jobId, { limit: 20 }, fileSystem);
|
|
2966
|
+
const payload = await buildCommandPayload('studio-resume', job, {
|
|
2967
|
+
events,
|
|
2968
|
+
projectPath,
|
|
2969
|
+
fileSystem
|
|
2970
|
+
});
|
|
2204
2971
|
payload.success = true;
|
|
2205
2972
|
printStudioPayload(payload, options);
|
|
2206
2973
|
return payload;
|
|
@@ -2252,12 +3019,17 @@ async function runStudioRollbackCommand(options = {}, dependencies = {}) {
|
|
|
2252
3019
|
}
|
|
2253
3020
|
|
|
2254
3021
|
await saveJob(paths, job, fileSystem);
|
|
2255
|
-
await appendStudioEvent(paths, job, 'job.rolled_back', {
|
|
3022
|
+
const rollbackEvent = await appendStudioEvent(paths, job, 'job.rolled_back', {
|
|
2256
3023
|
reason
|
|
2257
3024
|
}, fileSystem);
|
|
2258
3025
|
await writeLatestJob(paths, jobId, fileSystem);
|
|
2259
3026
|
|
|
2260
|
-
const payload = buildCommandPayload('studio-rollback', job
|
|
3027
|
+
const payload = await buildCommandPayload('studio-rollback', job, {
|
|
3028
|
+
stageName: 'rollback',
|
|
3029
|
+
event: rollbackEvent,
|
|
3030
|
+
projectPath,
|
|
3031
|
+
fileSystem
|
|
3032
|
+
});
|
|
2261
3033
|
payload.rollback = { ...job.rollback };
|
|
2262
3034
|
printStudioPayload(payload, options);
|
|
2263
3035
|
return payload;
|
|
@@ -2297,15 +3069,66 @@ async function runStudioEventsCommand(options = {}, dependencies = {}) {
|
|
|
2297
3069
|
}
|
|
2298
3070
|
|
|
2299
3071
|
const limit = normalizePositiveInteger(options.limit, 50);
|
|
2300
|
-
const
|
|
3072
|
+
const job = await loadJob(paths, jobId, fileSystem);
|
|
3073
|
+
const openhandsEventsPath = normalizeString(options.openhandsEvents);
|
|
3074
|
+
let sourceStream = 'studio';
|
|
3075
|
+
let events = await readStudioEvents(paths, jobId, { limit }, fileSystem);
|
|
3076
|
+
let openhandsSignals = null;
|
|
3077
|
+
if (openhandsEventsPath) {
|
|
3078
|
+
const absoluteOpenhandsPath = path.isAbsolute(openhandsEventsPath)
|
|
3079
|
+
? openhandsEventsPath
|
|
3080
|
+
: path.join(projectPath, openhandsEventsPath);
|
|
3081
|
+
const rawOpenhandsEvents = await readOpenHandsEventsFile(absoluteOpenhandsPath, fileSystem);
|
|
3082
|
+
openhandsSignals = mapOpenHandsEventsToTaskSignals(rawOpenhandsEvents, {
|
|
3083
|
+
jobId: job.job_id
|
|
3084
|
+
});
|
|
3085
|
+
events = openhandsSignals.event;
|
|
3086
|
+
sourceStream = 'openhands';
|
|
3087
|
+
}
|
|
2301
3088
|
|
|
2302
|
-
const payload = {
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
3089
|
+
const payload = await buildCommandPayload('studio-events', job, {
|
|
3090
|
+
events,
|
|
3091
|
+
projectPath,
|
|
3092
|
+
fileSystem
|
|
3093
|
+
});
|
|
3094
|
+
payload.limit = limit;
|
|
3095
|
+
payload.source_stream = sourceStream;
|
|
3096
|
+
if (sourceStream === 'openhands') {
|
|
3097
|
+
payload.openhands_events_file = path.relative(projectPath, path.isAbsolute(openhandsEventsPath)
|
|
3098
|
+
? openhandsEventsPath
|
|
3099
|
+
: path.join(projectPath, openhandsEventsPath)).replace(/\\/g, '/');
|
|
3100
|
+
payload.task = {
|
|
3101
|
+
...payload.task,
|
|
3102
|
+
summary: [
|
|
3103
|
+
`OpenHands events: ${events.length} | commands: ${openhandsSignals.commands.length} | errors: ${openhandsSignals.errors.length}`,
|
|
3104
|
+
`File changes: ${openhandsSignals.file_changes.length} | source: ${payload.openhands_events_file}`,
|
|
3105
|
+
`Next: ${payload.task.next_action}`
|
|
3106
|
+
],
|
|
3107
|
+
handoff: {
|
|
3108
|
+
...(payload.task.handoff || {}),
|
|
3109
|
+
source_stream: 'openhands',
|
|
3110
|
+
openhands_event_count: events.length,
|
|
3111
|
+
openhands_command_count: openhandsSignals.commands.length,
|
|
3112
|
+
openhands_error_count: openhandsSignals.errors.length
|
|
3113
|
+
},
|
|
3114
|
+
commands: openhandsSignals.commands,
|
|
3115
|
+
file_changes: openhandsSignals.file_changes,
|
|
3116
|
+
errors: openhandsSignals.errors,
|
|
3117
|
+
evidence: normalizeTaskEvidence([
|
|
3118
|
+
...(Array.isArray(payload.task.evidence) ? payload.task.evidence : []),
|
|
3119
|
+
...openhandsSignals.evidence,
|
|
3120
|
+
{
|
|
3121
|
+
type: 'openhands-event-file',
|
|
3122
|
+
ref: payload.openhands_events_file,
|
|
3123
|
+
detail: 'mapped'
|
|
3124
|
+
}
|
|
3125
|
+
])
|
|
3126
|
+
};
|
|
3127
|
+
payload.eventId = events.length > 0
|
|
3128
|
+
? events[events.length - 1].event_id
|
|
3129
|
+
: null;
|
|
3130
|
+
}
|
|
3131
|
+
payload.events = events;
|
|
2309
3132
|
printStudioEventsPayload(payload, options);
|
|
2310
3133
|
return payload;
|
|
2311
3134
|
}
|
|
@@ -2591,6 +3414,7 @@ function registerStudioCommands(program) {
|
|
|
2591
3414
|
.description('Show studio job event stream')
|
|
2592
3415
|
.option('--job <job-id>', 'Studio job id (defaults to latest)')
|
|
2593
3416
|
.option('--limit <number>', 'Maximum number of recent events to return', '50')
|
|
3417
|
+
.option('--openhands-events <path>', 'Optional OpenHands raw events file (.json/.jsonl) mapped to task stream')
|
|
2594
3418
|
.option('--json', 'Print machine-readable JSON output')
|
|
2595
3419
|
.action(async (options) => runStudioCommand(runStudioEventsCommand, options, 'events'));
|
|
2596
3420
|
|