scene-capability-engine 3.3.17 → 3.3.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +19 -10
- package/README.zh.md +21 -11
- package/bin/scene-capability-engine.js +4 -0
- package/docs/command-reference.md +94 -12
- package/docs/release-checklist.md +6 -0
- package/docs/zh/release-checklist.md +6 -0
- package/lib/commands/errorbook.js +1244 -0
- package/lib/commands/spec-bootstrap.js +126 -51
- package/lib/commands/spec-gate.js +92 -25
- package/lib/commands/spec-pipeline.js +86 -7
- package/lib/commands/studio.js +265 -30
- package/lib/runtime/multi-spec-scene-session.js +147 -0
- package/lib/runtime/scene-session-binding.js +109 -0
- package/lib/runtime/session-store.js +475 -3
- package/package.json +4 -2
- package/template/.sce/steering/CORE_PRINCIPLES.md +26 -1
|
@@ -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.21",
|
|
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"
|
|
@@ -102,6 +102,19 @@
|
|
|
102
102
|
|
|
103
103
|
**后果**: 违反导致用户困惑、功能难发现、门槛高、维护成本增
|
|
104
104
|
|
|
105
|
+
### 8.1 Git 托管强制门禁原则 🔐
|
|
106
|
+
|
|
107
|
+
**核心**: 新增或修改代码必须进入 Git 托管管理链路,禁止“本地改完未推送”进入发布流程
|
|
108
|
+
|
|
109
|
+
**默认强制**: 若仓库配置了 GitHub/GitLab 远端,发布前必须满足:
|
|
110
|
+
1) 工作区干净(无未提交变更)
|
|
111
|
+
2) 当前分支已配置 upstream
|
|
112
|
+
3) 与 upstream 完全同步(ahead=0, behind=0)
|
|
113
|
+
|
|
114
|
+
**豁免条件**: 仅当客户环境确实没有 GitHub/GitLab 托管时允许放行(策略控制),并需在交付记录中声明原因
|
|
115
|
+
|
|
116
|
+
**落地门禁**: `scripts/git-managed-gate.js`(`prepublishOnly` 与 release preflight 默认执行)
|
|
117
|
+
|
|
105
118
|
### 9. 版本同步和 Steering 刷新原则
|
|
106
119
|
|
|
107
120
|
**核心**: 版本更新或首次安装后,必须阅读 `.sce/README.md` 并刷新 Steering
|
|
@@ -155,6 +168,18 @@
|
|
|
155
168
|
|
|
156
169
|
**要求**: 需求分析、问题定位、模板沉淀、发布门禁均按该口径检查完整性与一致性
|
|
157
170
|
|
|
171
|
+
### 13. Scene 主会话强制治理原则 🎛️
|
|
172
|
+
|
|
173
|
+
**核心**: SCE 默认并强制采用 `1 Scene = 1 主会话`,会话生命周期由 Scene 驱动而不是由单个 Agent 的原生 session 决定
|
|
174
|
+
|
|
175
|
+
**硬规则**:
|
|
176
|
+
1) `studio plan` 必须提供 `--scene`,并绑定 Scene 主会话
|
|
177
|
+
2) 同一 Scene 同时只允许一个活动主会话
|
|
178
|
+
3) `Spec` 必须作为该 Scene 主会话下的子会话管理(创建、归档、摘要回写)
|
|
179
|
+
4) Scene 完成(如 release 成功)后,当前主会话自动归档,并自动开启下一周期主会话
|
|
180
|
+
|
|
181
|
+
**目标**: 保证跨 Agent 连续性、可追溯历史、上下文可控,避免因 Agent session 长短差异导致上下文漂移
|
|
182
|
+
|
|
158
183
|
---
|
|
159
184
|
|
|
160
|
-
|
|
185
|
+
v17.0 | 2026-02-26 | 新增 Scene 主会话强制治理原则
|