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.
- package/CHANGELOG.md +51 -0
- package/README.md +19 -10
- package/README.zh.md +21 -11
- package/docs/command-reference.md +71 -12
- package/docs/release-checklist.md +7 -0
- package/docs/zh/release-checklist.md +7 -0
- package/lib/commands/errorbook.js +455 -2
- package/lib/commands/spec-bootstrap.js +126 -51
- package/lib/commands/spec-gate.js +92 -25
- package/lib/commands/spec-pipeline.js +86 -7
- package/lib/commands/studio.js +265 -30
- package/lib/runtime/multi-spec-scene-session.js +147 -0
- package/lib/runtime/scene-session-binding.js +109 -0
- package/lib/runtime/session-store.js +475 -3
- package/package.json +4 -2
- package/template/.sce/steering/CORE_PRINCIPLES.md +35 -1
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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"
|