scene-capability-engine 3.3.14 → 3.3.16

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.
@@ -0,0 +1,1186 @@
1
+ const path = require('path');
2
+ const crypto = require('crypto');
3
+ const { spawnSync } = require('child_process');
4
+ const fs = require('fs-extra');
5
+ const chalk = require('chalk');
6
+
7
+ const STUDIO_JOB_API_VERSION = 'sce.studio.job/v0.1';
8
+ const STAGE_ORDER = ['plan', 'generate', 'apply', 'verify', 'release'];
9
+ const RELEASE_CHANNELS = new Set(['dev', 'prod']);
10
+ const STUDIO_EVENT_API_VERSION = 'sce.studio.event/v0.1';
11
+ const VERIFY_PROFILES = new Set(['fast', 'standard', 'strict']);
12
+ const RELEASE_PROFILES = new Set(['standard', 'strict']);
13
+ const STUDIO_REPORTS_DIR = '.sce/reports/studio';
14
+ const MAX_OUTPUT_PREVIEW_LENGTH = 2000;
15
+ const DEFAULT_STUDIO_SECURITY_POLICY = Object.freeze({
16
+ enabled: false,
17
+ require_auth_for: ['apply', 'release', 'rollback'],
18
+ password_env: 'SCE_STUDIO_AUTH_PASSWORD'
19
+ });
20
+
21
+ function resolveStudioPaths(projectPath = process.cwd()) {
22
+ const studioDir = path.join(projectPath, '.sce', 'studio');
23
+ return {
24
+ projectPath,
25
+ studioDir,
26
+ jobsDir: path.join(studioDir, 'jobs'),
27
+ latestFile: path.join(studioDir, 'latest-job.json'),
28
+ eventsDir: path.join(studioDir, 'events')
29
+ };
30
+ }
31
+
32
+ function nowIso() {
33
+ return new Date().toISOString();
34
+ }
35
+
36
+ function normalizeString(value) {
37
+ if (typeof value !== 'string') {
38
+ return '';
39
+ }
40
+ return value.trim();
41
+ }
42
+
43
+ function createJobId(prefix = 'studio') {
44
+ const random = crypto.randomBytes(4).toString('hex');
45
+ return `${prefix}-${Date.now()}-${random}`;
46
+ }
47
+
48
+ function createStageState() {
49
+ return {
50
+ plan: { status: 'pending', completed_at: null, metadata: {} },
51
+ generate: { status: 'pending', completed_at: null, metadata: {} },
52
+ apply: { status: 'pending', completed_at: null, metadata: {} },
53
+ verify: { status: 'pending', completed_at: null, metadata: {} },
54
+ release: { status: 'pending', completed_at: null, metadata: {} }
55
+ };
56
+ }
57
+
58
+ function clipOutput(value) {
59
+ if (typeof value !== 'string') {
60
+ return '';
61
+ }
62
+ if (value.length <= MAX_OUTPUT_PREVIEW_LENGTH) {
63
+ return value;
64
+ }
65
+ return `${value.slice(0, MAX_OUTPUT_PREVIEW_LENGTH)}...[truncated]`;
66
+ }
67
+
68
+ function defaultCommandRunner(command, args = [], options = {}) {
69
+ const startedAt = Date.now();
70
+ const result = spawnSync(command, args, {
71
+ cwd: options.cwd || process.cwd(),
72
+ env: options.env || process.env,
73
+ encoding: 'utf8',
74
+ maxBuffer: 1024 * 1024 * 10,
75
+ windowsHide: true
76
+ });
77
+
78
+ return {
79
+ status: Number.isInteger(result.status) ? result.status : 1,
80
+ stdout: `${result.stdout || ''}`,
81
+ stderr: `${result.stderr || ''}`,
82
+ error: result.error ? `${result.error.message || result.error}` : null,
83
+ duration_ms: Date.now() - startedAt
84
+ };
85
+ }
86
+
87
+ function buildCommandString(command, args = []) {
88
+ return [command, ...args].join(' ').trim();
89
+ }
90
+
91
+ function normalizeGateStep(step) {
92
+ return {
93
+ id: normalizeString(step.id),
94
+ name: normalizeString(step.name) || normalizeString(step.id),
95
+ command: normalizeString(step.command),
96
+ args: Array.isArray(step.args) ? step.args.map((item) => `${item}`) : [],
97
+ cwd: normalizeString(step.cwd) || null,
98
+ enabled: step.enabled !== false,
99
+ skip_reason: normalizeString(step.skip_reason),
100
+ required: step.required !== false
101
+ };
102
+ }
103
+
104
+ async function executeGateSteps(steps, dependencies = {}) {
105
+ const runner = dependencies.commandRunner || defaultCommandRunner;
106
+ const projectPath = dependencies.projectPath || process.cwd();
107
+ const env = dependencies.env || process.env;
108
+ const failOnRequiredSkip = dependencies.failOnRequiredSkip === true;
109
+
110
+ const normalizedSteps = Array.isArray(steps) ? steps.map((step) => normalizeGateStep(step)) : [];
111
+ const results = [];
112
+ let hasFailure = false;
113
+
114
+ for (const step of normalizedSteps) {
115
+ if (!step.enabled) {
116
+ const skippedAsFailure = failOnRequiredSkip && step.required;
117
+ if (skippedAsFailure) {
118
+ hasFailure = true;
119
+ }
120
+ results.push({
121
+ id: step.id,
122
+ name: step.name,
123
+ status: skippedAsFailure ? 'failed' : 'skipped',
124
+ required: step.required,
125
+ command: buildCommandString(step.command, step.args),
126
+ skip_reason: step.skip_reason || 'disabled',
127
+ output: skippedAsFailure
128
+ ? { stdout: '', stderr: '', error: 'required gate step disabled under strict profile' }
129
+ : undefined
130
+ });
131
+ continue;
132
+ }
133
+
134
+ const startedAt = nowIso();
135
+ const raw = await Promise.resolve(runner(step.command, step.args, {
136
+ cwd: step.cwd || projectPath,
137
+ env
138
+ }));
139
+ const statusCode = Number.isInteger(raw && raw.status) ? raw.status : 1;
140
+ const passed = statusCode === 0;
141
+ const endedAt = nowIso();
142
+ const output = {
143
+ stdout: clipOutput(raw && raw.stdout ? `${raw.stdout}` : ''),
144
+ stderr: clipOutput(raw && raw.stderr ? `${raw.stderr}` : ''),
145
+ error: raw && raw.error ? `${raw.error}` : null
146
+ };
147
+ const durationMs = Number.isFinite(Number(raw && raw.duration_ms))
148
+ ? Number(raw.duration_ms)
149
+ : null;
150
+
151
+ results.push({
152
+ id: step.id,
153
+ name: step.name,
154
+ status: passed ? 'passed' : 'failed',
155
+ required: step.required,
156
+ command: buildCommandString(step.command, step.args),
157
+ exit_code: statusCode,
158
+ started_at: startedAt,
159
+ completed_at: endedAt,
160
+ duration_ms: durationMs,
161
+ output
162
+ });
163
+
164
+ if (!passed && step.required) {
165
+ hasFailure = true;
166
+ }
167
+ }
168
+
169
+ return {
170
+ passed: !hasFailure,
171
+ steps: results
172
+ };
173
+ }
174
+
175
+ async function readPackageJson(projectPath, fileSystem = fs) {
176
+ const packageJsonPath = path.join(projectPath, 'package.json');
177
+ const exists = await fileSystem.pathExists(packageJsonPath);
178
+ if (!exists) {
179
+ return null;
180
+ }
181
+
182
+ try {
183
+ return await fileSystem.readJson(packageJsonPath);
184
+ } catch (_error) {
185
+ return null;
186
+ }
187
+ }
188
+
189
+ function normalizeSecurityPolicy(policy) {
190
+ const normalized = {
191
+ enabled: policy && policy.enabled === true,
192
+ require_auth_for: Array.isArray(policy && policy.require_auth_for)
193
+ ? policy.require_auth_for
194
+ .map((item) => normalizeString(item))
195
+ .filter(Boolean)
196
+ : [...DEFAULT_STUDIO_SECURITY_POLICY.require_auth_for],
197
+ password_env: normalizeString(policy && policy.password_env) || DEFAULT_STUDIO_SECURITY_POLICY.password_env
198
+ };
199
+ return normalized;
200
+ }
201
+
202
+ async function loadStudioSecurityPolicy(projectPath, fileSystem = fs, env = process.env) {
203
+ const policyPath = path.join(projectPath, '.sce', 'config', 'studio-security.json');
204
+ let filePolicy = {};
205
+
206
+ if (await fileSystem.pathExists(policyPath)) {
207
+ try {
208
+ filePolicy = await fileSystem.readJson(policyPath);
209
+ } catch (error) {
210
+ throw new Error(`Failed to read studio security policy: ${error.message}`);
211
+ }
212
+ }
213
+
214
+ const envEnabled = `${env.SCE_STUDIO_REQUIRE_AUTH || ''}`.trim() === '1';
215
+ const envPasswordVar = normalizeString(env.SCE_STUDIO_PASSWORD_ENV);
216
+
217
+ return normalizeSecurityPolicy({
218
+ ...DEFAULT_STUDIO_SECURITY_POLICY,
219
+ ...filePolicy,
220
+ enabled: envEnabled || filePolicy.enabled === true,
221
+ password_env: envPasswordVar || filePolicy.password_env || DEFAULT_STUDIO_SECURITY_POLICY.password_env
222
+ });
223
+ }
224
+
225
+ async function ensureStudioAuthorization(action, options = {}, dependencies = {}) {
226
+ const projectPath = dependencies.projectPath || process.cwd();
227
+ const fileSystem = dependencies.fileSystem || fs;
228
+ const env = dependencies.env || process.env;
229
+ const policy = await loadStudioSecurityPolicy(projectPath, fileSystem, env);
230
+ const requiredActions = new Set(policy.require_auth_for);
231
+ const requiresAuth = options.requireAuth === true || (policy.enabled && requiredActions.has(action));
232
+
233
+ if (!requiresAuth) {
234
+ return {
235
+ required: false,
236
+ passed: true,
237
+ policy
238
+ };
239
+ }
240
+
241
+ const passwordEnv = normalizeString(policy.password_env) || DEFAULT_STUDIO_SECURITY_POLICY.password_env;
242
+ const expectedPassword = normalizeString(dependencies.authSecret || env[passwordEnv]);
243
+ if (!expectedPassword) {
244
+ throw new Error(`Authorization required for studio ${action}, but ${passwordEnv} is not configured`);
245
+ }
246
+
247
+ const providedPassword = normalizeString(options.authPassword);
248
+ if (!providedPassword) {
249
+ throw new Error(`Authorization required for studio ${action}. Provide --auth-password`);
250
+ }
251
+
252
+ if (providedPassword !== expectedPassword) {
253
+ throw new Error(`Authorization failed for studio ${action}: invalid password`);
254
+ }
255
+
256
+ return {
257
+ required: true,
258
+ passed: true,
259
+ policy,
260
+ password_env: passwordEnv
261
+ };
262
+ }
263
+
264
+ async function buildVerifyGateSteps(options = {}, dependencies = {}) {
265
+ const projectPath = dependencies.projectPath || process.cwd();
266
+ const fileSystem = dependencies.fileSystem || fs;
267
+ const profile = normalizeString(options.profile) || 'standard';
268
+
269
+ if (!VERIFY_PROFILES.has(profile)) {
270
+ throw new Error(`Invalid verify profile "${profile}". Expected one of: ${Array.from(VERIFY_PROFILES).join(', ')}`);
271
+ }
272
+
273
+ const packageJson = await readPackageJson(projectPath, fileSystem);
274
+ const scripts = packageJson && packageJson.scripts ? packageJson.scripts : {};
275
+ const hasUnit = typeof scripts['test:unit'] === 'string';
276
+ const hasTest = typeof scripts.test === 'string';
277
+
278
+ const steps = [];
279
+ if (hasUnit || hasTest) {
280
+ const npmCommand = hasUnit
281
+ ? { args: ['run', 'test:unit', '--', '--runInBand'], name: 'npm run test:unit -- --runInBand', id: 'unit-tests' }
282
+ : { args: ['test', '--', '--runInBand'], name: 'npm test -- --runInBand', id: 'tests' };
283
+ steps.push({
284
+ id: npmCommand.id,
285
+ name: npmCommand.name,
286
+ command: 'npm',
287
+ args: npmCommand.args,
288
+ required: true
289
+ });
290
+ } else {
291
+ steps.push({
292
+ id: 'tests',
293
+ name: 'No npm test script',
294
+ command: 'npm',
295
+ args: ['test', '--', '--runInBand'],
296
+ enabled: false,
297
+ required: profile === 'strict',
298
+ skip_reason: 'package.json test script not found'
299
+ });
300
+ }
301
+
302
+ if (profile === 'standard' || profile === 'strict') {
303
+ const governanceScript = path.join(projectPath, 'scripts', 'interactive-governance-report.js');
304
+ const hasGovernanceScript = await fileSystem.pathExists(governanceScript);
305
+ steps.push({
306
+ id: 'interactive-governance-report',
307
+ name: 'interactive-governance-report',
308
+ command: 'node',
309
+ args: ['scripts/interactive-governance-report.js', '--period', 'weekly', '--json'],
310
+ required: true,
311
+ enabled: hasGovernanceScript,
312
+ skip_reason: hasGovernanceScript ? '' : 'scripts/interactive-governance-report.js not found'
313
+ });
314
+
315
+ const handoffManifest = path.join(projectPath, 'docs', 'handoffs', 'handoff-manifest.json');
316
+ const hasHandoffManifest = await fileSystem.pathExists(handoffManifest);
317
+ steps.push({
318
+ id: 'scene-package-publish-batch-dry-run',
319
+ name: 'scene package publish-batch dry-run',
320
+ command: 'node',
321
+ args: ['bin/sce.js', 'scene', 'package-publish-batch', '--manifest', 'docs/handoffs/handoff-manifest.json', '--dry-run', '--json'],
322
+ required: true,
323
+ enabled: hasHandoffManifest,
324
+ skip_reason: hasHandoffManifest ? '' : 'docs/handoffs/handoff-manifest.json not found'
325
+ });
326
+ }
327
+
328
+ return steps;
329
+ }
330
+
331
+ async function buildReleaseGateSteps(options = {}, dependencies = {}) {
332
+ const projectPath = dependencies.projectPath || process.cwd();
333
+ const fileSystem = dependencies.fileSystem || fs;
334
+ const profile = normalizeString(options.profile) || 'standard';
335
+ if (!RELEASE_PROFILES.has(profile)) {
336
+ throw new Error(`Invalid release profile "${profile}". Expected one of: ${Array.from(RELEASE_PROFILES).join(', ')}`);
337
+ }
338
+
339
+ const steps = [];
340
+ steps.push({
341
+ id: 'npm-pack-dry-run',
342
+ name: 'npm pack --dry-run',
343
+ command: 'npm',
344
+ args: ['pack', '--dry-run'],
345
+ required: true
346
+ });
347
+
348
+ const weeklySummaryPath = path.join(projectPath, '.sce', 'reports', 'release-evidence', 'release-ops-weekly-summary.json');
349
+ const hasWeeklySummary = await fileSystem.pathExists(weeklySummaryPath);
350
+ steps.push({
351
+ id: 'release-weekly-ops-gate',
352
+ name: 'release weekly ops gate',
353
+ command: 'node',
354
+ args: ['scripts/release-weekly-ops-gate.js'],
355
+ required: true,
356
+ enabled: hasWeeklySummary,
357
+ skip_reason: hasWeeklySummary ? '' : '.sce/reports/release-evidence/release-ops-weekly-summary.json not found'
358
+ });
359
+
360
+ const releaseEvidenceDir = path.join(projectPath, '.sce', 'reports', 'release-evidence');
361
+ const hasReleaseEvidenceDir = await fileSystem.pathExists(releaseEvidenceDir);
362
+ steps.push({
363
+ id: 'release-asset-integrity',
364
+ name: 'release asset integrity',
365
+ command: 'node',
366
+ args: ['scripts/release-asset-integrity-check.js'],
367
+ required: true,
368
+ enabled: hasReleaseEvidenceDir,
369
+ skip_reason: hasReleaseEvidenceDir ? '' : '.sce/reports/release-evidence directory not found'
370
+ });
371
+
372
+ const handoffManifest = path.join(projectPath, 'docs', 'handoffs', 'handoff-manifest.json');
373
+ const hasHandoffManifest = await fileSystem.pathExists(handoffManifest);
374
+ steps.push({
375
+ id: 'scene-package-publish-batch-dry-run',
376
+ name: 'scene package publish-batch dry-run (ontology gate)',
377
+ command: 'node',
378
+ args: [
379
+ 'bin/sce.js',
380
+ 'scene',
381
+ 'package-publish-batch',
382
+ '--manifest',
383
+ 'docs/handoffs/handoff-manifest.json',
384
+ '--dry-run',
385
+ '--ontology-min-average-score',
386
+ '70',
387
+ '--ontology-min-valid-rate',
388
+ '100',
389
+ '--json'
390
+ ],
391
+ required: true,
392
+ enabled: hasHandoffManifest,
393
+ skip_reason: hasHandoffManifest ? '' : 'docs/handoffs/handoff-manifest.json not found'
394
+ });
395
+
396
+ steps.push({
397
+ id: 'handoff-capability-matrix-gate',
398
+ name: 'handoff capability matrix gate',
399
+ command: 'node',
400
+ args: [
401
+ 'bin/sce.js',
402
+ 'auto',
403
+ 'handoff',
404
+ 'capability-matrix',
405
+ '--manifest',
406
+ 'docs/handoffs/handoff-manifest.json',
407
+ '--profile',
408
+ 'moqui',
409
+ '--fail-on-gap',
410
+ '--json'
411
+ ],
412
+ required: true,
413
+ enabled: hasHandoffManifest,
414
+ skip_reason: hasHandoffManifest ? '' : 'docs/handoffs/handoff-manifest.json not found'
415
+ });
416
+
417
+ return steps;
418
+ }
419
+
420
+ async function writeStudioReport(projectPath, relativePath, payload, fileSystem = fs) {
421
+ const absolutePath = path.join(projectPath, relativePath);
422
+ await fileSystem.ensureDir(path.dirname(absolutePath));
423
+ await fileSystem.writeJson(absolutePath, payload, { spaces: 2 });
424
+ }
425
+
426
+ async function ensureStudioDirectories(paths, fileSystem = fs) {
427
+ await fileSystem.ensureDir(paths.jobsDir);
428
+ await fileSystem.ensureDir(paths.eventsDir);
429
+ }
430
+
431
+ async function writeLatestJob(paths, jobId, fileSystem = fs) {
432
+ await fileSystem.writeJson(paths.latestFile, {
433
+ job_id: jobId,
434
+ updated_at: nowIso()
435
+ }, { spaces: 2 });
436
+ }
437
+
438
+ async function readLatestJob(paths, fileSystem = fs) {
439
+ const exists = await fileSystem.pathExists(paths.latestFile);
440
+ if (!exists) {
441
+ return null;
442
+ }
443
+
444
+ const payload = await fileSystem.readJson(paths.latestFile);
445
+ const jobId = normalizeString(payload.job_id);
446
+ return jobId || null;
447
+ }
448
+
449
+ function getJobFilePath(paths, jobId) {
450
+ return path.join(paths.jobsDir, `${jobId}.json`);
451
+ }
452
+
453
+ function getEventLogFilePath(paths, jobId) {
454
+ return path.join(paths.eventsDir, `${jobId}.jsonl`);
455
+ }
456
+
457
+ async function saveJob(paths, job, fileSystem = fs) {
458
+ const jobFile = getJobFilePath(paths, job.job_id);
459
+ await fileSystem.writeJson(jobFile, job, { spaces: 2 });
460
+ }
461
+
462
+ async function appendStudioEvent(paths, job, eventType, metadata = {}, fileSystem = fs) {
463
+ const event = {
464
+ api_version: STUDIO_EVENT_API_VERSION,
465
+ event_id: `evt-${Date.now()}-${crypto.randomBytes(2).toString('hex')}`,
466
+ job_id: job.job_id,
467
+ event_type: eventType,
468
+ timestamp: nowIso(),
469
+ metadata
470
+ };
471
+ const eventLine = `${JSON.stringify(event)}\n`;
472
+ const eventFile = getEventLogFilePath(paths, job.job_id);
473
+ await fileSystem.appendFile(eventFile, eventLine, 'utf8');
474
+ }
475
+
476
+ async function readStudioEvents(paths, jobId, options = {}, fileSystem = fs) {
477
+ const { limit = 50 } = options;
478
+ const eventFile = getEventLogFilePath(paths, jobId);
479
+ const exists = await fileSystem.pathExists(eventFile);
480
+ if (!exists) {
481
+ return [];
482
+ }
483
+
484
+ const content = await fileSystem.readFile(eventFile, 'utf8');
485
+ const lines = content
486
+ .split(/\r?\n/)
487
+ .map((line) => line.trim())
488
+ .filter(Boolean);
489
+
490
+ const parsed = [];
491
+ for (const line of lines) {
492
+ try {
493
+ const payload = JSON.parse(line);
494
+ parsed.push(payload);
495
+ } catch (_error) {
496
+ // Ignore malformed lines to keep event stream robust.
497
+ }
498
+ }
499
+
500
+ if (limit <= 0) {
501
+ return parsed;
502
+ }
503
+ return parsed.slice(-limit);
504
+ }
505
+
506
+ async function loadJob(paths, jobId, fileSystem = fs) {
507
+ const jobFile = getJobFilePath(paths, jobId);
508
+ const exists = await fileSystem.pathExists(jobFile);
509
+ if (!exists) {
510
+ throw new Error(`Studio job not found: ${jobId}`);
511
+ }
512
+ return fileSystem.readJson(jobFile);
513
+ }
514
+
515
+ function resolveRequestedJobId(options, latestJobId) {
516
+ const requested = normalizeString(options.job);
517
+ if (requested) {
518
+ return requested;
519
+ }
520
+ return latestJobId;
521
+ }
522
+
523
+ function buildProgress(job) {
524
+ const completed = STAGE_ORDER.filter((stageName) => {
525
+ const stage = job.stages && job.stages[stageName];
526
+ return stage && stage.status === 'completed';
527
+ }).length;
528
+
529
+ return {
530
+ completed,
531
+ total: STAGE_ORDER.length,
532
+ percent: Number(((completed / STAGE_ORDER.length) * 100).toFixed(2))
533
+ };
534
+ }
535
+
536
+ function resolveNextAction(job) {
537
+ if (job.status === 'rolled_back') {
538
+ return 'sce studio plan --from-chat <session>';
539
+ }
540
+ if (!job.stages.plan || job.stages.plan.status !== 'completed') {
541
+ return `sce studio plan --from-chat <session> --job ${job.job_id}`;
542
+ }
543
+ if (!job.stages.generate || job.stages.generate.status !== 'completed') {
544
+ return `sce studio generate --scene <scene-id> --job ${job.job_id}`;
545
+ }
546
+ if (!job.stages.apply || job.stages.apply.status !== 'completed') {
547
+ const patchBundleId = job.artifacts.patch_bundle_id || '<patch-bundle-id>';
548
+ return `sce studio apply --patch-bundle ${patchBundleId} --job ${job.job_id}`;
549
+ }
550
+ if (!job.stages.verify || job.stages.verify.status !== 'completed') {
551
+ return `sce studio verify --profile standard --job ${job.job_id}`;
552
+ }
553
+ if (!job.stages.release || job.stages.release.status !== 'completed') {
554
+ return `sce studio release --channel dev --job ${job.job_id}`;
555
+ }
556
+ return 'complete';
557
+ }
558
+
559
+ function printStudioPayload(payload, options = {}) {
560
+ if (options.json) {
561
+ console.log(JSON.stringify(payload, null, 2));
562
+ return;
563
+ }
564
+
565
+ console.log(chalk.blue(`Studio job: ${payload.job_id}`));
566
+ console.log(` Status: ${payload.status}`);
567
+ console.log(` Progress: ${payload.progress.completed}/${payload.progress.total} (${payload.progress.percent}%)`);
568
+ console.log(` Next: ${payload.next_action}`);
569
+ }
570
+
571
+ function ensureStageCompleted(job, stageName, metadata = {}) {
572
+ if (!job.stages || !job.stages[stageName]) {
573
+ job.stages = job.stages || createStageState();
574
+ job.stages[stageName] = { status: 'pending', completed_at: null, metadata: {} };
575
+ }
576
+
577
+ job.stages[stageName] = {
578
+ status: 'completed',
579
+ completed_at: nowIso(),
580
+ metadata
581
+ };
582
+ }
583
+
584
+ function isStageCompleted(job, stageName) {
585
+ return Boolean(job && job.stages && job.stages[stageName] && job.stages[stageName].status === 'completed');
586
+ }
587
+
588
+ function ensureStagePrerequisite(job, stageName, prerequisiteStage) {
589
+ if (!isStageCompleted(job, prerequisiteStage)) {
590
+ throw new Error(`Cannot run studio ${stageName}: stage "${prerequisiteStage}" is not completed`);
591
+ }
592
+ }
593
+
594
+ function ensureNotRolledBack(job, stageName) {
595
+ if (job.status === 'rolled_back') {
596
+ throw new Error(`Cannot run studio ${stageName}: job ${job.job_id} is rolled back`);
597
+ }
598
+ }
599
+
600
+ function buildCommandPayload(mode, job) {
601
+ return {
602
+ mode,
603
+ success: true,
604
+ job_id: job.job_id,
605
+ status: job.status,
606
+ progress: buildProgress(job),
607
+ next_action: resolveNextAction(job),
608
+ artifacts: { ...job.artifacts }
609
+ };
610
+ }
611
+
612
+ async function runStudioPlanCommand(options = {}, dependencies = {}) {
613
+ const projectPath = dependencies.projectPath || process.cwd();
614
+ const fileSystem = dependencies.fileSystem || fs;
615
+ const fromChat = normalizeString(options.fromChat);
616
+
617
+ if (!fromChat) {
618
+ throw new Error('--from-chat is required');
619
+ }
620
+
621
+ const paths = resolveStudioPaths(projectPath);
622
+ await ensureStudioDirectories(paths, fileSystem);
623
+
624
+ const jobId = normalizeString(options.job) || createJobId();
625
+ const now = nowIso();
626
+ const stages = createStageState();
627
+ stages.plan = {
628
+ status: 'completed',
629
+ completed_at: now,
630
+ metadata: {
631
+ from_chat: fromChat
632
+ }
633
+ };
634
+
635
+ const job = {
636
+ api_version: STUDIO_JOB_API_VERSION,
637
+ job_id: jobId,
638
+ created_at: now,
639
+ updated_at: now,
640
+ status: 'planned',
641
+ source: {
642
+ from_chat: fromChat,
643
+ goal: normalizeString(options.goal) || null
644
+ },
645
+ scene: {
646
+ id: null
647
+ },
648
+ target: normalizeString(options.target) || 'default',
649
+ stages,
650
+ artifacts: {
651
+ patch_bundle_id: null,
652
+ verify_report: null,
653
+ release_ref: null
654
+ }
655
+ };
656
+
657
+ await saveJob(paths, job, fileSystem);
658
+ await appendStudioEvent(paths, job, 'stage.plan.completed', {
659
+ from_chat: fromChat,
660
+ target: job.target
661
+ }, fileSystem);
662
+ await writeLatestJob(paths, jobId, fileSystem);
663
+
664
+ const payload = buildCommandPayload('studio-plan', job);
665
+ printStudioPayload(payload, options);
666
+ return payload;
667
+ }
668
+
669
+ async function runStudioGenerateCommand(options = {}, dependencies = {}) {
670
+ const projectPath = dependencies.projectPath || process.cwd();
671
+ const fileSystem = dependencies.fileSystem || fs;
672
+ const sceneId = normalizeString(options.scene);
673
+ if (!sceneId) {
674
+ throw new Error('--scene is required');
675
+ }
676
+
677
+ const paths = resolveStudioPaths(projectPath);
678
+ await ensureStudioDirectories(paths, fileSystem);
679
+ const latestJobId = await readLatestJob(paths, fileSystem);
680
+ const jobId = resolveRequestedJobId(options, latestJobId);
681
+ if (!jobId) {
682
+ throw new Error('No studio job found. Run: sce studio plan --from-chat <session>');
683
+ }
684
+
685
+ const job = await loadJob(paths, jobId, fileSystem);
686
+ ensureNotRolledBack(job, 'generate');
687
+ ensureStagePrerequisite(job, 'generate', 'plan');
688
+ const patchBundleId = normalizeString(options.patchBundle) || `patch-${sceneId}-${Date.now()}`;
689
+
690
+ job.scene = job.scene || {};
691
+ job.scene.id = sceneId;
692
+ job.target = normalizeString(options.target) || job.target || 'default';
693
+ job.status = 'generated';
694
+ job.artifacts = job.artifacts || {};
695
+ job.artifacts.patch_bundle_id = patchBundleId;
696
+ job.updated_at = nowIso();
697
+
698
+ ensureStageCompleted(job, 'generate', {
699
+ scene_id: sceneId,
700
+ target: job.target,
701
+ patch_bundle_id: patchBundleId
702
+ });
703
+
704
+ await saveJob(paths, job, fileSystem);
705
+ await appendStudioEvent(paths, job, 'stage.generate.completed', {
706
+ scene_id: sceneId,
707
+ target: job.target,
708
+ patch_bundle_id: patchBundleId
709
+ }, fileSystem);
710
+ await writeLatestJob(paths, jobId, fileSystem);
711
+
712
+ const payload = buildCommandPayload('studio-generate', job);
713
+ printStudioPayload(payload, options);
714
+ return payload;
715
+ }
716
+
717
+ async function runStudioApplyCommand(options = {}, dependencies = {}) {
718
+ const projectPath = dependencies.projectPath || process.cwd();
719
+ const fileSystem = dependencies.fileSystem || fs;
720
+ const paths = resolveStudioPaths(projectPath);
721
+ await ensureStudioDirectories(paths, fileSystem);
722
+
723
+ const latestJobId = await readLatestJob(paths, fileSystem);
724
+ const jobId = resolveRequestedJobId(options, latestJobId);
725
+ if (!jobId) {
726
+ throw new Error('No studio job found. Run: sce studio plan --from-chat <session>');
727
+ }
728
+
729
+ const job = await loadJob(paths, jobId, fileSystem);
730
+ ensureNotRolledBack(job, 'apply');
731
+ ensureStagePrerequisite(job, 'apply', 'generate');
732
+ const authResult = await ensureStudioAuthorization('apply', options, {
733
+ projectPath,
734
+ fileSystem,
735
+ env: dependencies.env,
736
+ authSecret: dependencies.authSecret
737
+ });
738
+ const patchBundleId = normalizeString(options.patchBundle) || normalizeString(job.artifacts.patch_bundle_id);
739
+ if (!patchBundleId) {
740
+ throw new Error('--patch-bundle is required (or generate stage must provide one)');
741
+ }
742
+
743
+ job.status = 'applied';
744
+ job.artifacts = job.artifacts || {};
745
+ job.artifacts.patch_bundle_id = patchBundleId;
746
+ job.updated_at = nowIso();
747
+
748
+ ensureStageCompleted(job, 'apply', {
749
+ patch_bundle_id: patchBundleId,
750
+ auth_required: authResult.required
751
+ });
752
+
753
+ await saveJob(paths, job, fileSystem);
754
+ await appendStudioEvent(paths, job, 'stage.apply.completed', {
755
+ patch_bundle_id: patchBundleId,
756
+ auth_required: authResult.required
757
+ }, fileSystem);
758
+ await writeLatestJob(paths, jobId, fileSystem);
759
+
760
+ const payload = buildCommandPayload('studio-apply', job);
761
+ printStudioPayload(payload, options);
762
+ return payload;
763
+ }
764
+
765
+ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
766
+ const projectPath = dependencies.projectPath || process.cwd();
767
+ const fileSystem = dependencies.fileSystem || fs;
768
+ const paths = resolveStudioPaths(projectPath);
769
+ await ensureStudioDirectories(paths, fileSystem);
770
+
771
+ const latestJobId = await readLatestJob(paths, fileSystem);
772
+ const jobId = resolveRequestedJobId(options, latestJobId);
773
+ if (!jobId) {
774
+ throw new Error('No studio job found. Run: sce studio plan --from-chat <session>');
775
+ }
776
+
777
+ const profile = normalizeString(options.profile) || 'standard';
778
+ const job = await loadJob(paths, jobId, fileSystem);
779
+ ensureNotRolledBack(job, 'verify');
780
+ ensureStagePrerequisite(job, 'verify', 'apply');
781
+
782
+ const verifyReportPath = `${STUDIO_REPORTS_DIR}/verify-${job.job_id}.json`;
783
+ const verifyStartedAt = nowIso();
784
+ const gateSteps = await buildVerifyGateSteps({ profile }, {
785
+ projectPath,
786
+ fileSystem
787
+ });
788
+ const gateResult = await executeGateSteps(gateSteps, {
789
+ projectPath,
790
+ commandRunner: dependencies.commandRunner,
791
+ env: dependencies.env,
792
+ failOnRequiredSkip: profile === 'strict'
793
+ });
794
+ const verifyCompletedAt = nowIso();
795
+ const verifyReport = {
796
+ mode: 'studio-verify',
797
+ api_version: STUDIO_JOB_API_VERSION,
798
+ job_id: job.job_id,
799
+ profile,
800
+ started_at: verifyStartedAt,
801
+ completed_at: verifyCompletedAt,
802
+ passed: gateResult.passed,
803
+ steps: gateResult.steps
804
+ };
805
+
806
+ await writeStudioReport(projectPath, verifyReportPath, verifyReport, fileSystem);
807
+
808
+ job.artifacts = job.artifacts || {};
809
+ job.artifacts.verify_report = verifyReportPath;
810
+ job.updated_at = verifyCompletedAt;
811
+
812
+ if (!gateResult.passed) {
813
+ job.status = 'verify_failed';
814
+ job.stages.verify = {
815
+ status: 'failed',
816
+ completed_at: null,
817
+ metadata: {
818
+ profile,
819
+ passed: false,
820
+ report: verifyReportPath
821
+ }
822
+ };
823
+ await saveJob(paths, job, fileSystem);
824
+ await appendStudioEvent(paths, job, 'stage.verify.failed', {
825
+ profile,
826
+ report: verifyReportPath
827
+ }, fileSystem);
828
+ await writeLatestJob(paths, jobId, fileSystem);
829
+ throw new Error(`studio verify failed: ${gateResult.steps.filter((step) => step.status === 'failed').map((step) => step.id).join(', ')}`);
830
+ }
831
+
832
+ job.status = 'verified';
833
+ ensureStageCompleted(job, 'verify', {
834
+ profile,
835
+ passed: true,
836
+ report: verifyReportPath
837
+ });
838
+
839
+ await saveJob(paths, job, fileSystem);
840
+ await appendStudioEvent(paths, job, 'stage.verify.completed', {
841
+ profile,
842
+ passed: true,
843
+ report: verifyReportPath
844
+ }, fileSystem);
845
+ await writeLatestJob(paths, jobId, fileSystem);
846
+
847
+ const payload = buildCommandPayload('studio-verify', job);
848
+ printStudioPayload(payload, options);
849
+ return payload;
850
+ }
851
+
852
+ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
853
+ const projectPath = dependencies.projectPath || process.cwd();
854
+ const fileSystem = dependencies.fileSystem || fs;
855
+ const paths = resolveStudioPaths(projectPath);
856
+ await ensureStudioDirectories(paths, fileSystem);
857
+
858
+ const latestJobId = await readLatestJob(paths, fileSystem);
859
+ const jobId = resolveRequestedJobId(options, latestJobId);
860
+ if (!jobId) {
861
+ throw new Error('No studio job found. Run: sce studio plan --from-chat <session>');
862
+ }
863
+
864
+ const channel = normalizeString(options.channel) || 'dev';
865
+ if (!RELEASE_CHANNELS.has(channel)) {
866
+ throw new Error(`Invalid --channel "${channel}". Expected one of: ${Array.from(RELEASE_CHANNELS).join(', ')}`);
867
+ }
868
+
869
+ const job = await loadJob(paths, jobId, fileSystem);
870
+ ensureNotRolledBack(job, 'release');
871
+ ensureStagePrerequisite(job, 'release', 'verify');
872
+ const authResult = await ensureStudioAuthorization('release', options, {
873
+ projectPath,
874
+ fileSystem,
875
+ env: dependencies.env,
876
+ authSecret: dependencies.authSecret
877
+ });
878
+ const releaseRef = normalizeString(options.releaseRef) || `${channel}-${Date.now()}`;
879
+
880
+ const profile = normalizeString(options.profile) || 'standard';
881
+ const releaseReportPath = `${STUDIO_REPORTS_DIR}/release-${job.job_id}.json`;
882
+ const releaseStartedAt = nowIso();
883
+ const gateSteps = await buildReleaseGateSteps({ profile }, {
884
+ projectPath,
885
+ fileSystem
886
+ });
887
+ const gateResult = await executeGateSteps(gateSteps, {
888
+ projectPath,
889
+ commandRunner: dependencies.commandRunner,
890
+ env: dependencies.env,
891
+ failOnRequiredSkip: profile === 'strict'
892
+ });
893
+ const releaseCompletedAt = nowIso();
894
+ const releaseReport = {
895
+ mode: 'studio-release',
896
+ api_version: STUDIO_JOB_API_VERSION,
897
+ job_id: job.job_id,
898
+ profile,
899
+ channel,
900
+ release_ref: releaseRef,
901
+ started_at: releaseStartedAt,
902
+ completed_at: releaseCompletedAt,
903
+ passed: gateResult.passed,
904
+ steps: gateResult.steps
905
+ };
906
+
907
+ await writeStudioReport(projectPath, releaseReportPath, releaseReport, fileSystem);
908
+
909
+ job.artifacts = job.artifacts || {};
910
+ job.artifacts.release_ref = releaseRef;
911
+ job.artifacts.release_report = releaseReportPath;
912
+ job.updated_at = releaseCompletedAt;
913
+
914
+ if (!gateResult.passed) {
915
+ job.status = 'release_failed';
916
+ job.stages.release = {
917
+ status: 'failed',
918
+ completed_at: null,
919
+ metadata: {
920
+ channel,
921
+ release_ref: releaseRef,
922
+ passed: false,
923
+ report: releaseReportPath,
924
+ auth_required: authResult.required
925
+ }
926
+ };
927
+ await saveJob(paths, job, fileSystem);
928
+ await appendStudioEvent(paths, job, 'stage.release.failed', {
929
+ channel,
930
+ release_ref: releaseRef,
931
+ report: releaseReportPath,
932
+ auth_required: authResult.required
933
+ }, fileSystem);
934
+ await writeLatestJob(paths, jobId, fileSystem);
935
+ throw new Error(`studio release failed: ${gateResult.steps.filter((step) => step.status === 'failed').map((step) => step.id).join(', ')}`);
936
+ }
937
+
938
+ job.status = 'released';
939
+ ensureStageCompleted(job, 'release', {
940
+ channel,
941
+ release_ref: releaseRef,
942
+ report: releaseReportPath,
943
+ auth_required: authResult.required
944
+ });
945
+
946
+ await saveJob(paths, job, fileSystem);
947
+ await appendStudioEvent(paths, job, 'stage.release.completed', {
948
+ channel,
949
+ release_ref: releaseRef,
950
+ report: releaseReportPath,
951
+ auth_required: authResult.required
952
+ }, fileSystem);
953
+ await writeLatestJob(paths, jobId, fileSystem);
954
+
955
+ const payload = buildCommandPayload('studio-release', job);
956
+ printStudioPayload(payload, options);
957
+ return payload;
958
+ }
959
+
960
+ async function runStudioResumeCommand(options = {}, dependencies = {}) {
961
+ const projectPath = dependencies.projectPath || process.cwd();
962
+ const fileSystem = dependencies.fileSystem || fs;
963
+ const paths = resolveStudioPaths(projectPath);
964
+ await ensureStudioDirectories(paths, fileSystem);
965
+
966
+ const latestJobId = await readLatestJob(paths, fileSystem);
967
+ const jobId = resolveRequestedJobId(options, latestJobId);
968
+ if (!jobId) {
969
+ throw new Error('No studio job found. Run: sce studio plan --from-chat <session>');
970
+ }
971
+
972
+ const job = await loadJob(paths, jobId, fileSystem);
973
+ const payload = buildCommandPayload('studio-resume', job);
974
+ payload.success = true;
975
+ printStudioPayload(payload, options);
976
+ return payload;
977
+ }
978
+
979
+ async function runStudioRollbackCommand(options = {}, dependencies = {}) {
980
+ const projectPath = dependencies.projectPath || process.cwd();
981
+ const fileSystem = dependencies.fileSystem || fs;
982
+ const paths = resolveStudioPaths(projectPath);
983
+ await ensureStudioDirectories(paths, fileSystem);
984
+
985
+ const latestJobId = await readLatestJob(paths, fileSystem);
986
+ const jobId = resolveRequestedJobId(options, latestJobId);
987
+ if (!jobId) {
988
+ throw new Error('No studio job found. Run: sce studio plan --from-chat <session>');
989
+ }
990
+
991
+ const reason = normalizeString(options.reason) || 'manual-rollback';
992
+ const job = await loadJob(paths, jobId, fileSystem);
993
+ const authResult = await ensureStudioAuthorization('rollback', options, {
994
+ projectPath,
995
+ fileSystem,
996
+ env: dependencies.env,
997
+ authSecret: dependencies.authSecret
998
+ });
999
+ if (!isStageCompleted(job, 'apply')) {
1000
+ throw new Error(`Cannot rollback studio job ${job.job_id}: apply stage is not completed`);
1001
+ }
1002
+
1003
+ job.status = 'rolled_back';
1004
+ job.updated_at = nowIso();
1005
+ job.rollback = {
1006
+ reason,
1007
+ rolled_back_at: job.updated_at,
1008
+ auth_required: authResult.required
1009
+ };
1010
+
1011
+ await saveJob(paths, job, fileSystem);
1012
+ await appendStudioEvent(paths, job, 'job.rolled_back', {
1013
+ reason
1014
+ }, fileSystem);
1015
+ await writeLatestJob(paths, jobId, fileSystem);
1016
+
1017
+ const payload = buildCommandPayload('studio-rollback', job);
1018
+ payload.rollback = { ...job.rollback };
1019
+ printStudioPayload(payload, options);
1020
+ return payload;
1021
+ }
1022
+
1023
+ function normalizePositiveInteger(value, fallback) {
1024
+ const parsed = Number.parseInt(String(value), 10);
1025
+ if (!Number.isFinite(parsed) || parsed <= 0) {
1026
+ return fallback;
1027
+ }
1028
+ return parsed;
1029
+ }
1030
+
1031
+ function printStudioEventsPayload(payload, options = {}) {
1032
+ if (options.json) {
1033
+ console.log(JSON.stringify(payload, null, 2));
1034
+ return;
1035
+ }
1036
+
1037
+ console.log(chalk.blue(`Studio events: ${payload.job_id}`));
1038
+ console.log(` Count: ${payload.events.length}`);
1039
+ for (const event of payload.events) {
1040
+ console.log(` - ${event.timestamp} ${event.event_type}`);
1041
+ }
1042
+ }
1043
+
1044
+ async function runStudioEventsCommand(options = {}, dependencies = {}) {
1045
+ const projectPath = dependencies.projectPath || process.cwd();
1046
+ const fileSystem = dependencies.fileSystem || fs;
1047
+ const paths = resolveStudioPaths(projectPath);
1048
+ await ensureStudioDirectories(paths, fileSystem);
1049
+
1050
+ const latestJobId = await readLatestJob(paths, fileSystem);
1051
+ const jobId = resolveRequestedJobId(options, latestJobId);
1052
+ if (!jobId) {
1053
+ throw new Error('No studio job found. Run: sce studio plan --from-chat <session>');
1054
+ }
1055
+
1056
+ const limit = normalizePositiveInteger(options.limit, 50);
1057
+ const events = await readStudioEvents(paths, jobId, { limit }, fileSystem);
1058
+
1059
+ const payload = {
1060
+ mode: 'studio-events',
1061
+ success: true,
1062
+ job_id: jobId,
1063
+ limit,
1064
+ events
1065
+ };
1066
+ printStudioEventsPayload(payload, options);
1067
+ return payload;
1068
+ }
1069
+
1070
+ async function runStudioCommand(handler, options) {
1071
+ try {
1072
+ await handler(options);
1073
+ } catch (error) {
1074
+ console.error(chalk.red(`Studio command failed: ${error.message}`));
1075
+ process.exitCode = 1;
1076
+ }
1077
+ }
1078
+
1079
+ function registerStudioCommands(program) {
1080
+ const studio = program
1081
+ .command('studio')
1082
+ .description('Run studio chat-to-release orchestration workflow');
1083
+
1084
+ studio
1085
+ .command('plan')
1086
+ .description('Create/refresh a studio plan job from chat context')
1087
+ .requiredOption('--from-chat <session>', 'Chat session identifier or transcript reference')
1088
+ .option('--goal <goal>', 'Optional goal summary')
1089
+ .option('--target <target>', 'Target integration profile', 'default')
1090
+ .option('--job <job-id>', 'Reuse an explicit studio job id')
1091
+ .option('--json', 'Print machine-readable JSON output')
1092
+ .action(async (options) => runStudioCommand(runStudioPlanCommand, options));
1093
+
1094
+ studio
1095
+ .command('generate')
1096
+ .description('Generate patch bundle metadata for a planned studio job')
1097
+ .requiredOption('--scene <scene-id>', 'Scene identifier to generate')
1098
+ .option('--target <target>', 'Target integration profile override')
1099
+ .option('--patch-bundle <id>', 'Explicit patch bundle id')
1100
+ .option('--job <job-id>', 'Studio job id (defaults to latest)')
1101
+ .option('--json', 'Print machine-readable JSON output')
1102
+ .action(async (options) => runStudioCommand(runStudioGenerateCommand, options));
1103
+
1104
+ studio
1105
+ .command('apply')
1106
+ .description('Apply generated patch bundle metadata to studio job')
1107
+ .option('--patch-bundle <id>', 'Patch bundle identifier (defaults to generated artifact)')
1108
+ .option('--auth-password <password>', 'Authorization password for protected apply action')
1109
+ .option('--require-auth', 'Require authorization even when policy is advisory')
1110
+ .option('--job <job-id>', 'Studio job id (defaults to latest)')
1111
+ .option('--json', 'Print machine-readable JSON output')
1112
+ .action(async (options) => runStudioCommand(runStudioApplyCommand, options));
1113
+
1114
+ studio
1115
+ .command('verify')
1116
+ .description('Record verification stage for studio job')
1117
+ .option('--profile <profile>', 'Verification profile', 'standard')
1118
+ .option('--job <job-id>', 'Studio job id (defaults to latest)')
1119
+ .option('--json', 'Print machine-readable JSON output')
1120
+ .action(async (options) => runStudioCommand(runStudioVerifyCommand, options));
1121
+
1122
+ studio
1123
+ .command('release')
1124
+ .description('Record release stage for studio job')
1125
+ .option('--channel <channel>', 'Release channel (dev|prod)', 'dev')
1126
+ .option('--profile <profile>', 'Release gate profile', 'standard')
1127
+ .option('--auth-password <password>', 'Authorization password for protected release action')
1128
+ .option('--require-auth', 'Require authorization even when policy is advisory')
1129
+ .option('--release-ref <ref>', 'Explicit release reference/tag')
1130
+ .option('--job <job-id>', 'Studio job id (defaults to latest)')
1131
+ .option('--json', 'Print machine-readable JSON output')
1132
+ .action(async (options) => runStudioCommand(runStudioReleaseCommand, options));
1133
+
1134
+ studio
1135
+ .command('resume')
1136
+ .description('Inspect current studio job and next action')
1137
+ .option('--job <job-id>', 'Studio job id (defaults to latest)')
1138
+ .option('--json', 'Print machine-readable JSON output')
1139
+ .action(async (options) => runStudioCommand(runStudioResumeCommand, options));
1140
+
1141
+ studio
1142
+ .command('events')
1143
+ .description('Show studio job event stream')
1144
+ .option('--job <job-id>', 'Studio job id (defaults to latest)')
1145
+ .option('--limit <number>', 'Maximum number of recent events to return', '50')
1146
+ .option('--json', 'Print machine-readable JSON output')
1147
+ .action(async (options) => runStudioCommand(runStudioEventsCommand, options));
1148
+
1149
+ studio
1150
+ .command('rollback')
1151
+ .description('Rollback a studio job after apply/release')
1152
+ .option('--job <job-id>', 'Studio job id (defaults to latest)')
1153
+ .option('--reason <reason>', 'Rollback reason')
1154
+ .option('--auth-password <password>', 'Authorization password for protected rollback action')
1155
+ .option('--require-auth', 'Require authorization even when policy is advisory')
1156
+ .option('--json', 'Print machine-readable JSON output')
1157
+ .action(async (options) => runStudioCommand(runStudioRollbackCommand, options));
1158
+ }
1159
+
1160
+ module.exports = {
1161
+ STUDIO_JOB_API_VERSION,
1162
+ STUDIO_EVENT_API_VERSION,
1163
+ STAGE_ORDER,
1164
+ RELEASE_CHANNELS,
1165
+ resolveStudioPaths,
1166
+ createJobId,
1167
+ createStageState,
1168
+ readStudioEvents,
1169
+ readLatestJob,
1170
+ executeGateSteps,
1171
+ loadStudioSecurityPolicy,
1172
+ ensureStudioAuthorization,
1173
+ buildVerifyGateSteps,
1174
+ buildReleaseGateSteps,
1175
+ resolveNextAction,
1176
+ buildProgress,
1177
+ runStudioPlanCommand,
1178
+ runStudioGenerateCommand,
1179
+ runStudioApplyCommand,
1180
+ runStudioVerifyCommand,
1181
+ runStudioReleaseCommand,
1182
+ runStudioRollbackCommand,
1183
+ runStudioEventsCommand,
1184
+ runStudioResumeCommand,
1185
+ registerStudioCommands
1186
+ };