scene-capability-engine 3.3.18 → 3.3.21
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 +31 -0
- package/README.md +19 -10
- package/README.zh.md +21 -11
- package/docs/command-reference.md +51 -12
- package/docs/release-checklist.md +6 -0
- package/docs/zh/release-checklist.md +6 -0
- package/lib/commands/errorbook.js +169 -0
- package/lib/commands/spec-bootstrap.js +126 -51
- package/lib/commands/spec-gate.js +92 -25
- package/lib/commands/spec-pipeline.js +86 -7
- package/lib/commands/studio.js +265 -30
- package/lib/runtime/multi-spec-scene-session.js +147 -0
- package/lib/runtime/scene-session-binding.js +109 -0
- package/lib/runtime/session-store.js +475 -3
- package/package.json +4 -2
- package/template/.sce/steering/CORE_PRINCIPLES.md +26 -1
package/lib/commands/studio.js
CHANGED
|
@@ -3,6 +3,7 @@ const crypto = require('crypto');
|
|
|
3
3
|
const { spawnSync } = require('child_process');
|
|
4
4
|
const fs = require('fs-extra');
|
|
5
5
|
const chalk = require('chalk');
|
|
6
|
+
const { SessionStore } = require('../runtime/session-store');
|
|
6
7
|
|
|
7
8
|
const STUDIO_JOB_API_VERSION = 'sce.studio.job/v0.1';
|
|
8
9
|
const STAGE_ORDER = ['plan', 'generate', 'apply', 'verify', 'release'];
|
|
@@ -101,11 +102,90 @@ function normalizeGateStep(step) {
|
|
|
101
102
|
};
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
function createGateFailureFingerprint(failure = {}, context = {}) {
|
|
106
|
+
const basis = JSON.stringify({
|
|
107
|
+
stage: normalizeString(context.stage),
|
|
108
|
+
profile: normalizeString(context.profile),
|
|
109
|
+
job_id: normalizeString(context.job_id),
|
|
110
|
+
step_id: normalizeString(failure.id),
|
|
111
|
+
command: normalizeString(failure.command),
|
|
112
|
+
exit_code: Number.isFinite(Number(failure.exit_code)) ? Number(failure.exit_code) : null,
|
|
113
|
+
skip_reason: normalizeString(failure.skip_reason),
|
|
114
|
+
stderr: normalizeString(failure?.output?.stderr || '').slice(0, 400)
|
|
115
|
+
});
|
|
116
|
+
const digest = crypto.createHash('sha1').update(basis).digest('hex').slice(0, 16);
|
|
117
|
+
return `studio-gate-${digest}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function autoRecordGateFailure(failure = {}, context = {}, dependencies = {}) {
|
|
121
|
+
if (dependencies.autoRecordFailures === false) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const { runErrorbookRecordCommand } = require('./errorbook');
|
|
127
|
+
const stage = normalizeString(context.stage) || 'verify';
|
|
128
|
+
const profile = normalizeString(context.profile) || 'standard';
|
|
129
|
+
const jobId = normalizeString(context.job_id);
|
|
130
|
+
const stepId = normalizeString(failure.id) || normalizeString(failure.name) || 'unknown-step';
|
|
131
|
+
const commandText = normalizeString(failure.command) || 'n/a';
|
|
132
|
+
const skipReason = normalizeString(failure.skip_reason);
|
|
133
|
+
const stderr = normalizeString(failure?.output?.stderr || '');
|
|
134
|
+
const errorText = normalizeString(failure?.output?.error || '');
|
|
135
|
+
const symptom = skipReason
|
|
136
|
+
? `Required studio ${stage} gate step "${stepId}" is unavailable: ${skipReason}.`
|
|
137
|
+
: `Required studio ${stage} gate step "${stepId}" failed (exit=${failure.exit_code ?? 'n/a'}).`;
|
|
138
|
+
const rootCause = skipReason
|
|
139
|
+
? `Gate dependency missing or disabled for studio ${stage} profile ${profile}; remediation required before release.`
|
|
140
|
+
: `Gate command execution failure in studio ${stage} profile ${profile}; root cause analysis pending.`;
|
|
141
|
+
const fixActions = [
|
|
142
|
+
`Inspect failed gate step ${stepId} in studio ${stage} stage.`,
|
|
143
|
+
`Rerun gate command: ${commandText}`
|
|
144
|
+
];
|
|
145
|
+
if (stderr) {
|
|
146
|
+
fixActions.push(`Analyze stderr signal: ${stderr.slice(0, 200)}`);
|
|
147
|
+
} else if (errorText) {
|
|
148
|
+
fixActions.push(`Analyze runtime error signal: ${errorText.slice(0, 200)}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const title = `[studio:${stage}] gate failure: ${stepId}`;
|
|
152
|
+
const tags = ['studio', 'gate-failure', 'release-blocker', `stage-${stage}`];
|
|
153
|
+
const fingerprint = createGateFailureFingerprint(failure, context);
|
|
154
|
+
const specRef = normalizeString(context.scene_id) || jobId;
|
|
155
|
+
|
|
156
|
+
const result = await runErrorbookRecordCommand({
|
|
157
|
+
title,
|
|
158
|
+
symptom,
|
|
159
|
+
rootCause,
|
|
160
|
+
fixAction: fixActions,
|
|
161
|
+
tags: tags.join(','),
|
|
162
|
+
ontology: 'execution_flow,decision_policy',
|
|
163
|
+
status: 'candidate',
|
|
164
|
+
fingerprint,
|
|
165
|
+
spec: specRef,
|
|
166
|
+
notes: `auto-captured from studio ${stage} gate`
|
|
167
|
+
}, {
|
|
168
|
+
projectPath: dependencies.projectPath || process.cwd(),
|
|
169
|
+
fileSystem: dependencies.fileSystem || fs
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
errorbook_entry_id: result && result.entry ? result.entry.id : null,
|
|
174
|
+
fingerprint
|
|
175
|
+
};
|
|
176
|
+
} catch (_error) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
104
181
|
async function executeGateSteps(steps, dependencies = {}) {
|
|
105
182
|
const runner = dependencies.commandRunner || defaultCommandRunner;
|
|
106
183
|
const projectPath = dependencies.projectPath || process.cwd();
|
|
107
184
|
const env = dependencies.env || process.env;
|
|
108
185
|
const failOnRequiredSkip = dependencies.failOnRequiredSkip === true;
|
|
186
|
+
const onFailure = typeof dependencies.onFailure === 'function'
|
|
187
|
+
? dependencies.onFailure
|
|
188
|
+
: null;
|
|
109
189
|
|
|
110
190
|
const normalizedSteps = Array.isArray(steps) ? steps.map((step) => normalizeGateStep(step)) : [];
|
|
111
191
|
const results = [];
|
|
@@ -128,6 +208,14 @@ async function executeGateSteps(steps, dependencies = {}) {
|
|
|
128
208
|
? { stdout: '', stderr: '', error: 'required gate step disabled under strict profile' }
|
|
129
209
|
: undefined
|
|
130
210
|
});
|
|
211
|
+
if (skippedAsFailure && onFailure) {
|
|
212
|
+
const failure = results[results.length - 1];
|
|
213
|
+
await Promise.resolve(onFailure({
|
|
214
|
+
reason: 'required_skip',
|
|
215
|
+
step,
|
|
216
|
+
failure
|
|
217
|
+
})).catch(() => {});
|
|
218
|
+
}
|
|
131
219
|
continue;
|
|
132
220
|
}
|
|
133
221
|
|
|
@@ -163,6 +251,14 @@ async function executeGateSteps(steps, dependencies = {}) {
|
|
|
163
251
|
|
|
164
252
|
if (!passed && step.required) {
|
|
165
253
|
hasFailure = true;
|
|
254
|
+
if (onFailure) {
|
|
255
|
+
const failure = results[results.length - 1];
|
|
256
|
+
await Promise.resolve(onFailure({
|
|
257
|
+
reason: 'command_failed',
|
|
258
|
+
step,
|
|
259
|
+
failure
|
|
260
|
+
})).catch(() => {});
|
|
261
|
+
}
|
|
166
262
|
}
|
|
167
263
|
}
|
|
168
264
|
|
|
@@ -345,6 +441,30 @@ async function buildReleaseGateSteps(options = {}, dependencies = {}) {
|
|
|
345
441
|
required: true
|
|
346
442
|
});
|
|
347
443
|
|
|
444
|
+
const gitManagedGateScript = path.join(projectPath, 'scripts', 'git-managed-gate.js');
|
|
445
|
+
const hasGitManagedGateScript = await fileSystem.pathExists(gitManagedGateScript);
|
|
446
|
+
steps.push({
|
|
447
|
+
id: 'git-managed-gate',
|
|
448
|
+
name: 'git managed release gate',
|
|
449
|
+
command: 'node',
|
|
450
|
+
args: ['scripts/git-managed-gate.js', '--fail-on-violation', '--json'],
|
|
451
|
+
required: true,
|
|
452
|
+
enabled: hasGitManagedGateScript,
|
|
453
|
+
skip_reason: hasGitManagedGateScript ? '' : 'scripts/git-managed-gate.js not found'
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const errorbookReleaseGateScript = path.join(projectPath, 'scripts', 'errorbook-release-gate.js');
|
|
457
|
+
const hasErrorbookReleaseGateScript = await fileSystem.pathExists(errorbookReleaseGateScript);
|
|
458
|
+
steps.push({
|
|
459
|
+
id: 'errorbook-release-gate',
|
|
460
|
+
name: 'errorbook release gate',
|
|
461
|
+
command: 'node',
|
|
462
|
+
args: ['scripts/errorbook-release-gate.js', '--fail-on-block', '--json'],
|
|
463
|
+
required: true,
|
|
464
|
+
enabled: hasErrorbookReleaseGateScript,
|
|
465
|
+
skip_reason: hasErrorbookReleaseGateScript ? '' : 'scripts/errorbook-release-gate.js not found'
|
|
466
|
+
});
|
|
467
|
+
|
|
348
468
|
const weeklySummaryPath = path.join(projectPath, '.sce', 'reports', 'release-evidence', 'release-ops-weekly-summary.json');
|
|
349
469
|
const hasWeeklySummary = await fileSystem.pathExists(weeklySummaryPath);
|
|
350
470
|
steps.push({
|
|
@@ -534,14 +654,16 @@ function buildProgress(job) {
|
|
|
534
654
|
}
|
|
535
655
|
|
|
536
656
|
function resolveNextAction(job) {
|
|
657
|
+
const sceneRef = normalizeString(job && job.scene && job.scene.id);
|
|
658
|
+
const planSceneArg = sceneRef || '<scene-id>';
|
|
537
659
|
if (job.status === 'rolled_back') {
|
|
538
|
-
return
|
|
660
|
+
return `sce studio plan --scene ${planSceneArg} --from-chat <session>`;
|
|
539
661
|
}
|
|
540
662
|
if (!job.stages.plan || job.stages.plan.status !== 'completed') {
|
|
541
|
-
return `sce studio plan --from-chat <session> --job ${job.job_id}`;
|
|
663
|
+
return `sce studio plan --scene ${planSceneArg} --from-chat <session> --job ${job.job_id}`;
|
|
542
664
|
}
|
|
543
665
|
if (!job.stages.generate || job.stages.generate.status !== 'completed') {
|
|
544
|
-
return `sce studio generate --
|
|
666
|
+
return `sce studio generate --job ${job.job_id}`;
|
|
545
667
|
}
|
|
546
668
|
if (!job.stages.apply || job.stages.apply.status !== 'completed') {
|
|
547
669
|
const patchBundleId = job.artifacts.patch_bundle_id || '<patch-bundle-id>';
|
|
@@ -613,10 +735,14 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
|
|
|
613
735
|
const projectPath = dependencies.projectPath || process.cwd();
|
|
614
736
|
const fileSystem = dependencies.fileSystem || fs;
|
|
615
737
|
const fromChat = normalizeString(options.fromChat);
|
|
738
|
+
const sceneId = normalizeString(options.scene);
|
|
616
739
|
|
|
617
740
|
if (!fromChat) {
|
|
618
741
|
throw new Error('--from-chat is required');
|
|
619
742
|
}
|
|
743
|
+
if (!sceneId) {
|
|
744
|
+
throw new Error('--scene is required');
|
|
745
|
+
}
|
|
620
746
|
|
|
621
747
|
const paths = resolveStudioPaths(projectPath);
|
|
622
748
|
await ensureStudioDirectories(paths, fileSystem);
|
|
@@ -624,11 +750,20 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
|
|
|
624
750
|
const jobId = normalizeString(options.job) || createJobId();
|
|
625
751
|
const now = nowIso();
|
|
626
752
|
const stages = createStageState();
|
|
753
|
+
const sessionStore = dependencies.sessionStore || new SessionStore(projectPath);
|
|
754
|
+
const sceneSessionBinding = await sessionStore.beginSceneSession({
|
|
755
|
+
sceneId,
|
|
756
|
+
objective: normalizeString(options.goal) || `Studio scene cycle for ${sceneId}`,
|
|
757
|
+
tool: normalizeString(options.tool) || 'generic'
|
|
758
|
+
});
|
|
627
759
|
stages.plan = {
|
|
628
760
|
status: 'completed',
|
|
629
761
|
completed_at: now,
|
|
630
762
|
metadata: {
|
|
631
|
-
from_chat: fromChat
|
|
763
|
+
from_chat: fromChat,
|
|
764
|
+
scene_id: sceneId,
|
|
765
|
+
scene_session_id: sceneSessionBinding.session.session_id,
|
|
766
|
+
scene_cycle: sceneSessionBinding.scene_cycle
|
|
632
767
|
}
|
|
633
768
|
};
|
|
634
769
|
|
|
@@ -643,7 +778,14 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
|
|
|
643
778
|
goal: normalizeString(options.goal) || null
|
|
644
779
|
},
|
|
645
780
|
scene: {
|
|
646
|
-
id:
|
|
781
|
+
id: sceneId
|
|
782
|
+
},
|
|
783
|
+
session: {
|
|
784
|
+
policy: 'mandatory.scene-primary',
|
|
785
|
+
scene_id: sceneId,
|
|
786
|
+
scene_session_id: sceneSessionBinding.session.session_id,
|
|
787
|
+
scene_cycle: sceneSessionBinding.scene_cycle,
|
|
788
|
+
created_new_scene_session: sceneSessionBinding.created_new === true
|
|
647
789
|
},
|
|
648
790
|
target: normalizeString(options.target) || 'default',
|
|
649
791
|
stages,
|
|
@@ -657,6 +799,9 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
|
|
|
657
799
|
await saveJob(paths, job, fileSystem);
|
|
658
800
|
await appendStudioEvent(paths, job, 'stage.plan.completed', {
|
|
659
801
|
from_chat: fromChat,
|
|
802
|
+
scene_id: sceneId,
|
|
803
|
+
scene_session_id: sceneSessionBinding.session.session_id,
|
|
804
|
+
scene_cycle: sceneSessionBinding.scene_cycle,
|
|
660
805
|
target: job.target
|
|
661
806
|
}, fileSystem);
|
|
662
807
|
await writeLatestJob(paths, jobId, fileSystem);
|
|
@@ -669,22 +814,27 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
|
|
|
669
814
|
async function runStudioGenerateCommand(options = {}, dependencies = {}) {
|
|
670
815
|
const projectPath = dependencies.projectPath || process.cwd();
|
|
671
816
|
const fileSystem = dependencies.fileSystem || fs;
|
|
672
|
-
const
|
|
673
|
-
if (!sceneId) {
|
|
674
|
-
throw new Error('--scene is required');
|
|
675
|
-
}
|
|
817
|
+
const sceneArg = normalizeString(options.scene);
|
|
676
818
|
|
|
677
819
|
const paths = resolveStudioPaths(projectPath);
|
|
678
820
|
await ensureStudioDirectories(paths, fileSystem);
|
|
679
821
|
const latestJobId = await readLatestJob(paths, fileSystem);
|
|
680
822
|
const jobId = resolveRequestedJobId(options, latestJobId);
|
|
681
823
|
if (!jobId) {
|
|
682
|
-
throw new Error('No studio job found. Run: sce studio plan --from-chat <session>');
|
|
824
|
+
throw new Error('No studio job found. Run: sce studio plan --scene <scene-id> --from-chat <session>');
|
|
683
825
|
}
|
|
684
826
|
|
|
685
827
|
const job = await loadJob(paths, jobId, fileSystem);
|
|
686
828
|
ensureNotRolledBack(job, 'generate');
|
|
687
829
|
ensureStagePrerequisite(job, 'generate', 'plan');
|
|
830
|
+
const jobSceneId = normalizeString(job && job.scene && job.scene.id);
|
|
831
|
+
if (!jobSceneId) {
|
|
832
|
+
throw new Error('Cannot run studio generate: scene is not defined in plan stage');
|
|
833
|
+
}
|
|
834
|
+
if (sceneArg && sceneArg !== jobSceneId) {
|
|
835
|
+
throw new Error(`Scene mismatch: planned scene is "${jobSceneId}" but --scene provided "${sceneArg}"`);
|
|
836
|
+
}
|
|
837
|
+
const sceneId = sceneArg || jobSceneId;
|
|
688
838
|
const patchBundleId = normalizeString(options.patchBundle) || `patch-${sceneId}-${Date.now()}`;
|
|
689
839
|
|
|
690
840
|
job.scene = job.scene || {};
|
|
@@ -723,7 +873,7 @@ async function runStudioApplyCommand(options = {}, dependencies = {}) {
|
|
|
723
873
|
const latestJobId = await readLatestJob(paths, fileSystem);
|
|
724
874
|
const jobId = resolveRequestedJobId(options, latestJobId);
|
|
725
875
|
if (!jobId) {
|
|
726
|
-
throw new Error('No studio job found. Run: sce studio plan --from-chat <session>');
|
|
876
|
+
throw new Error('No studio job found. Run: sce studio plan --scene <scene-id> --from-chat <session>');
|
|
727
877
|
}
|
|
728
878
|
|
|
729
879
|
const job = await loadJob(paths, jobId, fileSystem);
|
|
@@ -771,7 +921,7 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
|
|
|
771
921
|
const latestJobId = await readLatestJob(paths, fileSystem);
|
|
772
922
|
const jobId = resolveRequestedJobId(options, latestJobId);
|
|
773
923
|
if (!jobId) {
|
|
774
|
-
throw new Error('No studio job found. Run: sce studio plan --from-chat <session>');
|
|
924
|
+
throw new Error('No studio job found. Run: sce studio plan --scene <scene-id> --from-chat <session>');
|
|
775
925
|
}
|
|
776
926
|
|
|
777
927
|
const profile = normalizeString(options.profile) || 'standard';
|
|
@@ -781,6 +931,7 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
|
|
|
781
931
|
|
|
782
932
|
const verifyReportPath = `${STUDIO_REPORTS_DIR}/verify-${job.job_id}.json`;
|
|
783
933
|
const verifyStartedAt = nowIso();
|
|
934
|
+
const autoErrorbookRecords = [];
|
|
784
935
|
const gateSteps = await buildVerifyGateSteps({ profile }, {
|
|
785
936
|
projectPath,
|
|
786
937
|
fileSystem
|
|
@@ -789,7 +940,26 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
|
|
|
789
940
|
projectPath,
|
|
790
941
|
commandRunner: dependencies.commandRunner,
|
|
791
942
|
env: dependencies.env,
|
|
792
|
-
failOnRequiredSkip: profile === 'strict'
|
|
943
|
+
failOnRequiredSkip: profile === 'strict',
|
|
944
|
+
onFailure: async ({ failure }) => {
|
|
945
|
+
const captured = await autoRecordGateFailure(failure, {
|
|
946
|
+
stage: 'verify',
|
|
947
|
+
profile,
|
|
948
|
+
job_id: job.job_id,
|
|
949
|
+
scene_id: job?.scene?.id
|
|
950
|
+
}, {
|
|
951
|
+
projectPath,
|
|
952
|
+
fileSystem,
|
|
953
|
+
autoRecordFailures: dependencies.autoRecordFailures
|
|
954
|
+
});
|
|
955
|
+
if (captured && captured.errorbook_entry_id) {
|
|
956
|
+
autoErrorbookRecords.push({
|
|
957
|
+
step_id: failure.id,
|
|
958
|
+
entry_id: captured.errorbook_entry_id,
|
|
959
|
+
fingerprint: captured.fingerprint
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
}
|
|
793
963
|
});
|
|
794
964
|
const verifyCompletedAt = nowIso();
|
|
795
965
|
const verifyReport = {
|
|
@@ -800,7 +970,8 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
|
|
|
800
970
|
started_at: verifyStartedAt,
|
|
801
971
|
completed_at: verifyCompletedAt,
|
|
802
972
|
passed: gateResult.passed,
|
|
803
|
-
steps: gateResult.steps
|
|
973
|
+
steps: gateResult.steps,
|
|
974
|
+
auto_errorbook_records: autoErrorbookRecords
|
|
804
975
|
};
|
|
805
976
|
|
|
806
977
|
await writeStudioReport(projectPath, verifyReportPath, verifyReport, fileSystem);
|
|
@@ -817,13 +988,15 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
|
|
|
817
988
|
metadata: {
|
|
818
989
|
profile,
|
|
819
990
|
passed: false,
|
|
820
|
-
report: verifyReportPath
|
|
991
|
+
report: verifyReportPath,
|
|
992
|
+
auto_errorbook_records: autoErrorbookRecords
|
|
821
993
|
}
|
|
822
994
|
};
|
|
823
995
|
await saveJob(paths, job, fileSystem);
|
|
824
996
|
await appendStudioEvent(paths, job, 'stage.verify.failed', {
|
|
825
997
|
profile,
|
|
826
|
-
report: verifyReportPath
|
|
998
|
+
report: verifyReportPath,
|
|
999
|
+
auto_errorbook_records: autoErrorbookRecords
|
|
827
1000
|
}, fileSystem);
|
|
828
1001
|
await writeLatestJob(paths, jobId, fileSystem);
|
|
829
1002
|
throw new Error(`studio verify failed: ${gateResult.steps.filter((step) => step.status === 'failed').map((step) => step.id).join(', ')}`);
|
|
@@ -833,14 +1006,16 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
|
|
|
833
1006
|
ensureStageCompleted(job, 'verify', {
|
|
834
1007
|
profile,
|
|
835
1008
|
passed: true,
|
|
836
|
-
report: verifyReportPath
|
|
1009
|
+
report: verifyReportPath,
|
|
1010
|
+
auto_errorbook_records: autoErrorbookRecords
|
|
837
1011
|
});
|
|
838
1012
|
|
|
839
1013
|
await saveJob(paths, job, fileSystem);
|
|
840
1014
|
await appendStudioEvent(paths, job, 'stage.verify.completed', {
|
|
841
1015
|
profile,
|
|
842
1016
|
passed: true,
|
|
843
|
-
report: verifyReportPath
|
|
1017
|
+
report: verifyReportPath,
|
|
1018
|
+
auto_errorbook_records: autoErrorbookRecords
|
|
844
1019
|
}, fileSystem);
|
|
845
1020
|
await writeLatestJob(paths, jobId, fileSystem);
|
|
846
1021
|
|
|
@@ -858,7 +1033,7 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
|
|
|
858
1033
|
const latestJobId = await readLatestJob(paths, fileSystem);
|
|
859
1034
|
const jobId = resolveRequestedJobId(options, latestJobId);
|
|
860
1035
|
if (!jobId) {
|
|
861
|
-
throw new Error('No studio job found. Run: sce studio plan --from-chat <session>');
|
|
1036
|
+
throw new Error('No studio job found. Run: sce studio plan --scene <scene-id> --from-chat <session>');
|
|
862
1037
|
}
|
|
863
1038
|
|
|
864
1039
|
const channel = normalizeString(options.channel) || 'dev';
|
|
@@ -880,6 +1055,7 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
|
|
|
880
1055
|
const profile = normalizeString(options.profile) || 'standard';
|
|
881
1056
|
const releaseReportPath = `${STUDIO_REPORTS_DIR}/release-${job.job_id}.json`;
|
|
882
1057
|
const releaseStartedAt = nowIso();
|
|
1058
|
+
const autoErrorbookRecords = [];
|
|
883
1059
|
const gateSteps = await buildReleaseGateSteps({ profile }, {
|
|
884
1060
|
projectPath,
|
|
885
1061
|
fileSystem
|
|
@@ -888,7 +1064,26 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
|
|
|
888
1064
|
projectPath,
|
|
889
1065
|
commandRunner: dependencies.commandRunner,
|
|
890
1066
|
env: dependencies.env,
|
|
891
|
-
failOnRequiredSkip: profile === 'strict'
|
|
1067
|
+
failOnRequiredSkip: profile === 'strict',
|
|
1068
|
+
onFailure: async ({ failure }) => {
|
|
1069
|
+
const captured = await autoRecordGateFailure(failure, {
|
|
1070
|
+
stage: 'release',
|
|
1071
|
+
profile,
|
|
1072
|
+
job_id: job.job_id,
|
|
1073
|
+
scene_id: job?.scene?.id
|
|
1074
|
+
}, {
|
|
1075
|
+
projectPath,
|
|
1076
|
+
fileSystem,
|
|
1077
|
+
autoRecordFailures: dependencies.autoRecordFailures
|
|
1078
|
+
});
|
|
1079
|
+
if (captured && captured.errorbook_entry_id) {
|
|
1080
|
+
autoErrorbookRecords.push({
|
|
1081
|
+
step_id: failure.id,
|
|
1082
|
+
entry_id: captured.errorbook_entry_id,
|
|
1083
|
+
fingerprint: captured.fingerprint
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
892
1087
|
});
|
|
893
1088
|
const releaseCompletedAt = nowIso();
|
|
894
1089
|
const releaseReport = {
|
|
@@ -901,7 +1096,8 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
|
|
|
901
1096
|
started_at: releaseStartedAt,
|
|
902
1097
|
completed_at: releaseCompletedAt,
|
|
903
1098
|
passed: gateResult.passed,
|
|
904
|
-
steps: gateResult.steps
|
|
1099
|
+
steps: gateResult.steps,
|
|
1100
|
+
auto_errorbook_records: autoErrorbookRecords
|
|
905
1101
|
};
|
|
906
1102
|
|
|
907
1103
|
await writeStudioReport(projectPath, releaseReportPath, releaseReport, fileSystem);
|
|
@@ -921,7 +1117,8 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
|
|
|
921
1117
|
release_ref: releaseRef,
|
|
922
1118
|
passed: false,
|
|
923
1119
|
report: releaseReportPath,
|
|
924
|
-
auth_required: authResult.required
|
|
1120
|
+
auth_required: authResult.required,
|
|
1121
|
+
auto_errorbook_records: autoErrorbookRecords
|
|
925
1122
|
}
|
|
926
1123
|
};
|
|
927
1124
|
await saveJob(paths, job, fileSystem);
|
|
@@ -929,7 +1126,8 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
|
|
|
929
1126
|
channel,
|
|
930
1127
|
release_ref: releaseRef,
|
|
931
1128
|
report: releaseReportPath,
|
|
932
|
-
auth_required: authResult.required
|
|
1129
|
+
auth_required: authResult.required,
|
|
1130
|
+
auto_errorbook_records: autoErrorbookRecords
|
|
933
1131
|
}, fileSystem);
|
|
934
1132
|
await writeLatestJob(paths, jobId, fileSystem);
|
|
935
1133
|
throw new Error(`studio release failed: ${gateResult.steps.filter((step) => step.status === 'failed').map((step) => step.id).join(', ')}`);
|
|
@@ -940,15 +1138,38 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
|
|
|
940
1138
|
channel,
|
|
941
1139
|
release_ref: releaseRef,
|
|
942
1140
|
report: releaseReportPath,
|
|
943
|
-
auth_required: authResult.required
|
|
1141
|
+
auth_required: authResult.required,
|
|
1142
|
+
auto_errorbook_records: autoErrorbookRecords
|
|
944
1143
|
});
|
|
945
1144
|
|
|
1145
|
+
const sceneId = normalizeString(job && job.scene && job.scene.id);
|
|
1146
|
+
const sceneSessionId = normalizeString(job && job.session && job.session.scene_session_id);
|
|
1147
|
+
if (sceneId && sceneSessionId) {
|
|
1148
|
+
const sessionStore = dependencies.sessionStore || new SessionStore(projectPath);
|
|
1149
|
+
const rollover = await sessionStore.completeSceneSession(sceneId, sceneSessionId, {
|
|
1150
|
+
summary: `Scene ${sceneId} completed by studio release ${releaseRef}`,
|
|
1151
|
+
status: 'completed',
|
|
1152
|
+
jobId: job.job_id,
|
|
1153
|
+
releaseRef,
|
|
1154
|
+
channel,
|
|
1155
|
+
nextObjective: `Next cycle for scene ${sceneId} after release ${releaseRef}`
|
|
1156
|
+
});
|
|
1157
|
+
job.session = {
|
|
1158
|
+
...(job.session || {}),
|
|
1159
|
+
completed_scene_session_id: rollover.completed_session.session_id,
|
|
1160
|
+
scene_session_id: rollover.next_session ? rollover.next_session.session_id : null,
|
|
1161
|
+
scene_cycle: rollover.next_scene_cycle || null,
|
|
1162
|
+
rolled_over_at: nowIso()
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
|
|
946
1166
|
await saveJob(paths, job, fileSystem);
|
|
947
1167
|
await appendStudioEvent(paths, job, 'stage.release.completed', {
|
|
948
1168
|
channel,
|
|
949
1169
|
release_ref: releaseRef,
|
|
950
1170
|
report: releaseReportPath,
|
|
951
|
-
auth_required: authResult.required
|
|
1171
|
+
auth_required: authResult.required,
|
|
1172
|
+
auto_errorbook_records: autoErrorbookRecords
|
|
952
1173
|
}, fileSystem);
|
|
953
1174
|
await writeLatestJob(paths, jobId, fileSystem);
|
|
954
1175
|
|
|
@@ -966,7 +1187,7 @@ async function runStudioResumeCommand(options = {}, dependencies = {}) {
|
|
|
966
1187
|
const latestJobId = await readLatestJob(paths, fileSystem);
|
|
967
1188
|
const jobId = resolveRequestedJobId(options, latestJobId);
|
|
968
1189
|
if (!jobId) {
|
|
969
|
-
throw new Error('No studio job found. Run: sce studio plan --from-chat <session>');
|
|
1190
|
+
throw new Error('No studio job found. Run: sce studio plan --scene <scene-id> --from-chat <session>');
|
|
970
1191
|
}
|
|
971
1192
|
|
|
972
1193
|
const job = await loadJob(paths, jobId, fileSystem);
|
|
@@ -985,7 +1206,7 @@ async function runStudioRollbackCommand(options = {}, dependencies = {}) {
|
|
|
985
1206
|
const latestJobId = await readLatestJob(paths, fileSystem);
|
|
986
1207
|
const jobId = resolveRequestedJobId(options, latestJobId);
|
|
987
1208
|
if (!jobId) {
|
|
988
|
-
throw new Error('No studio job found. Run: sce studio plan --from-chat <session>');
|
|
1209
|
+
throw new Error('No studio job found. Run: sce studio plan --scene <scene-id> --from-chat <session>');
|
|
989
1210
|
}
|
|
990
1211
|
|
|
991
1212
|
const reason = normalizeString(options.reason) || 'manual-rollback';
|
|
@@ -1008,6 +1229,19 @@ async function runStudioRollbackCommand(options = {}, dependencies = {}) {
|
|
|
1008
1229
|
auth_required: authResult.required
|
|
1009
1230
|
};
|
|
1010
1231
|
|
|
1232
|
+
const sceneSessionId = normalizeString(job && job.session && job.session.scene_session_id);
|
|
1233
|
+
if (sceneSessionId) {
|
|
1234
|
+
const sessionStore = dependencies.sessionStore || new SessionStore(projectPath);
|
|
1235
|
+
await sessionStore.snapshotSession(sceneSessionId, {
|
|
1236
|
+
summary: `Studio rollback: ${reason}`,
|
|
1237
|
+
status: 'rolled_back',
|
|
1238
|
+
payload: {
|
|
1239
|
+
job_id: job.job_id,
|
|
1240
|
+
reason
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1011
1245
|
await saveJob(paths, job, fileSystem);
|
|
1012
1246
|
await appendStudioEvent(paths, job, 'job.rolled_back', {
|
|
1013
1247
|
reason
|
|
@@ -1050,7 +1284,7 @@ async function runStudioEventsCommand(options = {}, dependencies = {}) {
|
|
|
1050
1284
|
const latestJobId = await readLatestJob(paths, fileSystem);
|
|
1051
1285
|
const jobId = resolveRequestedJobId(options, latestJobId);
|
|
1052
1286
|
if (!jobId) {
|
|
1053
|
-
throw new Error('No studio job found. Run: sce studio plan --from-chat <session>');
|
|
1287
|
+
throw new Error('No studio job found. Run: sce studio plan --scene <scene-id> --from-chat <session>');
|
|
1054
1288
|
}
|
|
1055
1289
|
|
|
1056
1290
|
const limit = normalizePositiveInteger(options.limit, 50);
|
|
@@ -1084,6 +1318,7 @@ function registerStudioCommands(program) {
|
|
|
1084
1318
|
studio
|
|
1085
1319
|
.command('plan')
|
|
1086
1320
|
.description('Create/refresh a studio plan job from chat context')
|
|
1321
|
+
.requiredOption('--scene <scene-id>', 'Scene identifier (mandatory primary session anchor)')
|
|
1087
1322
|
.requiredOption('--from-chat <session>', 'Chat session identifier or transcript reference')
|
|
1088
1323
|
.option('--goal <goal>', 'Optional goal summary')
|
|
1089
1324
|
.option('--target <target>', 'Target integration profile', 'default')
|
|
@@ -1093,8 +1328,8 @@ function registerStudioCommands(program) {
|
|
|
1093
1328
|
|
|
1094
1329
|
studio
|
|
1095
1330
|
.command('generate')
|
|
1096
|
-
.description('Generate patch bundle metadata for a planned studio job')
|
|
1097
|
-
.
|
|
1331
|
+
.description('Generate patch bundle metadata for a planned studio job (scene inherited from plan)')
|
|
1332
|
+
.option('--scene <scene-id>', 'Optional scene identifier; must match planned scene when provided')
|
|
1098
1333
|
.option('--target <target>', 'Target integration profile override')
|
|
1099
1334
|
.option('--patch-bundle <id>', 'Explicit patch bundle id')
|
|
1100
1335
|
.option('--job <job-id>', 'Studio job id (defaults to latest)')
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
const { resolveSpecSceneBinding } = require('./scene-session-binding');
|
|
2
|
+
|
|
3
|
+
function normalizeString(value) {
|
|
4
|
+
if (typeof value !== 'string') {
|
|
5
|
+
return '';
|
|
6
|
+
}
|
|
7
|
+
return value.trim();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function toSpecStatus(specId, orchestratePayload = {}, topLevelStatus = '') {
|
|
11
|
+
const completed = new Set(Array.isArray(orchestratePayload.completed) ? orchestratePayload.completed : []);
|
|
12
|
+
const failed = new Set([
|
|
13
|
+
...(Array.isArray(orchestratePayload.failed) ? orchestratePayload.failed : []),
|
|
14
|
+
...(Array.isArray(orchestratePayload.skipped) ? orchestratePayload.skipped : [])
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
if (completed.has(specId)) {
|
|
18
|
+
return 'completed';
|
|
19
|
+
}
|
|
20
|
+
if (failed.has(specId)) {
|
|
21
|
+
return 'failed';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const orchestrationStatus = normalizeString(orchestratePayload.status).toLowerCase();
|
|
25
|
+
if (orchestrationStatus === 'completed') {
|
|
26
|
+
return 'completed';
|
|
27
|
+
}
|
|
28
|
+
if (orchestrationStatus === 'failed' || orchestrationStatus === 'stopped') {
|
|
29
|
+
return 'failed';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const topStatus = normalizeString(topLevelStatus).toLowerCase();
|
|
33
|
+
if (topStatus === 'completed') {
|
|
34
|
+
return 'completed';
|
|
35
|
+
}
|
|
36
|
+
return 'failed';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function bindMultiSpecSceneSession(options = {}, dependencies = {}) {
|
|
40
|
+
const {
|
|
41
|
+
specTargets = [],
|
|
42
|
+
sceneId = null,
|
|
43
|
+
commandName = 'spec-command',
|
|
44
|
+
commandLabel = 'spec-command',
|
|
45
|
+
commandOptions = {},
|
|
46
|
+
runViaOrchestrate
|
|
47
|
+
} = options;
|
|
48
|
+
const {
|
|
49
|
+
projectPath = process.cwd(),
|
|
50
|
+
fileSystem,
|
|
51
|
+
sessionStore
|
|
52
|
+
} = dependencies;
|
|
53
|
+
|
|
54
|
+
if (!Array.isArray(specTargets) || specTargets.length === 0) {
|
|
55
|
+
throw new Error('specTargets is required for bindMultiSpecSceneSession');
|
|
56
|
+
}
|
|
57
|
+
if (typeof runViaOrchestrate !== 'function') {
|
|
58
|
+
throw new Error('runViaOrchestrate callback is required for bindMultiSpecSceneSession');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const sceneBinding = await resolveSpecSceneBinding({
|
|
62
|
+
sceneId,
|
|
63
|
+
allowNoScene: false
|
|
64
|
+
}, {
|
|
65
|
+
projectPath,
|
|
66
|
+
fileSystem,
|
|
67
|
+
sessionStore
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const shouldTrackSessions = commandOptions.dryRun !== true;
|
|
71
|
+
const specSessions = [];
|
|
72
|
+
if (shouldTrackSessions) {
|
|
73
|
+
for (const specId of specTargets) {
|
|
74
|
+
const linked = await sessionStore.startSpecSession({
|
|
75
|
+
sceneId: sceneBinding.scene_id,
|
|
76
|
+
specId,
|
|
77
|
+
objective: `${commandLabel} (orchestrate): ${specId}`
|
|
78
|
+
});
|
|
79
|
+
specSessions.push({
|
|
80
|
+
spec_id: specId,
|
|
81
|
+
spec_session_id: linked.spec_session.session_id
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const result = await runViaOrchestrate();
|
|
88
|
+
const orchestratePayload = result && result.orchestrate_result
|
|
89
|
+
? result.orchestrate_result
|
|
90
|
+
: {};
|
|
91
|
+
|
|
92
|
+
if (shouldTrackSessions) {
|
|
93
|
+
for (const item of specSessions) {
|
|
94
|
+
const specStatus = toSpecStatus(item.spec_id, orchestratePayload, result && result.status);
|
|
95
|
+
await sessionStore.completeSpecSession({
|
|
96
|
+
specSessionRef: item.spec_session_id,
|
|
97
|
+
status: specStatus,
|
|
98
|
+
summary: `${commandLabel} orchestrate ${specStatus}: ${item.spec_id}`,
|
|
99
|
+
payload: {
|
|
100
|
+
command: commandName,
|
|
101
|
+
mode: 'multi-spec-orchestrate',
|
|
102
|
+
spec: item.spec_id,
|
|
103
|
+
orchestration_status: normalizeString(orchestratePayload.status) || null
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
...result,
|
|
111
|
+
scene_session: {
|
|
112
|
+
bound: true,
|
|
113
|
+
scene_id: sceneBinding.scene_id,
|
|
114
|
+
scene_cycle: sceneBinding.scene_cycle,
|
|
115
|
+
scene_session_id: sceneBinding.scene_session_id,
|
|
116
|
+
binding_source: sceneBinding.source,
|
|
117
|
+
multi_spec_child_sessions: specSessions
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (shouldTrackSessions) {
|
|
122
|
+
for (const item of specSessions) {
|
|
123
|
+
try {
|
|
124
|
+
await sessionStore.completeSpecSession({
|
|
125
|
+
specSessionRef: item.spec_session_id,
|
|
126
|
+
status: 'failed',
|
|
127
|
+
summary: `${commandLabel} orchestrate failed: ${item.spec_id}`,
|
|
128
|
+
payload: {
|
|
129
|
+
command: commandName,
|
|
130
|
+
mode: 'multi-spec-orchestrate',
|
|
131
|
+
spec: item.spec_id,
|
|
132
|
+
error: error.message
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
} catch (_innerError) {
|
|
136
|
+
// best-effort close
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = {
|
|
145
|
+
bindMultiSpecSceneSession,
|
|
146
|
+
toSpecStatus
|
|
147
|
+
};
|