scene-capability-engine 3.3.18 → 3.3.22

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,109 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+ const { SessionStore } = require('./session-store');
4
+
5
+ function normalizeString(value) {
6
+ if (typeof value !== 'string') {
7
+ return '';
8
+ }
9
+ return value.trim();
10
+ }
11
+
12
+ async function loadLatestStudioJob(projectPath, fileSystem = fs) {
13
+ const latestPath = path.join(projectPath, '.sce', 'studio', 'latest-job.json');
14
+ if (!await fileSystem.pathExists(latestPath)) {
15
+ return null;
16
+ }
17
+
18
+ let latestPayload;
19
+ try {
20
+ latestPayload = await fileSystem.readJson(latestPath);
21
+ } catch (_error) {
22
+ return null;
23
+ }
24
+
25
+ const jobId = normalizeString(latestPayload && latestPayload.job_id);
26
+ if (!jobId) {
27
+ return null;
28
+ }
29
+
30
+ const jobPath = path.join(projectPath, '.sce', 'studio', 'jobs', `${jobId}.json`);
31
+ if (!await fileSystem.pathExists(jobPath)) {
32
+ return null;
33
+ }
34
+
35
+ try {
36
+ return await fileSystem.readJson(jobPath);
37
+ } catch (_error) {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ async function resolveSpecSceneBinding(options = {}, dependencies = {}) {
43
+ const projectPath = dependencies.projectPath || process.cwd();
44
+ const fileSystem = dependencies.fileSystem || fs;
45
+ const sessionStore = dependencies.sessionStore || new SessionStore(projectPath);
46
+ const explicitSceneId = normalizeString(options.sceneId || options.scene);
47
+ const allowNoScene = options.allowNoScene !== false;
48
+
49
+ if (explicitSceneId) {
50
+ const active = await sessionStore.getActiveSceneSession(explicitSceneId);
51
+ if (!active) {
52
+ throw new Error(`No active scene session for scene "${explicitSceneId}". Run "sce studio plan --scene ${explicitSceneId} --from-chat <session>" first.`);
53
+ }
54
+ return {
55
+ source: 'explicit',
56
+ scene_id: explicitSceneId,
57
+ scene_cycle: active.scene_cycle,
58
+ scene_session_id: active.session.session_id,
59
+ scene_session: active.session
60
+ };
61
+ }
62
+
63
+ const studioJob = await loadLatestStudioJob(projectPath, fileSystem);
64
+ const studioSceneId = normalizeString(studioJob && studioJob.scene && studioJob.scene.id);
65
+ const studioSceneSessionId = normalizeString(studioJob && studioJob.session && studioJob.session.scene_session_id);
66
+ if (studioSceneId && studioSceneSessionId) {
67
+ try {
68
+ const studioSession = await sessionStore.getSession(studioSceneSessionId);
69
+ if (studioSession && studioSession.status === 'active') {
70
+ return {
71
+ source: 'studio-latest',
72
+ scene_id: studioSceneId,
73
+ scene_cycle: studioSession.scene && studioSession.scene.cycle ? studioSession.scene.cycle : null,
74
+ scene_session_id: studioSession.session_id,
75
+ scene_session: studioSession
76
+ };
77
+ }
78
+ } catch (_error) {
79
+ // fall through to active scene scan
80
+ }
81
+ }
82
+
83
+ const activeScenes = await sessionStore.listActiveSceneSessions();
84
+ if (activeScenes.length === 1) {
85
+ return {
86
+ source: 'active-scene-auto',
87
+ scene_id: activeScenes[0].scene_id,
88
+ scene_cycle: activeScenes[0].scene_cycle,
89
+ scene_session_id: activeScenes[0].session.session_id,
90
+ scene_session: activeScenes[0].session
91
+ };
92
+ }
93
+
94
+ if (activeScenes.length > 1) {
95
+ const sceneIds = activeScenes.map((item) => item.scene_id).join(', ');
96
+ throw new Error(`Multiple active scene sessions detected (${sceneIds}). Use --scene <scene-id> to bind spec session explicitly.`);
97
+ }
98
+
99
+ if (allowNoScene) {
100
+ return null;
101
+ }
102
+
103
+ throw new Error('No active scene session found. Run "sce studio plan --scene <scene-id> --from-chat <session>" first.');
104
+ }
105
+
106
+ module.exports = {
107
+ resolveSpecSceneBinding,
108
+ loadLatestStudioJob
109
+ };
@@ -4,6 +4,9 @@ const { SteeringContract, normalizeToolName } = require('./steering-contract');
4
4
 
5
5
  const SESSION_SCHEMA_VERSION = '1.0';
6
6
  const SESSION_DIR = path.join('.sce', 'sessions');
7
+ const SESSION_GOVERNANCE_SCHEMA_VERSION = '1.0';
8
+ const SESSION_GOVERNANCE_DIR = path.join('.sce', 'session-governance');
9
+ const SESSION_SCENE_INDEX_FILE = 'scene-index.json';
7
10
 
8
11
  function toRelativePosix(workspaceRoot, absolutePath) {
9
12
  return path.relative(workspaceRoot, absolutePath).replace(/\\/g, '/');
@@ -25,10 +28,26 @@ function generateSessionId() {
25
28
  return `sess-${yyyy}${mm}${dd}-${hh}${mi}${ss}-${rand}`;
26
29
  }
27
30
 
31
+ function nowIso() {
32
+ return new Date().toISOString();
33
+ }
34
+
35
+ function safeSceneId(value) {
36
+ const normalized = `${value || ''}`.trim().replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
37
+ return normalized || 'scene';
38
+ }
39
+
40
+ function nextSnapshotId(session) {
41
+ const snapshots = Array.isArray(session && session.snapshots) ? session.snapshots : [];
42
+ return `snap-${snapshots.length + 1}`;
43
+ }
44
+
28
45
  class SessionStore {
29
46
  constructor(workspaceRoot, steeringContract = null) {
30
47
  this._workspaceRoot = workspaceRoot;
31
48
  this._sessionsDir = path.join(workspaceRoot, SESSION_DIR);
49
+ this._sessionGovernanceDir = path.join(workspaceRoot, SESSION_GOVERNANCE_DIR);
50
+ this._sceneIndexPath = path.join(this._sessionGovernanceDir, SESSION_SCENE_INDEX_FILE);
32
51
  this._steeringContract = steeringContract || new SteeringContract(workspaceRoot);
33
52
  }
34
53
 
@@ -38,7 +57,7 @@ class SessionStore {
38
57
  const objective = `${options.objective || ''}`.trim();
39
58
  const requestedId = safeSessionId(options.sessionId);
40
59
  const sessionId = requestedId || generateSessionId();
41
- const now = new Date().toISOString();
60
+ const now = nowIso();
42
61
 
43
62
  await this._steeringContract.ensureContract();
44
63
  const steeringPayload = await this._steeringContract.buildCompilePayload(tool, agentVersion);
@@ -86,7 +105,7 @@ class SessionStore {
86
105
 
87
106
  async resumeSession(sessionRef = 'latest', options = {}) {
88
107
  const { sessionId, session } = await this._resolveSession(sessionRef);
89
- const now = new Date().toISOString();
108
+ const now = nowIso();
90
109
  const status = `${options.status || 'active'}`.trim() || 'active';
91
110
 
92
111
  session.status = status;
@@ -104,7 +123,7 @@ class SessionStore {
104
123
 
105
124
  async snapshotSession(sessionRef = 'latest', options = {}) {
106
125
  const { sessionId, session } = await this._resolveSession(sessionRef);
107
- const now = new Date().toISOString();
126
+ const now = nowIso();
108
127
  const summary = `${options.summary || ''}`.trim();
109
128
  const status = options.status ? `${options.status}`.trim() : session.status;
110
129
  const payload = options.payload == null ? null : options.payload;
@@ -167,6 +186,414 @@ class SessionStore {
167
186
  return records;
168
187
  }
169
188
 
189
+ async beginSceneSession(options = {}) {
190
+ const sceneId = `${options.sceneId || ''}`.trim();
191
+ if (!sceneId) {
192
+ throw new Error('sceneId is required for beginSceneSession');
193
+ }
194
+
195
+ const forceNew = options.forceNew === true;
196
+ const sceneIndex = await this._readSceneIndex();
197
+ const sceneRecord = sceneIndex.scenes[sceneId] || {
198
+ scene_id: sceneId,
199
+ active_session_id: null,
200
+ active_cycle: null,
201
+ latest_completed_session_id: null,
202
+ last_cycle: 0,
203
+ cycles: []
204
+ };
205
+
206
+ if (!forceNew && sceneRecord.active_session_id) {
207
+ try {
208
+ const activeSession = await this.getSession(sceneRecord.active_session_id);
209
+ if (
210
+ activeSession &&
211
+ activeSession.status === 'active' &&
212
+ activeSession.scene &&
213
+ activeSession.scene.id === sceneId
214
+ ) {
215
+ return {
216
+ created_new: false,
217
+ scene_cycle: Number(sceneRecord.active_cycle || 1),
218
+ session: activeSession,
219
+ scene_record: sceneRecord
220
+ };
221
+ }
222
+ } catch (_error) {
223
+ // stale active pointer; continue and create a new one
224
+ }
225
+ }
226
+
227
+ const cycle = Number(sceneRecord.last_cycle || 0) + 1;
228
+ const requestedSessionId = safeSessionId(options.sessionId);
229
+ const generatedScenePrefix = `scene-${safeSceneId(sceneId)}-c${cycle}`;
230
+ const candidateSessionId = requestedSessionId || `${generatedScenePrefix}-${Math.random().toString(36).slice(2, 8)}`;
231
+
232
+ const session = await this.startSession({
233
+ tool: options.tool || 'generic',
234
+ agentVersion: options.agentVersion || null,
235
+ objective: options.objective || `Scene ${sceneId} cycle ${cycle}`,
236
+ sessionId: candidateSessionId
237
+ });
238
+
239
+ const now = nowIso();
240
+ session.scene = {
241
+ id: sceneId,
242
+ role: 'primary',
243
+ cycle,
244
+ state: 'active'
245
+ };
246
+ session.children = session.children || {};
247
+ session.children.spec_sessions = Array.isArray(session.children.spec_sessions)
248
+ ? session.children.spec_sessions
249
+ : [];
250
+ session.updated_at = now;
251
+ session.timeline = Array.isArray(session.timeline) ? session.timeline : [];
252
+ session.timeline.push({
253
+ at: now,
254
+ event: 'scene_session_started',
255
+ detail: {
256
+ scene_id: sceneId,
257
+ cycle
258
+ }
259
+ });
260
+ await this._writeSession(session.session_id, session);
261
+
262
+ sceneRecord.active_session_id = session.session_id;
263
+ sceneRecord.active_cycle = cycle;
264
+ sceneRecord.last_cycle = cycle;
265
+ sceneRecord.updated_at = now;
266
+ sceneRecord.cycles = Array.isArray(sceneRecord.cycles) ? sceneRecord.cycles : [];
267
+ sceneRecord.cycles.push({
268
+ cycle,
269
+ session_id: session.session_id,
270
+ status: 'active',
271
+ started_at: session.started_at,
272
+ completed_at: null
273
+ });
274
+ sceneIndex.scenes[sceneId] = sceneRecord;
275
+ await this._writeSceneIndex(sceneIndex);
276
+
277
+ return {
278
+ created_new: true,
279
+ scene_cycle: cycle,
280
+ session,
281
+ scene_record: sceneRecord
282
+ };
283
+ }
284
+
285
+ async completeSceneSession(sceneId, sessionRef = null, options = {}) {
286
+ const normalizedSceneId = `${sceneId || ''}`.trim();
287
+ if (!normalizedSceneId) {
288
+ throw new Error('sceneId is required for completeSceneSession');
289
+ }
290
+
291
+ const sceneIndex = await this._readSceneIndex();
292
+ const sceneRecord = sceneIndex.scenes[normalizedSceneId];
293
+ if (!sceneRecord) {
294
+ throw new Error(`Scene is not tracked in session governance: ${normalizedSceneId}`);
295
+ }
296
+
297
+ const resolvedSessionRef = `${sessionRef || sceneRecord.active_session_id || ''}`.trim();
298
+ if (!resolvedSessionRef) {
299
+ throw new Error(`No active scene session to complete: ${normalizedSceneId}`);
300
+ }
301
+
302
+ const { sessionId, session } = await this._resolveSession(resolvedSessionRef);
303
+ if (!session.scene || session.scene.id !== normalizedSceneId) {
304
+ throw new Error(`Session ${sessionId} is not bound to scene ${normalizedSceneId}`);
305
+ }
306
+
307
+ const now = nowIso();
308
+ const completionStatus = `${options.status || 'completed'}`.trim() || 'completed';
309
+ const summary = `${options.summary || ''}`.trim();
310
+ const payload = {
311
+ job_id: options.jobId || null,
312
+ release_ref: options.releaseRef || null,
313
+ channel: options.channel || null
314
+ };
315
+
316
+ session.snapshots = Array.isArray(session.snapshots) ? session.snapshots : [];
317
+ session.snapshots.push({
318
+ snapshot_id: nextSnapshotId(session),
319
+ captured_at: now,
320
+ status: completionStatus,
321
+ summary,
322
+ payload
323
+ });
324
+ session.status = completionStatus;
325
+ session.updated_at = now;
326
+ session.scene.state = 'completed';
327
+ session.scene.completed_at = now;
328
+ session.timeline = Array.isArray(session.timeline) ? session.timeline : [];
329
+ session.timeline.push({
330
+ at: now,
331
+ event: 'scene_session_completed',
332
+ detail: {
333
+ scene_id: normalizedSceneId,
334
+ release_ref: payload.release_ref,
335
+ channel: payload.channel
336
+ }
337
+ });
338
+ await this._writeSession(sessionId, session);
339
+
340
+ sceneRecord.cycles = Array.isArray(sceneRecord.cycles) ? sceneRecord.cycles : [];
341
+ for (const cycle of sceneRecord.cycles) {
342
+ if (cycle && cycle.session_id === sessionId) {
343
+ cycle.status = completionStatus;
344
+ cycle.completed_at = now;
345
+ cycle.release_ref = payload.release_ref;
346
+ }
347
+ }
348
+ sceneRecord.active_session_id = null;
349
+ sceneRecord.active_cycle = null;
350
+ sceneRecord.latest_completed_session_id = sessionId;
351
+ sceneRecord.updated_at = now;
352
+ sceneIndex.scenes[normalizedSceneId] = sceneRecord;
353
+ await this._writeSceneIndex(sceneIndex);
354
+
355
+ if (options.autoStartNext === false) {
356
+ return {
357
+ completed_session: session,
358
+ next_session: null,
359
+ next_scene_cycle: null
360
+ };
361
+ }
362
+
363
+ const next = await this.beginSceneSession({
364
+ sceneId: normalizedSceneId,
365
+ tool: session.tool,
366
+ agentVersion: session.agent_version || null,
367
+ objective: options.nextObjective || `Scene ${normalizedSceneId} follow-up cycle`
368
+ });
369
+ next.session.timeline = Array.isArray(next.session.timeline) ? next.session.timeline : [];
370
+ next.session.timeline.push({
371
+ at: nowIso(),
372
+ event: 'scene_session_rollover',
373
+ detail: {
374
+ from_session_id: sessionId,
375
+ scene_id: normalizedSceneId
376
+ }
377
+ });
378
+ await this._writeSession(next.session.session_id, next.session);
379
+
380
+ return {
381
+ completed_session: session,
382
+ next_session: next.session,
383
+ next_scene_cycle: next.scene_cycle
384
+ };
385
+ }
386
+
387
+ async startSpecSession(options = {}) {
388
+ const sceneId = `${options.sceneId || ''}`.trim();
389
+ const specId = `${options.specId || ''}`.trim();
390
+ if (!sceneId) {
391
+ throw new Error('sceneId is required for startSpecSession');
392
+ }
393
+ if (!specId) {
394
+ throw new Error('specId is required for startSpecSession');
395
+ }
396
+
397
+ const sceneIndex = await this._readSceneIndex();
398
+ const sceneRecord = sceneIndex.scenes[sceneId];
399
+ if (!sceneRecord || !sceneRecord.active_session_id) {
400
+ throw new Error(`No active scene session for scene: ${sceneId}`);
401
+ }
402
+
403
+ const parentSession = await this.getSession(sceneRecord.active_session_id);
404
+ const cycle = parentSession && parentSession.scene && parentSession.scene.cycle
405
+ ? Number(parentSession.scene.cycle)
406
+ : Number(sceneRecord.active_cycle || 1);
407
+ const specSessionId = safeSessionId(options.sessionId)
408
+ || `spec-${safeSceneId(specId)}-${Math.random().toString(36).slice(2, 8)}`;
409
+
410
+ const specSession = await this.startSession({
411
+ tool: parentSession.tool || options.tool || 'generic',
412
+ agentVersion: parentSession.agent_version || options.agentVersion || null,
413
+ objective: options.objective || `Spec ${specId} for scene ${sceneId}`,
414
+ sessionId: specSessionId
415
+ });
416
+
417
+ const now = nowIso();
418
+ specSession.scene = {
419
+ id: sceneId,
420
+ role: 'spec',
421
+ cycle,
422
+ parent_session_id: parentSession.session_id,
423
+ spec_id: specId,
424
+ state: 'active'
425
+ };
426
+ specSession.updated_at = now;
427
+ specSession.timeline = Array.isArray(specSession.timeline) ? specSession.timeline : [];
428
+ specSession.timeline.push({
429
+ at: now,
430
+ event: 'spec_session_started',
431
+ detail: {
432
+ scene_id: sceneId,
433
+ spec_id: specId,
434
+ parent_session_id: parentSession.session_id
435
+ }
436
+ });
437
+ await this._writeSession(specSession.session_id, specSession);
438
+
439
+ parentSession.children = parentSession.children || {};
440
+ parentSession.children.spec_sessions = Array.isArray(parentSession.children.spec_sessions)
441
+ ? parentSession.children.spec_sessions
442
+ : [];
443
+ parentSession.children.spec_sessions.push({
444
+ spec_id: specId,
445
+ spec_session_id: specSession.session_id,
446
+ status: 'active',
447
+ started_at: now
448
+ });
449
+ parentSession.updated_at = now;
450
+ parentSession.timeline = Array.isArray(parentSession.timeline) ? parentSession.timeline : [];
451
+ parentSession.timeline.push({
452
+ at: now,
453
+ event: 'spec_session_attached',
454
+ detail: {
455
+ scene_id: sceneId,
456
+ spec_id: specId,
457
+ spec_session_id: specSession.session_id
458
+ }
459
+ });
460
+ await this._writeSession(parentSession.session_id, parentSession);
461
+
462
+ return {
463
+ scene_session: parentSession,
464
+ spec_session: specSession
465
+ };
466
+ }
467
+
468
+ async completeSpecSession(options = {}) {
469
+ const specSessionRef = `${options.specSessionRef || ''}`.trim();
470
+ if (!specSessionRef) {
471
+ throw new Error('specSessionRef is required for completeSpecSession');
472
+ }
473
+
474
+ const now = nowIso();
475
+ const summary = `${options.summary || ''}`.trim();
476
+ const status = `${options.status || 'completed'}`.trim() || 'completed';
477
+ const payload = options.payload == null ? null : options.payload;
478
+
479
+ const { sessionId, session } = await this._resolveSession(specSessionRef);
480
+ if (!session.scene || session.scene.role !== 'spec') {
481
+ throw new Error(`Session ${sessionId} is not a spec child session`);
482
+ }
483
+
484
+ session.snapshots = Array.isArray(session.snapshots) ? session.snapshots : [];
485
+ session.snapshots.push({
486
+ snapshot_id: nextSnapshotId(session),
487
+ captured_at: now,
488
+ status,
489
+ summary,
490
+ payload
491
+ });
492
+ session.status = status;
493
+ session.scene.state = 'completed';
494
+ session.scene.completed_at = now;
495
+ session.updated_at = now;
496
+ session.timeline = Array.isArray(session.timeline) ? session.timeline : [];
497
+ session.timeline.push({
498
+ at: now,
499
+ event: 'spec_session_completed',
500
+ detail: {
501
+ scene_id: session.scene.id,
502
+ spec_id: session.scene.spec_id
503
+ }
504
+ });
505
+ await this._writeSession(sessionId, session);
506
+
507
+ if (session.scene.parent_session_id) {
508
+ try {
509
+ const parent = await this.getSession(session.scene.parent_session_id);
510
+ parent.children = parent.children || {};
511
+ parent.children.spec_sessions = Array.isArray(parent.children.spec_sessions)
512
+ ? parent.children.spec_sessions
513
+ : [];
514
+ parent.children.spec_sessions = parent.children.spec_sessions.map((entry) => {
515
+ if (entry && entry.spec_session_id === sessionId) {
516
+ return {
517
+ ...entry,
518
+ status,
519
+ completed_at: now
520
+ };
521
+ }
522
+ return entry;
523
+ });
524
+ parent.updated_at = now;
525
+ parent.timeline = Array.isArray(parent.timeline) ? parent.timeline : [];
526
+ parent.timeline.push({
527
+ at: now,
528
+ event: 'spec_session_archived',
529
+ detail: {
530
+ scene_id: session.scene.id,
531
+ spec_id: session.scene.spec_id,
532
+ spec_session_id: sessionId
533
+ }
534
+ });
535
+ await this._writeSession(parent.session_id, parent);
536
+ } catch (_error) {
537
+ // parent may have been removed manually; spec session has already been archived
538
+ }
539
+ }
540
+
541
+ return session;
542
+ }
543
+
544
+ async listSceneRecords() {
545
+ const sceneIndex = await this._readSceneIndex();
546
+ return Object.values(sceneIndex.scenes || {});
547
+ }
548
+
549
+ async listActiveSceneSessions() {
550
+ const records = await this.listSceneRecords();
551
+ const active = [];
552
+ for (const record of records) {
553
+ if (!record || !record.active_session_id) {
554
+ continue;
555
+ }
556
+ try {
557
+ const session = await this.getSession(record.active_session_id);
558
+ if (session && session.status === 'active') {
559
+ active.push({
560
+ scene_id: record.scene_id,
561
+ scene_cycle: record.active_cycle,
562
+ session
563
+ });
564
+ }
565
+ } catch (_error) {
566
+ // skip stale references
567
+ }
568
+ }
569
+ return active;
570
+ }
571
+
572
+ async getActiveSceneSession(sceneId) {
573
+ const normalizedSceneId = `${sceneId || ''}`.trim();
574
+ if (!normalizedSceneId) {
575
+ throw new Error('sceneId is required for getActiveSceneSession');
576
+ }
577
+ const sceneIndex = await this._readSceneIndex();
578
+ const record = sceneIndex.scenes[normalizedSceneId];
579
+ if (!record || !record.active_session_id) {
580
+ return null;
581
+ }
582
+ try {
583
+ const session = await this.getSession(record.active_session_id);
584
+ if (!session || session.status !== 'active') {
585
+ return null;
586
+ }
587
+ return {
588
+ scene_id: normalizedSceneId,
589
+ scene_cycle: record.active_cycle,
590
+ session
591
+ };
592
+ } catch (_error) {
593
+ return null;
594
+ }
595
+ }
596
+
170
597
  async _resolveSession(sessionRef) {
171
598
  const ref = `${sessionRef || 'latest'}`.trim();
172
599
  if (ref === 'latest') {
@@ -195,6 +622,48 @@ class SessionStore {
195
622
  await fs.writeJson(this._sessionPath(sessionId), session, { spaces: 2 });
196
623
  }
197
624
 
625
+ async _readSceneIndex() {
626
+ if (!await fs.pathExists(this._sceneIndexPath)) {
627
+ return {
628
+ schema_version: SESSION_GOVERNANCE_SCHEMA_VERSION,
629
+ updated_at: nowIso(),
630
+ scenes: {}
631
+ };
632
+ }
633
+
634
+ try {
635
+ const payload = await fs.readJson(this._sceneIndexPath);
636
+ return {
637
+ schema_version: payload && payload.schema_version
638
+ ? payload.schema_version
639
+ : SESSION_GOVERNANCE_SCHEMA_VERSION,
640
+ updated_at: payload && payload.updated_at ? payload.updated_at : nowIso(),
641
+ scenes: payload && typeof payload.scenes === 'object' && payload.scenes
642
+ ? payload.scenes
643
+ : {}
644
+ };
645
+ } catch (_error) {
646
+ return {
647
+ schema_version: SESSION_GOVERNANCE_SCHEMA_VERSION,
648
+ updated_at: nowIso(),
649
+ scenes: {}
650
+ };
651
+ }
652
+ }
653
+
654
+ async _writeSceneIndex(indexPayload) {
655
+ const payload = {
656
+ schema_version: SESSION_GOVERNANCE_SCHEMA_VERSION,
657
+ updated_at: nowIso(),
658
+ scenes: indexPayload && typeof indexPayload.scenes === 'object' && indexPayload.scenes
659
+ ? indexPayload.scenes
660
+ : {}
661
+ };
662
+ await fs.ensureDir(this._sessionGovernanceDir);
663
+ await fs.writeJson(this._sceneIndexPath, payload, { spaces: 2 });
664
+ return payload;
665
+ }
666
+
198
667
  _sessionPath(sessionId) {
199
668
  return path.join(this._sessionsDir, `${sessionId}.json`);
200
669
  }
@@ -204,4 +673,7 @@ module.exports = {
204
673
  SessionStore,
205
674
  SESSION_SCHEMA_VERSION,
206
675
  SESSION_DIR,
676
+ SESSION_GOVERNANCE_SCHEMA_VERSION,
677
+ SESSION_GOVERNANCE_DIR,
678
+ SESSION_SCENE_INDEX_FILE,
207
679
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scene-capability-engine",
3
- "version": "3.3.18",
3
+ "version": "3.3.22",
4
4
  "description": "SCE (Scene Capability Engine) - A CLI tool and npm package for spec-driven development with AI coding assistants.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -68,10 +68,12 @@
68
68
  "report:interactive-governance": "node scripts/interactive-governance-report.js --period weekly --json",
69
69
  "report:release-ops-weekly": "node scripts/release-ops-weekly-summary.js --json",
70
70
  "gate:release-ops-weekly": "node scripts/release-weekly-ops-gate.js",
71
+ "gate:errorbook-release": "node scripts/errorbook-release-gate.js --fail-on-block",
72
+ "gate:git-managed": "node scripts/git-managed-gate.js --fail-on-violation",
71
73
  "gate:release-asset-integrity": "node scripts/release-asset-integrity-check.js",
72
74
  "report:release-risk-remediation": "node scripts/release-risk-remediation-bundle.js --json",
73
75
  "report:moqui-core-regression": "node scripts/moqui-core-regression-suite.js --json",
74
- "prepublishOnly": "npm run test:full && npm run test:skip-audit && npm run test:sce-tracking && npm run test:brand-consistency && npm run report:interactive-governance -- --fail-on-alert",
76
+ "prepublishOnly": "npm run test:full && npm run test:skip-audit && npm run test:sce-tracking && npm run test:brand-consistency && npm run gate:git-managed && npm run gate:errorbook-release && npm run report:interactive-governance -- --fail-on-alert",
75
77
  "publish:manual": "npm publish --access public",
76
78
  "install-global": "npm install -g .",
77
79
  "uninstall-global": "npm uninstall -g scene-capability-engine"