scene-capability-engine 3.6.2 → 3.6.4

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,659 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+ const { getSceStateStore } = require('./sce-state-store');
4
+ const { ProjectTimelineStore } = require('../runtime/project-timeline');
5
+ const { SessionStore } = require('../runtime/session-store');
6
+
7
+ const COMPONENT_AGENT_REGISTRY = 'collab.agent-registry';
8
+ const COMPONENT_TIMELINE_INDEX = 'runtime.timeline-index';
9
+ const COMPONENT_SCENE_SESSION_INDEX = 'runtime.scene-session-index';
10
+ const DEFAULT_STATE_EXPORT_PATH = '.sce/reports/state-migration/state-export.latest.json';
11
+
12
+ const COMPONENT_DEFINITIONS = Object.freeze([
13
+ {
14
+ id: COMPONENT_AGENT_REGISTRY,
15
+ source_path: '.sce/config/agent-registry.json'
16
+ },
17
+ {
18
+ id: COMPONENT_TIMELINE_INDEX,
19
+ source_path: '.sce/timeline/index.json'
20
+ },
21
+ {
22
+ id: COMPONENT_SCENE_SESSION_INDEX,
23
+ source_path: '.sce/session-governance/scene-index.json'
24
+ }
25
+ ]);
26
+
27
+ function normalizeString(value) {
28
+ if (typeof value !== 'string') {
29
+ return '';
30
+ }
31
+ return value.trim();
32
+ }
33
+
34
+ function normalizeBoolean(value, fallback = false) {
35
+ if (typeof value === 'boolean') {
36
+ return value;
37
+ }
38
+ const normalized = normalizeString(`${value}`).toLowerCase();
39
+ if (!normalized) {
40
+ return fallback;
41
+ }
42
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) {
43
+ return true;
44
+ }
45
+ if (['0', 'false', 'no', 'off'].includes(normalized)) {
46
+ return false;
47
+ }
48
+ return fallback;
49
+ }
50
+
51
+ function normalizeInteger(value, fallback = 0) {
52
+ const parsed = Number.parseInt(`${value}`, 10);
53
+ if (!Number.isFinite(parsed) || parsed < 0) {
54
+ return fallback;
55
+ }
56
+ return parsed;
57
+ }
58
+
59
+ function nowIso() {
60
+ return new Date().toISOString();
61
+ }
62
+
63
+ function normalizeCount(value) {
64
+ if (!Number.isFinite(Number(value))) {
65
+ return 0;
66
+ }
67
+ return Math.max(0, Number.parseInt(`${value}`, 10) || 0);
68
+ }
69
+
70
+ function parseComponentIds(value) {
71
+ if (Array.isArray(value)) {
72
+ return Array.from(new Set(value.map((item) => normalizeString(item)).filter(Boolean)));
73
+ }
74
+ const normalized = normalizeString(value);
75
+ if (!normalized) {
76
+ return [];
77
+ }
78
+ return Array.from(new Set(normalized.split(/[,\s]+/g).map((item) => normalizeString(item)).filter(Boolean)));
79
+ }
80
+
81
+ function resolveDefinitionsById(componentIds = []) {
82
+ if (!Array.isArray(componentIds) || componentIds.length === 0) {
83
+ return [...COMPONENT_DEFINITIONS];
84
+ }
85
+ const selected = new Set(componentIds);
86
+ return COMPONENT_DEFINITIONS.filter((item) => selected.has(item.id));
87
+ }
88
+
89
+ function mapAgentRegistryPayload(payload = {}) {
90
+ if (!payload || typeof payload !== 'object' || !payload.agents || typeof payload.agents !== 'object') {
91
+ return [];
92
+ }
93
+ return Object.values(payload.agents)
94
+ .map((item) => ({
95
+ agent_id: normalizeString(item && item.agentId),
96
+ machine_id: normalizeString(item && item.machineId),
97
+ instance_index: normalizeInteger(item && item.instanceIndex, 0),
98
+ hostname: normalizeString(item && item.hostname),
99
+ registered_at: normalizeString(item && item.registeredAt),
100
+ last_heartbeat: normalizeString(item && item.lastHeartbeat),
101
+ status: normalizeString(item && item.status),
102
+ current_task: item && item.currentTask && typeof item.currentTask === 'object'
103
+ ? item.currentTask
104
+ : null
105
+ }))
106
+ .filter((item) => item.agent_id);
107
+ }
108
+
109
+ function mapTimelineIndexPayload(payload = {}) {
110
+ if (!payload || typeof payload !== 'object' || !Array.isArray(payload.snapshots)) {
111
+ return [];
112
+ }
113
+ return payload.snapshots
114
+ .map((item) => ({
115
+ snapshot_id: normalizeString(item && item.snapshot_id),
116
+ created_at: normalizeString(item && item.created_at),
117
+ trigger: normalizeString(item && item.trigger),
118
+ event: normalizeString(item && item.event),
119
+ summary: normalizeString(item && item.summary),
120
+ scene_id: normalizeString(item && item.scene_id),
121
+ session_id: normalizeString(item && item.session_id),
122
+ command: normalizeString(item && item.command),
123
+ file_count: normalizeInteger(item && item.file_count, 0),
124
+ total_bytes: normalizeInteger(item && item.total_bytes, 0),
125
+ snapshot_path: normalizeString(item && (item.path || item.snapshot_path)),
126
+ git: item && item.git && typeof item.git === 'object' ? item.git : {}
127
+ }))
128
+ .filter((item) => item.snapshot_id);
129
+ }
130
+
131
+ function mapSceneSessionIndexPayload(payload = {}) {
132
+ if (!payload || typeof payload !== 'object' || !payload.scenes || typeof payload.scenes !== 'object') {
133
+ return [];
134
+ }
135
+
136
+ const records = [];
137
+ for (const [sceneId, sceneRecord] of Object.entries(payload.scenes)) {
138
+ const normalizedSceneId = normalizeString(sceneId);
139
+ if (!normalizedSceneId) {
140
+ continue;
141
+ }
142
+ const cycles = Array.isArray(sceneRecord && sceneRecord.cycles) ? sceneRecord.cycles : [];
143
+ for (const cycle of cycles) {
144
+ const cycleNo = normalizeInteger(cycle && cycle.cycle, 0);
145
+ const sessionId = normalizeString(cycle && cycle.session_id);
146
+ if (!cycleNo || !sessionId) {
147
+ continue;
148
+ }
149
+ records.push({
150
+ scene_id: normalizedSceneId,
151
+ cycle: cycleNo,
152
+ session_id: sessionId,
153
+ status: normalizeString(cycle && cycle.status),
154
+ started_at: normalizeString(cycle && cycle.started_at),
155
+ completed_at: normalizeString(cycle && cycle.completed_at)
156
+ });
157
+ }
158
+ }
159
+ return records;
160
+ }
161
+
162
+ async function readJsonSource(absolutePath, fileSystem = fs) {
163
+ if (!await fileSystem.pathExists(absolutePath)) {
164
+ return {
165
+ exists: false,
166
+ payload: null,
167
+ parse_error: null
168
+ };
169
+ }
170
+
171
+ try {
172
+ const payload = await fileSystem.readJson(absolutePath);
173
+ return {
174
+ exists: true,
175
+ payload,
176
+ parse_error: null
177
+ };
178
+ } catch (error) {
179
+ return {
180
+ exists: true,
181
+ payload: null,
182
+ parse_error: error.message
183
+ };
184
+ }
185
+ }
186
+
187
+ async function readComponentSnapshot(component = {}, projectPath = process.cwd(), fileSystem = fs) {
188
+ const absolutePath = path.join(projectPath, component.source_path);
189
+ const source = await readJsonSource(absolutePath, fileSystem);
190
+
191
+ let records = [];
192
+ if (!source.parse_error && source.payload) {
193
+ if (component.id === COMPONENT_AGENT_REGISTRY) {
194
+ records = mapAgentRegistryPayload(source.payload);
195
+ } else if (component.id === COMPONENT_TIMELINE_INDEX) {
196
+ records = mapTimelineIndexPayload(source.payload);
197
+ } else if (component.id === COMPONENT_SCENE_SESSION_INDEX) {
198
+ records = mapSceneSessionIndexPayload(source.payload);
199
+ }
200
+ }
201
+
202
+ return {
203
+ id: component.id,
204
+ source_path: component.source_path,
205
+ exists: source.exists,
206
+ parse_error: source.parse_error,
207
+ source_record_count: records.length,
208
+ records
209
+ };
210
+ }
211
+
212
+ function buildComponentPlan(componentSnapshot = {}) {
213
+ const exists = componentSnapshot.exists === true;
214
+ const parseError = normalizeString(componentSnapshot.parse_error);
215
+ const sourceCount = normalizeInteger(componentSnapshot.source_record_count, 0);
216
+ let status = 'ready';
217
+ if (!exists) {
218
+ status = 'missing';
219
+ } else if (parseError) {
220
+ status = 'parse-error';
221
+ } else if (sourceCount <= 0) {
222
+ status = 'empty';
223
+ }
224
+
225
+ return {
226
+ id: componentSnapshot.id,
227
+ source_path: componentSnapshot.source_path,
228
+ exists,
229
+ parse_error: parseError || null,
230
+ source_record_count: sourceCount,
231
+ status
232
+ };
233
+ }
234
+
235
+ async function buildStateMigrationPlan(options = {}, dependencies = {}) {
236
+ const projectPath = dependencies.projectPath || process.cwd();
237
+ const fileSystem = dependencies.fileSystem || fs;
238
+ const env = dependencies.env || process.env;
239
+ const componentIds = parseComponentIds(options.components || options.component || options.componentIds);
240
+ const definitions = resolveDefinitionsById(componentIds);
241
+ const stateStore = dependencies.stateStore || getSceStateStore(projectPath, { fileSystem, env });
242
+
243
+ const snapshots = [];
244
+ for (const definition of definitions) {
245
+ const snapshot = await readComponentSnapshot(definition, projectPath, fileSystem);
246
+ snapshots.push(snapshot);
247
+ }
248
+
249
+ const components = snapshots.map((snapshot) => buildComponentPlan(snapshot));
250
+ const totalSourceRecords = components.reduce((sum, item) => sum + normalizeInteger(item.source_record_count, 0), 0);
251
+ const readyComponents = components.filter((item) => item.status === 'ready').length;
252
+
253
+ return {
254
+ mode: 'state-plan',
255
+ success: true,
256
+ generated_at: nowIso(),
257
+ store_path: stateStore.getStoreRelativePath ? stateStore.getStoreRelativePath() : null,
258
+ sqlite: {
259
+ configured: stateStore.isSqliteConfigured ? stateStore.isSqliteConfigured() : false,
260
+ available: stateStore.isSqliteAvailable ? stateStore.isSqliteAvailable() : false
261
+ },
262
+ components,
263
+ summary: {
264
+ total_components: components.length,
265
+ ready_components: readyComponents,
266
+ total_source_records: totalSourceRecords
267
+ },
268
+ snapshots
269
+ };
270
+ }
271
+
272
+ async function writeComponentToStateStore(componentSnapshot = {}, stateStore, componentId = '') {
273
+ if (componentId === COMPONENT_AGENT_REGISTRY) {
274
+ return stateStore.upsertAgentRuntimeRecords(componentSnapshot.records, {
275
+ source: componentId
276
+ });
277
+ }
278
+ if (componentId === COMPONENT_TIMELINE_INDEX) {
279
+ return stateStore.upsertTimelineSnapshotIndex(componentSnapshot.records, {
280
+ source: componentId
281
+ });
282
+ }
283
+ if (componentId === COMPONENT_SCENE_SESSION_INDEX) {
284
+ return stateStore.upsertSceneSessionCycles(componentSnapshot.records, {
285
+ source: componentId
286
+ });
287
+ }
288
+ return null;
289
+ }
290
+
291
+ async function runStateMigration(options = {}, dependencies = {}) {
292
+ const projectPath = dependencies.projectPath || process.cwd();
293
+ const fileSystem = dependencies.fileSystem || fs;
294
+ const env = dependencies.env || process.env;
295
+ const apply = normalizeBoolean(options.apply, false);
296
+ const requestedIds = parseComponentIds(options.components || options.component || options.componentIds);
297
+ const migrateAll = options.all === true || requestedIds.length === 0;
298
+ const selectedIds = migrateAll
299
+ ? COMPONENT_DEFINITIONS.map((item) => item.id)
300
+ : requestedIds;
301
+
302
+ const plan = await buildStateMigrationPlan({
303
+ componentIds: selectedIds
304
+ }, {
305
+ projectPath,
306
+ fileSystem,
307
+ env
308
+ });
309
+ const stateStore = dependencies.stateStore || getSceStateStore(projectPath, { fileSystem, env });
310
+
311
+ const operations = [];
312
+ let migratedComponents = 0;
313
+ let migratedRecords = 0;
314
+
315
+ for (const snapshot of plan.snapshots) {
316
+ const componentPlan = buildComponentPlan(snapshot);
317
+ const op = {
318
+ component_id: snapshot.id,
319
+ source_path: snapshot.source_path,
320
+ source_record_count: snapshot.source_record_count,
321
+ status: componentPlan.status,
322
+ applied: false,
323
+ result: null
324
+ };
325
+
326
+ if (componentPlan.status !== 'ready') {
327
+ operations.push(op);
328
+ continue;
329
+ }
330
+
331
+ if (!apply) {
332
+ op.status = 'planned';
333
+ operations.push(op);
334
+ continue;
335
+ }
336
+
337
+ const startedAt = nowIso();
338
+ const writeResult = await writeComponentToStateStore(snapshot, stateStore, snapshot.id);
339
+ if (!writeResult) {
340
+ op.status = 'failed';
341
+ op.error = 'SQLite state backend unavailable for migration write';
342
+ operations.push(op);
343
+ await stateStore.appendStateMigrationRecord({
344
+ component_id: snapshot.id,
345
+ source_path: snapshot.source_path,
346
+ mode: 'apply',
347
+ status: 'failed',
348
+ metrics: {
349
+ source_record_count: snapshot.source_record_count
350
+ },
351
+ detail: {
352
+ error: op.error
353
+ },
354
+ started_at: startedAt,
355
+ completed_at: nowIso()
356
+ });
357
+ continue;
358
+ }
359
+
360
+ op.applied = true;
361
+ op.status = 'migrated';
362
+ op.result = writeResult;
363
+ operations.push(op);
364
+ migratedComponents += 1;
365
+ migratedRecords += normalizeInteger(writeResult.written, 0);
366
+
367
+ await stateStore.appendStateMigrationRecord({
368
+ component_id: snapshot.id,
369
+ source_path: snapshot.source_path,
370
+ mode: 'apply',
371
+ status: 'completed',
372
+ metrics: {
373
+ source_record_count: snapshot.source_record_count,
374
+ written: normalizeInteger(writeResult.written, 0),
375
+ target_total: normalizeInteger(writeResult.total, 0)
376
+ },
377
+ detail: {
378
+ component_id: snapshot.id
379
+ },
380
+ started_at: startedAt,
381
+ completed_at: nowIso()
382
+ });
383
+ }
384
+
385
+ return {
386
+ mode: 'state-migrate',
387
+ success: operations.every((item) => item.status !== 'failed'),
388
+ apply,
389
+ generated_at: nowIso(),
390
+ store_path: stateStore.getStoreRelativePath ? stateStore.getStoreRelativePath() : null,
391
+ operations,
392
+ summary: {
393
+ selected_components: selectedIds.length,
394
+ migrated_components: migratedComponents,
395
+ migrated_records: migratedRecords
396
+ }
397
+ };
398
+ }
399
+
400
+ async function runStateDoctor(options = {}, dependencies = {}) {
401
+ const projectPath = dependencies.projectPath || process.cwd();
402
+ const fileSystem = dependencies.fileSystem || fs;
403
+ const env = dependencies.env || process.env;
404
+ const stateStore = dependencies.stateStore || getSceStateStore(projectPath, { fileSystem, env });
405
+ const plan = await buildStateMigrationPlan({}, { projectPath, fileSystem, env, stateStore });
406
+
407
+ const [agentRows, timelineRows, sessionRows, migrations] = await Promise.all([
408
+ stateStore.listAgentRuntimeRecords({ limit: 0 }),
409
+ stateStore.listTimelineSnapshotIndex({ limit: 0 }),
410
+ stateStore.listSceneSessionCycles({ limit: 0 }),
411
+ stateStore.listStateMigrations({ limit: 20 })
412
+ ]);
413
+
414
+ const targetCounts = new Map([
415
+ [COMPONENT_AGENT_REGISTRY, Array.isArray(agentRows) ? agentRows.length : 0],
416
+ [COMPONENT_TIMELINE_INDEX, Array.isArray(timelineRows) ? timelineRows.length : 0],
417
+ [COMPONENT_SCENE_SESSION_INDEX, Array.isArray(sessionRows) ? sessionRows.length : 0]
418
+ ]);
419
+
420
+ const checks = plan.components.map((component) => {
421
+ const sourceCount = normalizeInteger(component.source_record_count, 0);
422
+ const targetCount = normalizeInteger(targetCounts.get(component.id), 0);
423
+ let syncStatus = 'synced';
424
+ if (component.status === 'missing') {
425
+ syncStatus = targetCount > 0 ? 'sqlite-only' : 'missing-source';
426
+ } else if (component.status === 'parse-error') {
427
+ syncStatus = 'source-parse-error';
428
+ } else if (sourceCount === 0 && targetCount === 0) {
429
+ syncStatus = 'empty';
430
+ } else if (targetCount < sourceCount) {
431
+ syncStatus = 'pending-migration';
432
+ }
433
+ return {
434
+ id: component.id,
435
+ source_path: component.source_path,
436
+ source_record_count: sourceCount,
437
+ sqlite_record_count: targetCount,
438
+ source_status: component.status,
439
+ sync_status: syncStatus
440
+ };
441
+ });
442
+
443
+ const runtime = await collectRuntimeDiagnostics({
444
+ projectPath,
445
+ fileSystem,
446
+ env,
447
+ stateStore
448
+ });
449
+
450
+ const blocking = [];
451
+ if (!plan.sqlite.available) {
452
+ blocking.push('sqlite-unavailable');
453
+ }
454
+ if (checks.some((item) => item.sync_status === 'source-parse-error')) {
455
+ blocking.push('source-parse-error');
456
+ }
457
+
458
+ const alerts = checks
459
+ .filter((item) => item.sync_status === 'pending-migration')
460
+ .map((item) => `pending migration: ${item.id}`);
461
+
462
+ if (runtime.timeline && runtime.timeline.consistency && runtime.timeline.consistency.status === 'pending-sync') {
463
+ alerts.push('runtime timeline index pending-sync');
464
+ }
465
+ if (runtime.timeline && runtime.timeline.consistency && runtime.timeline.consistency.status === 'sqlite-ahead') {
466
+ alerts.push('runtime timeline index sqlite-ahead');
467
+ }
468
+ if (runtime.scene_session && runtime.scene_session.consistency && runtime.scene_session.consistency.status === 'pending-sync') {
469
+ alerts.push('runtime scene-session index pending-sync');
470
+ }
471
+ if (runtime.scene_session && runtime.scene_session.consistency && runtime.scene_session.consistency.status === 'sqlite-ahead') {
472
+ alerts.push('runtime scene-session index sqlite-ahead');
473
+ }
474
+
475
+ const summary = summarizeDoctorChecks(checks, alerts, blocking);
476
+
477
+ return {
478
+ mode: 'state-doctor',
479
+ success: blocking.length === 0,
480
+ generated_at: nowIso(),
481
+ store_path: plan.store_path,
482
+ sqlite: plan.sqlite,
483
+ summary,
484
+ runtime,
485
+ checks,
486
+ migrations: Array.isArray(migrations) ? migrations : [],
487
+ blocking,
488
+ alerts
489
+ };
490
+ }
491
+
492
+ function summarizeDoctorChecks(checks = [], alerts = [], blocking = []) {
493
+ const normalizedChecks = Array.isArray(checks) ? checks : [];
494
+ const sourceRecords = normalizedChecks.reduce((sum, item) => sum + normalizeCount(item.source_record_count), 0);
495
+ const sqliteRecords = normalizedChecks.reduce((sum, item) => sum + normalizeCount(item.sqlite_record_count), 0);
496
+ const pendingComponents = normalizedChecks.filter((item) => item.sync_status === 'pending-migration').length;
497
+ const syncedComponents = normalizedChecks.filter((item) => item.sync_status === 'synced').length;
498
+ const sqliteOnlyComponents = normalizedChecks.filter((item) => item.sync_status === 'sqlite-only').length;
499
+ const missingSourceComponents = normalizedChecks.filter((item) => item.sync_status === 'missing-source').length;
500
+ const driftRecords = normalizedChecks.reduce((sum, item) => {
501
+ const source = normalizeCount(item.source_record_count);
502
+ const target = normalizeCount(item.sqlite_record_count);
503
+ return sum + Math.abs(source - target);
504
+ }, 0);
505
+
506
+ return {
507
+ total_components: normalizedChecks.length,
508
+ synced_components: syncedComponents,
509
+ pending_components: pendingComponents,
510
+ sqlite_only_components: sqliteOnlyComponents,
511
+ missing_source_components: missingSourceComponents,
512
+ total_source_records: sourceRecords,
513
+ total_sqlite_records: sqliteRecords,
514
+ total_record_drift: driftRecords,
515
+ blocking_count: Array.isArray(blocking) ? blocking.length : 0,
516
+ alert_count: Array.isArray(alerts) ? alerts.length : 0
517
+ };
518
+ }
519
+
520
+ async function collectRuntimeDiagnostics(dependencies = {}) {
521
+ const projectPath = dependencies.projectPath || process.cwd();
522
+ const fileSystem = dependencies.fileSystem || fs;
523
+ const env = dependencies.env || process.env;
524
+ const stateStore = dependencies.stateStore || getSceStateStore(projectPath, {
525
+ fileSystem,
526
+ env
527
+ });
528
+
529
+ const runtime = {
530
+ timeline: {
531
+ read_source: 'unavailable',
532
+ consistency: {
533
+ status: 'unavailable',
534
+ file_index_count: 0,
535
+ sqlite_index_count: null
536
+ }
537
+ },
538
+ scene_session: {
539
+ read_preference: 'file',
540
+ consistency: {
541
+ status: 'unavailable',
542
+ file_index_count: 0,
543
+ sqlite_index_count: null
544
+ }
545
+ }
546
+ };
547
+
548
+ try {
549
+ const timelineStore = new ProjectTimelineStore(projectPath, fileSystem, {
550
+ env,
551
+ stateStore,
552
+ preferSqliteReads: true
553
+ });
554
+ const timelineView = await timelineStore.listSnapshots({ limit: 1 });
555
+ runtime.timeline = {
556
+ read_source: normalizeString(timelineView.read_source) || 'file',
557
+ consistency: {
558
+ status: normalizeString(timelineView && timelineView.consistency && timelineView.consistency.status) || 'unknown',
559
+ file_index_count: normalizeCount(timelineView && timelineView.consistency && timelineView.consistency.file_index_count),
560
+ sqlite_index_count: Number.isFinite(Number(timelineView && timelineView.consistency && timelineView.consistency.sqlite_index_count))
561
+ ? Number.parseInt(`${timelineView.consistency.sqlite_index_count}`, 10)
562
+ : null
563
+ }
564
+ };
565
+ } catch (_error) {
566
+ runtime.timeline = {
567
+ read_source: 'unavailable',
568
+ consistency: {
569
+ status: 'unavailable',
570
+ file_index_count: 0,
571
+ sqlite_index_count: null
572
+ }
573
+ };
574
+ }
575
+
576
+ try {
577
+ const sessionStore = new SessionStore(projectPath, fileSystem, {
578
+ env,
579
+ stateStore,
580
+ preferSqliteSceneReads: true
581
+ });
582
+ const sceneIndex = await sessionStore.getSceneIndexDiagnostics();
583
+ runtime.scene_session = {
584
+ read_preference: normalizeString(sceneIndex.read_preference) || 'file',
585
+ consistency: {
586
+ status: normalizeString(sceneIndex.status) || 'unknown',
587
+ file_index_count: normalizeCount(sceneIndex.file_scene_count),
588
+ sqlite_index_count: Number.isFinite(Number(sceneIndex.sqlite_scene_count))
589
+ ? Number.parseInt(`${sceneIndex.sqlite_scene_count}`, 10)
590
+ : null
591
+ }
592
+ };
593
+ } catch (_error) {
594
+ runtime.scene_session = {
595
+ read_preference: 'file',
596
+ consistency: {
597
+ status: 'unavailable',
598
+ file_index_count: 0,
599
+ sqlite_index_count: null
600
+ }
601
+ };
602
+ }
603
+
604
+ return runtime;
605
+ }
606
+
607
+ async function runStateExport(options = {}, dependencies = {}) {
608
+ const projectPath = dependencies.projectPath || process.cwd();
609
+ const fileSystem = dependencies.fileSystem || fs;
610
+ const env = dependencies.env || process.env;
611
+ const outPathRaw = normalizeString(options.out) || DEFAULT_STATE_EXPORT_PATH;
612
+ const outPath = path.isAbsolute(outPathRaw)
613
+ ? outPathRaw
614
+ : path.join(projectPath, outPathRaw);
615
+ const stateStore = dependencies.stateStore || getSceStateStore(projectPath, { fileSystem, env });
616
+
617
+ const [agentRows, timelineRows, sessionRows, migrations] = await Promise.all([
618
+ stateStore.listAgentRuntimeRecords({ limit: 0 }),
619
+ stateStore.listTimelineSnapshotIndex({ limit: 0 }),
620
+ stateStore.listSceneSessionCycles({ limit: 0 }),
621
+ stateStore.listStateMigrations({ limit: 0 })
622
+ ]);
623
+
624
+ const payload = {
625
+ mode: 'state-export',
626
+ success: true,
627
+ exported_at: nowIso(),
628
+ store_path: stateStore.getStoreRelativePath ? stateStore.getStoreRelativePath() : null,
629
+ tables: {
630
+ agent_runtime_registry: Array.isArray(agentRows) ? agentRows : [],
631
+ timeline_snapshot_registry: Array.isArray(timelineRows) ? timelineRows : [],
632
+ scene_session_cycle_registry: Array.isArray(sessionRows) ? sessionRows : [],
633
+ state_migration_registry: Array.isArray(migrations) ? migrations : []
634
+ },
635
+ summary: {
636
+ agent_runtime_registry: Array.isArray(agentRows) ? agentRows.length : 0,
637
+ timeline_snapshot_registry: Array.isArray(timelineRows) ? timelineRows.length : 0,
638
+ scene_session_cycle_registry: Array.isArray(sessionRows) ? sessionRows.length : 0,
639
+ state_migration_registry: Array.isArray(migrations) ? migrations.length : 0
640
+ },
641
+ out_file: path.relative(projectPath, outPath).replace(/\\/g, '/')
642
+ };
643
+
644
+ await fileSystem.ensureDir(path.dirname(outPath));
645
+ await fileSystem.writeJson(outPath, payload, { spaces: 2 });
646
+ return payload;
647
+ }
648
+
649
+ module.exports = {
650
+ COMPONENT_AGENT_REGISTRY,
651
+ COMPONENT_TIMELINE_INDEX,
652
+ COMPONENT_SCENE_SESSION_INDEX,
653
+ COMPONENT_DEFINITIONS,
654
+ DEFAULT_STATE_EXPORT_PATH,
655
+ buildStateMigrationPlan,
656
+ runStateMigration,
657
+ runStateDoctor,
658
+ runStateExport
659
+ };
@@ -43,6 +43,12 @@ class ComplianceErrorReporter {
43
43
  message += ' ✓ ENVIRONMENT.md\n';
44
44
  message += ' ✓ CURRENT_CONTEXT.md\n';
45
45
  message += ' ✓ RULES_GUIDE.md\n';
46
+ message += ' ✓ manifest.yaml\n';
47
+ message += '\n' + chalk.green.bold('Allowed Subdirectories:') + '\n';
48
+ message += ' ✓ compiled/\n';
49
+ message += '\n' + chalk.green.bold('Allowed Runtime Temp Files:') + '\n';
50
+ message += ' ✓ *.lock\n';
51
+ message += ' ✓ *.pending.<agentId>\n';
46
52
 
47
53
  message += '\n' + chalk.cyan.bold('Fix Suggestions:') + '\n';
48
54
  message += ' • Move analysis reports to: .sce/specs/{spec-name}/reports/\n';