scene-capability-engine 3.6.64 → 3.6.67

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.
Files changed (125) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +17 -6
  3. package/README.zh.md +18 -6
  4. package/bin/scene-capability-engine.js +4 -0
  5. package/docs/README.md +2 -2
  6. package/docs/command-reference.md +385 -8
  7. package/docs/document-governance.md +3 -2
  8. package/docs/integration-modes.md +62 -478
  9. package/docs/integration-philosophy.md +56 -263
  10. package/docs/magicball-cli-invocation-examples.md +1 -0
  11. package/docs/magicball-project-portfolio-contract.md +125 -4
  12. package/docs/project-management/README.md +14 -0
  13. package/docs/project-management/assurance/backup.md +3 -0
  14. package/docs/project-management/assurance/config.md +3 -0
  15. package/docs/project-management/assurance/evidence/README.md +3 -0
  16. package/docs/project-management/assurance/incidents/README.md +3 -0
  17. package/docs/project-management/assurance/logs.md +3 -0
  18. package/docs/project-management/assurance/overview.md +3 -0
  19. package/docs/project-management/assurance/recovery/README.md +3 -0
  20. package/docs/project-management/assurance/resource.md +3 -0
  21. package/docs/project-management/assurance/runbooks/README.md +3 -0
  22. package/docs/project-management/delivery/acceptance/README.md +3 -0
  23. package/docs/project-management/delivery/acceptance/evidence/README.md +3 -0
  24. package/docs/project-management/delivery/acceptance/exceptions/README.md +3 -0
  25. package/docs/project-management/delivery/acceptance/reports/README.md +3 -0
  26. package/docs/project-management/delivery/documents/changes.md +3 -0
  27. package/docs/project-management/delivery/documents/issues.md +3 -0
  28. package/docs/project-management/delivery/documents/overview.md +3 -0
  29. package/docs/project-management/delivery/documents/planning.md +3 -0
  30. package/docs/project-management/delivery/documents/requirements.md +3 -0
  31. package/docs/project-management/delivery/documents/tracking.md +3 -0
  32. package/docs/project-management/delivery/handoffs/README.md +3 -0
  33. package/docs/project-management/delivery/handoffs/evidence/README.md +3 -0
  34. package/docs/project-management/delivery/handoffs/records/README.md +3 -0
  35. package/docs/project-management/delivery/overview.md +10 -0
  36. package/docs/project-management/delivery/releases/README.md +3 -0
  37. package/docs/project-management/delivery/releases/baselines/README.md +3 -0
  38. package/docs/project-management/delivery/releases/evidence/README.md +3 -0
  39. package/docs/project-management/delivery/tables/changes.md +3 -0
  40. package/docs/project-management/delivery/tables/issues.md +3 -0
  41. package/docs/project-management/delivery/tables/planning.md +3 -0
  42. package/docs/project-management/delivery/tables/requirements.md +3 -0
  43. package/docs/project-management/delivery/tables/tracking.md +3 -0
  44. package/docs/project-management/environment/agent-discovery.md +3 -0
  45. package/docs/project-management/environment/development.md +3 -0
  46. package/docs/project-management/environment/overview.md +10 -0
  47. package/docs/project-management/environment/testing.md +3 -0
  48. package/docs/project-management/environment/version-alignment.md +3 -0
  49. package/docs/quick-start-with-ai-tools.md +68 -308
  50. package/docs/releases/README.md +3 -0
  51. package/docs/releases/v3.6.65.md +25 -0
  52. package/docs/releases/v3.6.66.md +23 -0
  53. package/docs/releases/v3.6.67.md +23 -0
  54. package/docs/steering-governance.md +64 -2
  55. package/docs/zh/README.md +2 -2
  56. package/docs/zh/releases/README.md +3 -0
  57. package/docs/zh/releases/v3.6.65.md +25 -0
  58. package/docs/zh/releases/v3.6.66.md +23 -0
  59. package/docs/zh/releases/v3.6.67.md +23 -0
  60. package/lib/commands/adopt.js +24 -0
  61. package/lib/commands/native.js +158 -0
  62. package/lib/commands/project.js +96 -0
  63. package/lib/commands/semantic.js +1459 -0
  64. package/lib/commands/session.js +74 -3
  65. package/lib/commands/spec-bootstrap.js +10 -1
  66. package/lib/commands/spec-gate.js +10 -1
  67. package/lib/commands/spec-pipeline.js +10 -1
  68. package/lib/commands/studio.js +405 -30
  69. package/lib/commands/task.js +141 -7
  70. package/lib/governance/supreme-principles.js +530 -0
  71. package/lib/problem/problem-evaluator.js +4 -0
  72. package/lib/project/candidate-inspection-service.js +24 -1
  73. package/lib/project/portfolio-projection-service.js +315 -5
  74. package/lib/project/project-channel-output.js +94 -0
  75. package/lib/project/project-channel-projection.js +181 -0
  76. package/lib/project/root-onboarding-service.js +107 -7
  77. package/lib/project/semantic-shared-source-projection.js +150 -0
  78. package/lib/project/supervision-action-model.js +277 -0
  79. package/lib/project/supervision-projection-service.js +305 -5
  80. package/lib/project/target-resolution-service.js +70 -5
  81. package/lib/project/visibility-policy.js +93 -0
  82. package/lib/runtime/multi-spec-scene-session.js +8 -1
  83. package/lib/runtime/project-channel-context-store.js +387 -0
  84. package/lib/runtime/project-channel-context.js +406 -0
  85. package/lib/runtime/scene-session-binding.js +46 -0
  86. package/lib/runtime/session-store.js +186 -0
  87. package/lib/runtime/steering-contract.js +7 -1
  88. package/lib/semantic/archive-report.js +283 -0
  89. package/lib/semantic/archive-routing.js +67 -0
  90. package/lib/semantic/backflow-report.js +245 -0
  91. package/lib/semantic/capability-contract.js +30 -0
  92. package/lib/semantic/delta-export.js +145 -0
  93. package/lib/semantic/interaction-observer.js +254 -0
  94. package/lib/semantic/kernel-loader.js +881 -0
  95. package/lib/semantic/native-runtime.js +359 -0
  96. package/lib/semantic/progress-ledger.js +433 -0
  97. package/lib/semantic/replay-evaluator.js +382 -0
  98. package/lib/semantic/shared-publication.js +592 -0
  99. package/lib/semantic/shared-source-config.js +183 -0
  100. package/lib/semantic/shared-source-connect.js +139 -0
  101. package/lib/semantic/shared-source-discovery.js +98 -0
  102. package/lib/semantic/shared-sync-export.js +413 -0
  103. package/lib/semantic/shared-sync-intake.js +592 -0
  104. package/lib/semantic/shared-sync-merge.js +547 -0
  105. package/lib/semantic/shared-sync-release.js +463 -0
  106. package/lib/semantic/supreme-intent-report.js +300 -0
  107. package/lib/state/sce-state-store.js +1360 -0
  108. package/lib/steering/context-sync-manager.js +276 -25
  109. package/lib/studio/spec-intake-governor.js +39 -3
  110. package/lib/studio/task-envelope.js +35 -2
  111. package/lib/workspace/takeover-baseline.js +342 -83
  112. package/package.json +7 -2
  113. package/scripts/agent-governance-baseline-audit.js +395 -0
  114. package/scripts/clarification-first-audit.js +9 -9
  115. package/scripts/deprecated-entry-audit.js +240 -0
  116. package/scripts/release-doc-version-audit.js +24 -0
  117. package/scripts/release-posture-report.js +262 -0
  118. package/template/.sce/README.md +62 -228
  119. package/template/.sce/config/semantic-shared-sources.json +5 -0
  120. package/template/.sce/config/supreme-principles-policy.json +105 -0
  121. package/template/.sce/config/takeover-baseline.json +7 -0
  122. package/template/.sce/steering/CORE_PRINCIPLES.md +23 -63
  123. package/template/.sce/steering/CURRENT_CONTEXT.md +4 -0
  124. package/template/.sce/steering/RULES_GUIDE.md +17 -9
  125. package/template/README.md +32 -96
@@ -0,0 +1,406 @@
1
+ const DEFAULT_PROJECT_ID = 'project';
2
+ const DEFAULT_CHANNEL_ID = 'channel-default';
3
+ const DEFAULT_ACTIVE_SCENE = 'scene.default';
4
+ const DEFAULT_ACTIVE_SPEC_ID = 'spec.generic';
5
+ const DEFAULT_ACTIVE_DOC = 'requirements';
6
+ const DEFAULT_RUN_STATE = 'idle';
7
+ const RUN_STATE_VALUES = new Set(['idle', 'running', 'blocked', 'completed', 'failed']);
8
+
9
+ function nowIso() {
10
+ return new Date().toISOString();
11
+ }
12
+
13
+ function normalizeString(value) {
14
+ if (typeof value !== 'string') {
15
+ return '';
16
+ }
17
+ return value.trim();
18
+ }
19
+
20
+ function normalizeDraftInput(value) {
21
+ if (value == null) {
22
+ return '';
23
+ }
24
+ return typeof value === 'string' ? value : `${value}`;
25
+ }
26
+
27
+ function normalizeStringArray(values) {
28
+ if (!Array.isArray(values)) {
29
+ return [];
30
+ }
31
+
32
+ const seen = new Set();
33
+ const normalized = [];
34
+ for (const value of values) {
35
+ const item = normalizeString(value);
36
+ if (!item || seen.has(item)) {
37
+ continue;
38
+ }
39
+ seen.add(item);
40
+ normalized.push(item);
41
+ }
42
+ return normalized;
43
+ }
44
+
45
+ function safePathSegment(value, fallback) {
46
+ const normalized = `${value || ''}`
47
+ .trim()
48
+ .replace(/[^a-zA-Z0-9._-]+/g, '-')
49
+ .replace(/^-+|-+$/g, '');
50
+ return normalized || fallback;
51
+ }
52
+
53
+ function normalizeRunState(value, fallback = DEFAULT_RUN_STATE) {
54
+ const normalized = normalizeString(value).toLowerCase();
55
+ if (!RUN_STATE_VALUES.has(normalized)) {
56
+ return fallback;
57
+ }
58
+ return normalized;
59
+ }
60
+
61
+ function normalizeDateTime(value, fallback = null) {
62
+ const candidate = normalizeString(value);
63
+ if (!candidate) {
64
+ return fallback || nowIso();
65
+ }
66
+
67
+ const parsed = Date.parse(candidate);
68
+ if (!Number.isFinite(parsed)) {
69
+ return fallback || nowIso();
70
+ }
71
+ return new Date(parsed).toISOString();
72
+ }
73
+
74
+ function pickFirstString(candidates = [], fallback = '') {
75
+ for (const candidate of candidates) {
76
+ const normalized = normalizeString(candidate);
77
+ if (normalized) {
78
+ return normalized;
79
+ }
80
+ }
81
+ return fallback;
82
+ }
83
+
84
+ function normalizeAgentRuntime(agentRuntime) {
85
+ if (!agentRuntime || typeof agentRuntime !== 'object') {
86
+ return null;
87
+ }
88
+
89
+ const agentId = normalizeString(agentRuntime.agentId);
90
+ const backendProfile = normalizeString(agentRuntime.backendProfile);
91
+ const runtimeSessionId = normalizeString(agentRuntime.runtimeSessionId);
92
+
93
+ if (!agentId || !backendProfile || !runtimeSessionId) {
94
+ return null;
95
+ }
96
+
97
+ return {
98
+ agentId,
99
+ backendProfile,
100
+ runtimeSessionId
101
+ };
102
+ }
103
+
104
+ function buildDefaultSessionPath(projectId, channelId) {
105
+ const safeProjectId = safePathSegment(projectId, DEFAULT_PROJECT_ID);
106
+ const safeChannelId = safePathSegment(channelId, DEFAULT_CHANNEL_ID);
107
+ return `.sce/sessions/${safeProjectId}/${safeChannelId}/session.json`;
108
+ }
109
+
110
+ function createDefaultChannelState(channelId, options = {}) {
111
+ const defaults = options.defaults && typeof options.defaults === 'object'
112
+ ? options.defaults
113
+ : {};
114
+ const normalizedChannelId = pickFirstString([channelId, defaults.channelId], DEFAULT_CHANNEL_ID);
115
+ const projectId = pickFirstString([options.projectId, defaults.projectId], DEFAULT_PROJECT_ID);
116
+ const activeSpecId = pickFirstString(
117
+ [options.activeSpecId, defaults.activeSpecId],
118
+ DEFAULT_ACTIVE_SPEC_ID
119
+ );
120
+ const activeDoc = pickFirstString(
121
+ [options.activeDoc, defaults.activeDoc],
122
+ DEFAULT_ACTIVE_DOC
123
+ );
124
+ const updatedAt = normalizeDateTime(
125
+ options.updatedAt || defaults.updatedAt,
126
+ normalizeDateTime(options.now)
127
+ );
128
+
129
+ const state = {
130
+ channelId: normalizedChannelId,
131
+ activeScene: pickFirstString(
132
+ [options.activeScene, defaults.activeScene],
133
+ DEFAULT_ACTIVE_SCENE
134
+ ),
135
+ activeSpecId,
136
+ activeDoc,
137
+ activeSessionPath: pickFirstString(
138
+ [options.activeSessionPath, defaults.activeSessionPath],
139
+ buildDefaultSessionPath(projectId, normalizedChannelId)
140
+ ),
141
+ openedTabs: normalizeStringArray(
142
+ options.openedTabs !== undefined ? options.openedTabs : defaults.openedTabs
143
+ ),
144
+ selectedTreeNodeId: pickFirstString(
145
+ [options.selectedTreeNodeId, defaults.selectedTreeNodeId],
146
+ `spec:${activeSpecId}`
147
+ ),
148
+ draftInput: normalizeDraftInput(
149
+ options.draftInput !== undefined ? options.draftInput : defaults.draftInput
150
+ ),
151
+ runState: normalizeRunState(
152
+ options.runState !== undefined ? options.runState : defaults.runState,
153
+ DEFAULT_RUN_STATE
154
+ ),
155
+ updatedAt
156
+ };
157
+
158
+ const agentRuntime = normalizeAgentRuntime(
159
+ options.agentRuntime !== undefined ? options.agentRuntime : defaults.agentRuntime
160
+ );
161
+ if (agentRuntime) {
162
+ state.agentRuntime = agentRuntime;
163
+ }
164
+
165
+ return state;
166
+ }
167
+
168
+ function normalizeChannelState(channelId, state = {}, options = {}) {
169
+ const payload = state && typeof state === 'object' ? state : {};
170
+ const defaults = options.defaults && typeof options.defaults === 'object'
171
+ ? options.defaults
172
+ : {};
173
+ const normalizedChannelId = pickFirstString(
174
+ [payload.channelId, channelId, defaults.channelId],
175
+ DEFAULT_CHANNEL_ID
176
+ );
177
+
178
+ const mergedAgentRuntime = payload.agentRuntime && defaults.agentRuntime
179
+ ? {
180
+ ...defaults.agentRuntime,
181
+ ...payload.agentRuntime
182
+ }
183
+ : (payload.agentRuntime || defaults.agentRuntime);
184
+
185
+ return createDefaultChannelState(normalizedChannelId, {
186
+ projectId: options.projectId,
187
+ now: options.now,
188
+ defaults,
189
+ activeScene: payload.activeScene,
190
+ activeSpecId: payload.activeSpecId,
191
+ activeDoc: payload.activeDoc,
192
+ activeSessionPath: payload.activeSessionPath,
193
+ openedTabs: payload.openedTabs,
194
+ selectedTreeNodeId: payload.selectedTreeNodeId,
195
+ draftInput: payload.draftInput,
196
+ runState: payload.runState,
197
+ updatedAt: payload.updatedAt,
198
+ agentRuntime: mergedAgentRuntime
199
+ });
200
+ }
201
+
202
+ function chooseFocusedChannelId(channels, focusedChannelId, preferredChannelId = '') {
203
+ if (!channels || typeof channels !== 'object') {
204
+ return '';
205
+ }
206
+
207
+ const ids = Object.keys(channels).filter(Boolean);
208
+ if (ids.length === 0) {
209
+ return '';
210
+ }
211
+
212
+ const normalizedFocused = normalizeString(focusedChannelId);
213
+ if (normalizedFocused && channels[normalizedFocused]) {
214
+ return normalizedFocused;
215
+ }
216
+
217
+ const normalizedPreferred = normalizeString(preferredChannelId);
218
+ if (normalizedPreferred && channels[normalizedPreferred]) {
219
+ return normalizedPreferred;
220
+ }
221
+
222
+ return [...ids].sort((left, right) => left.localeCompare(right))[0];
223
+ }
224
+
225
+ function normalizeProjectChannelContext(context = {}, options = {}) {
226
+ const payload = context && typeof context === 'object' ? context : {};
227
+ const projectId = pickFirstString(
228
+ [payload.projectId, options.projectId],
229
+ DEFAULT_PROJECT_ID
230
+ );
231
+ const allowDefaultChannelSynthesis = options.allowDefaultChannelSynthesis !== false;
232
+ const defaultChannelId = pickFirstString(
233
+ [options.defaultChannelId, payload.focusedChannelId],
234
+ DEFAULT_CHANNEL_ID
235
+ );
236
+ const channelDefaults = options.channelDefaults && typeof options.channelDefaults === 'object'
237
+ ? options.channelDefaults
238
+ : {};
239
+
240
+ const nextChannels = {};
241
+ const sourceChannels = payload.channels && typeof payload.channels === 'object'
242
+ ? payload.channels
243
+ : {};
244
+
245
+ for (const [channelKey, rawState] of Object.entries(sourceChannels)) {
246
+ const resolvedDefaults = channelDefaults[channelKey] && typeof channelDefaults[channelKey] === 'object'
247
+ ? channelDefaults[channelKey]
248
+ : {};
249
+ const channelState = normalizeChannelState(channelKey, rawState, {
250
+ projectId,
251
+ now: options.now,
252
+ defaults: resolvedDefaults
253
+ });
254
+ nextChannels[channelState.channelId] = channelState;
255
+ }
256
+
257
+ if (Object.keys(nextChannels).length === 0 && allowDefaultChannelSynthesis) {
258
+ const resolvedDefaults = channelDefaults[defaultChannelId] && typeof channelDefaults[defaultChannelId] === 'object'
259
+ ? channelDefaults[defaultChannelId]
260
+ : {};
261
+ const defaultState = createDefaultChannelState(defaultChannelId, {
262
+ projectId,
263
+ now: options.now,
264
+ defaults: resolvedDefaults
265
+ });
266
+ nextChannels[defaultState.channelId] = defaultState;
267
+ }
268
+
269
+ const focusedChannelId = chooseFocusedChannelId(
270
+ nextChannels,
271
+ payload.focusedChannelId,
272
+ defaultChannelId
273
+ );
274
+
275
+ return {
276
+ projectId,
277
+ focusedChannelId,
278
+ channels: nextChannels
279
+ };
280
+ }
281
+
282
+ function listChannelIds(context = {}) {
283
+ const normalized = normalizeProjectChannelContext(context);
284
+ return Object.keys(normalized.channels).sort((left, right) => left.localeCompare(right));
285
+ }
286
+
287
+ function getChannelState(context = {}, channelId) {
288
+ const normalized = normalizeProjectChannelContext(context);
289
+ const normalizedChannelId = normalizeString(channelId);
290
+ if (!normalizedChannelId || !normalized.channels[normalizedChannelId]) {
291
+ return null;
292
+ }
293
+ return normalized.channels[normalizedChannelId];
294
+ }
295
+
296
+ function getFocusedChannelState(context = {}) {
297
+ const normalized = normalizeProjectChannelContext(context);
298
+ return normalized.channels[normalized.focusedChannelId] || null;
299
+ }
300
+
301
+ function setFocusedChannel(context = {}, channelId, options = {}) {
302
+ const requestedChannelId = normalizeString(channelId);
303
+ if (!requestedChannelId) {
304
+ throw new Error('channelId is required');
305
+ }
306
+
307
+ const normalized = normalizeProjectChannelContext(context, {
308
+ projectId: options.projectId,
309
+ defaultChannelId: options.defaultChannelId,
310
+ now: options.now,
311
+ channelDefaults: options.channelDefaults
312
+ });
313
+
314
+ const nextChannels = { ...normalized.channels };
315
+ if (!nextChannels[requestedChannelId]) {
316
+ if (options.createIfMissing !== true) {
317
+ throw new Error(`Channel not found: ${requestedChannelId}`);
318
+ }
319
+
320
+ const defaults = options.channelDefaults && options.channelDefaults[requestedChannelId]
321
+ ? options.channelDefaults[requestedChannelId]
322
+ : (options.channelState || {});
323
+ nextChannels[requestedChannelId] = normalizeChannelState(requestedChannelId, defaults, {
324
+ projectId: normalized.projectId,
325
+ now: options.now,
326
+ defaults
327
+ });
328
+ }
329
+
330
+ return {
331
+ projectId: normalized.projectId,
332
+ focusedChannelId: requestedChannelId,
333
+ channels: nextChannels
334
+ };
335
+ }
336
+
337
+ function updateChannelState(context = {}, channelId, patch = {}, options = {}) {
338
+ const requestedChannelId = pickFirstString([channelId, patch && patch.channelId], '');
339
+ if (!requestedChannelId) {
340
+ throw new Error('channelId is required');
341
+ }
342
+
343
+ const normalized = normalizeProjectChannelContext(context, {
344
+ projectId: options.projectId,
345
+ defaultChannelId: options.defaultChannelId,
346
+ now: options.now,
347
+ channelDefaults: options.channelDefaults
348
+ });
349
+ const currentState = normalized.channels[requestedChannelId] || null;
350
+ const rawPatch = patch && typeof patch === 'object' ? patch : {};
351
+
352
+ const mergedState = currentState
353
+ ? {
354
+ ...currentState,
355
+ ...rawPatch,
356
+ channelId: requestedChannelId,
357
+ updatedAt: rawPatch.updatedAt !== undefined ? rawPatch.updatedAt : (options.now || currentState.updatedAt),
358
+ agentRuntime: rawPatch.agentRuntime && currentState.agentRuntime
359
+ ? { ...currentState.agentRuntime, ...rawPatch.agentRuntime }
360
+ : (rawPatch.agentRuntime !== undefined ? rawPatch.agentRuntime : currentState.agentRuntime)
361
+ }
362
+ : {
363
+ ...rawPatch,
364
+ channelId: requestedChannelId,
365
+ updatedAt: rawPatch.updatedAt !== undefined ? rawPatch.updatedAt : options.now
366
+ };
367
+
368
+ const defaults = options.channelDefaults && options.channelDefaults[requestedChannelId]
369
+ ? options.channelDefaults[requestedChannelId]
370
+ : {};
371
+ const nextChannelState = normalizeChannelState(requestedChannelId, mergedState, {
372
+ projectId: normalized.projectId,
373
+ now: options.now,
374
+ defaults
375
+ });
376
+
377
+ return {
378
+ projectId: normalized.projectId,
379
+ focusedChannelId: normalized.focusedChannelId,
380
+ channels: {
381
+ ...normalized.channels,
382
+ [requestedChannelId]: nextChannelState
383
+ }
384
+ };
385
+ }
386
+
387
+ module.exports = {
388
+ DEFAULT_PROJECT_ID,
389
+ DEFAULT_CHANNEL_ID,
390
+ DEFAULT_ACTIVE_SCENE,
391
+ DEFAULT_ACTIVE_SPEC_ID,
392
+ DEFAULT_ACTIVE_DOC,
393
+ DEFAULT_RUN_STATE,
394
+ RUN_STATE_VALUES,
395
+ buildDefaultSessionPath,
396
+ chooseFocusedChannelId,
397
+ createDefaultChannelState,
398
+ getChannelState,
399
+ getFocusedChannelState,
400
+ listChannelIds,
401
+ normalizeAgentRuntime,
402
+ normalizeChannelState,
403
+ normalizeProjectChannelContext,
404
+ setFocusedChannel,
405
+ updateChannelState
406
+ };
@@ -1,6 +1,7 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs-extra');
3
3
  const { SessionStore } = require('./session-store');
4
+ const { buildProjectChannelOutput } = require('../project/project-channel-output');
4
5
 
5
6
  function normalizeString(value) {
6
7
  if (typeof value !== 'string') {
@@ -44,6 +45,8 @@ async function resolveSpecSceneBinding(options = {}, dependencies = {}) {
44
45
  const fileSystem = dependencies.fileSystem || fs;
45
46
  const sessionStore = dependencies.sessionStore || new SessionStore(projectPath);
46
47
  const explicitSceneId = normalizeString(options.sceneId || options.scene);
48
+ const projectId = normalizeString(options.projectId);
49
+ const collabChannel = normalizeString(options.collabChannel || options.channelId || options.channel);
47
50
  const allowNoScene = options.allowNoScene !== false;
48
51
 
49
52
  if (explicitSceneId) {
@@ -60,6 +63,49 @@ async function resolveSpecSceneBinding(options = {}, dependencies = {}) {
60
63
  };
61
64
  }
62
65
 
66
+ if (projectId || collabChannel) {
67
+ if (!projectId || !collabChannel) {
68
+ throw new Error('Both projectId and collabChannel are required for project-channel scene binding.');
69
+ }
70
+
71
+ const resolved = await sessionStore.getProjectChannelSession(projectId, collabChannel);
72
+ if (!resolved.contextAvailable) {
73
+ throw new Error(`Project channel context unavailable: ${projectId} / ${collabChannel}.`);
74
+ }
75
+ if (!resolved.channel) {
76
+ throw new Error(`Project channel unavailable: ${projectId} / ${collabChannel}.`);
77
+ }
78
+
79
+ const activeScene = normalizeString(resolved.channel.activeScene);
80
+ if (!activeScene) {
81
+ throw new Error(`Project channel has no active scene: ${projectId} / ${collabChannel}.`);
82
+ }
83
+
84
+ const active = await sessionStore.getActiveSceneSession(activeScene);
85
+ if (!active) {
86
+ throw new Error(`Project channel points to scene "${activeScene}" but no active scene session exists. Run "sce studio plan --project-id ${projectId} --collab-channel ${collabChannel} --from-chat <session>" first.`);
87
+ }
88
+
89
+ return {
90
+ source: 'project-channel',
91
+ scene_id: activeScene,
92
+ scene_cycle: active.scene_cycle,
93
+ scene_session_id: active.session.session_id,
94
+ scene_session: active.session,
95
+ project_channel: buildProjectChannelOutput({
96
+ projectId,
97
+ canonicalProjectId: projectId,
98
+ requestedChannelId: collabChannel,
99
+ storageMode: resolved.storageMode,
100
+ contextAvailable: resolved.contextAvailable,
101
+ context: resolved.context,
102
+ channel: resolved.channel,
103
+ session: resolved.session,
104
+ resolvedChannelId: resolved.resolvedChannelId
105
+ })
106
+ };
107
+ }
108
+
63
109
  const studioJob = await loadLatestStudioJob(projectPath, fileSystem);
64
110
  const studioSceneId = normalizeString(studioJob && studioJob.scene && studioJob.scene.id);
65
111
  const studioSceneSessionId = normalizeString(studioJob && studioJob.session && studioJob.session.scene_session_id);
@@ -2,6 +2,7 @@ const fs = require('fs-extra');
2
2
  const path = require('path');
3
3
  const { SteeringContract, normalizeToolName } = require('./steering-contract');
4
4
  const { getSceStateStore } = require('../state/sce-state-store');
5
+ const { ProjectChannelContextStore } = require('./project-channel-context-store');
5
6
 
6
7
  const SESSION_SCHEMA_VERSION = '1.0';
7
8
  const SESSION_DIR = path.join('.sce', 'sessions');
@@ -43,6 +44,46 @@ function nextSnapshotId(session) {
43
44
  return `snap-${snapshots.length + 1}`;
44
45
  }
45
46
 
47
+ function normalizeString(value) {
48
+ if (typeof value !== 'string') {
49
+ return '';
50
+ }
51
+ return value.trim();
52
+ }
53
+
54
+ function mapSessionStatusToChannelRunState(status) {
55
+ const normalized = normalizeString(status).toLowerCase();
56
+ if (!normalized) {
57
+ return 'idle';
58
+ }
59
+ if (normalized === 'active' || normalized === 'running') {
60
+ return 'running';
61
+ }
62
+ if (normalized === 'paused' || normalized === 'blocked') {
63
+ return 'blocked';
64
+ }
65
+ if (normalized === 'completed') {
66
+ return 'completed';
67
+ }
68
+ if (normalized === 'failed' || normalized === 'timeout' || normalized === 'rolled_back') {
69
+ return 'failed';
70
+ }
71
+ return 'idle';
72
+ }
73
+
74
+ function toSessionFileRelativePath(sessionId) {
75
+ return path.join(SESSION_DIR, `${sessionId}.json`).replace(/\\/g, '/');
76
+ }
77
+
78
+ function sessionIdFromRelativePath(sessionPath) {
79
+ const normalized = normalizeString(sessionPath);
80
+ if (!normalized) {
81
+ return '';
82
+ }
83
+ const parsed = path.posix.parse(normalized.replace(/\\/g, '/'));
84
+ return parsed.ext === '.json' ? parsed.name : '';
85
+ }
86
+
46
87
  class SessionStore {
47
88
  constructor(workspaceRoot, steeringContract = null, options = {}) {
48
89
  this._workspaceRoot = workspaceRoot;
@@ -57,6 +98,8 @@ class SessionStore {
57
98
  env: this._env,
58
99
  sqliteModule: options.sqliteModule
59
100
  });
101
+ this._projectChannelContextStore = options.projectChannelContextStore
102
+ || new ProjectChannelContextStore(workspaceRoot, this._fileSystem);
60
103
  this._preferSqliteSceneReads = options.preferSqliteSceneReads !== undefined
61
104
  ? options.preferSqliteSceneReads === true
62
105
  : true;
@@ -197,6 +240,149 @@ class SessionStore {
197
240
  return records;
198
241
  }
199
242
 
243
+ async bindSessionToProjectChannel(sessionRef = 'latest', options = {}) {
244
+ const projectId = normalizeString(options.projectId);
245
+ const channelId = normalizeString(options.channelId);
246
+ if (!projectId) {
247
+ throw new Error('projectId is required for bindSessionToProjectChannel');
248
+ }
249
+ if (!channelId) {
250
+ throw new Error('channelId is required for bindSessionToProjectChannel');
251
+ }
252
+
253
+ const { session } = await this._resolveSession(sessionRef);
254
+ const updatedAt = normalizeString(session.updated_at) || nowIso();
255
+ const patch = {
256
+ activeSessionPath: toSessionFileRelativePath(session.session_id),
257
+ activeScene: normalizeString(options.activeScene)
258
+ || normalizeString(session && session.scene && session.scene.id),
259
+ activeSpecId: normalizeString(options.activeSpecId)
260
+ || normalizeString(session && session.scene && session.scene.spec_id),
261
+ activeDoc: normalizeString(options.activeDoc),
262
+ selectedTreeNodeId: normalizeString(options.selectedTreeNodeId),
263
+ draftInput: options.draftInput !== undefined ? options.draftInput : undefined,
264
+ openedTabs: Array.isArray(options.openedTabs) ? options.openedTabs : undefined,
265
+ runState: normalizeString(options.runState) || mapSessionStatusToChannelRunState(session.status),
266
+ updatedAt
267
+ };
268
+
269
+ const sanitizedPatch = {};
270
+ for (const [key, value] of Object.entries(patch)) {
271
+ if (value !== undefined && value !== '') {
272
+ sanitizedPatch[key] = value;
273
+ }
274
+ }
275
+
276
+ let context = await this._projectChannelContextStore.updateChannel(
277
+ projectId,
278
+ channelId,
279
+ sanitizedPatch,
280
+ {
281
+ now: updatedAt,
282
+ syncCurrentContext: false
283
+ }
284
+ );
285
+
286
+ const shouldFocus = options.focus !== false;
287
+ if (shouldFocus) {
288
+ context = await this._projectChannelContextStore.focusChannel(projectId, channelId, {
289
+ syncCurrentContext: false
290
+ });
291
+ }
292
+
293
+ if (options.syncCurrentContext === true) {
294
+ await this._projectChannelContextStore.syncCurrentContext(projectId, {
295
+ projectChannelContext: context
296
+ });
297
+ }
298
+
299
+ return {
300
+ session,
301
+ context,
302
+ channel: context.channels[channelId]
303
+ };
304
+ }
305
+
306
+ async getProjectChannelSession(projectId, channelId = null, options = {}) {
307
+ const normalizedProjectId = normalizeString(projectId);
308
+ const requestedChannelId = normalizeString(channelId) || null;
309
+ if (!normalizedProjectId) {
310
+ throw new Error('projectId is required for getProjectChannelSession');
311
+ }
312
+
313
+ const detection = await this._projectChannelContextStore.detectPersistedContext(normalizedProjectId);
314
+ if (!detection.available) {
315
+ return {
316
+ projectId: normalizedProjectId,
317
+ requestedChannelId,
318
+ resolvedChannelId: null,
319
+ contextAvailable: false,
320
+ storageMode: detection.storageMode,
321
+ context: null,
322
+ channel: null,
323
+ session: null
324
+ };
325
+ }
326
+
327
+ const context = await this._projectChannelContextStore.load(normalizedProjectId, {
328
+ ...options,
329
+ allowDefaultChannelSynthesis: false
330
+ });
331
+ const resolvedChannelId = requestedChannelId || normalizeString(context.focusedChannelId);
332
+ if (!resolvedChannelId || !context.channels[resolvedChannelId]) {
333
+ return {
334
+ projectId: normalizedProjectId,
335
+ requestedChannelId,
336
+ resolvedChannelId: null,
337
+ contextAvailable: true,
338
+ storageMode: detection.storageMode,
339
+ context,
340
+ channel: null,
341
+ session: null
342
+ };
343
+ }
344
+
345
+ const channel = context.channels[resolvedChannelId];
346
+ const sessionId = sessionIdFromRelativePath(channel.activeSessionPath);
347
+ if (!sessionId) {
348
+ return {
349
+ projectId: normalizedProjectId,
350
+ requestedChannelId,
351
+ resolvedChannelId,
352
+ contextAvailable: true,
353
+ storageMode: detection.storageMode,
354
+ context,
355
+ channel,
356
+ session: null
357
+ };
358
+ }
359
+
360
+ try {
361
+ const session = await this.getSession(sessionId);
362
+ return {
363
+ projectId: normalizedProjectId,
364
+ requestedChannelId,
365
+ resolvedChannelId,
366
+ contextAvailable: true,
367
+ storageMode: detection.storageMode,
368
+ context,
369
+ channel,
370
+ session
371
+ };
372
+ } catch (_error) {
373
+ return {
374
+ projectId: normalizedProjectId,
375
+ requestedChannelId,
376
+ resolvedChannelId,
377
+ contextAvailable: true,
378
+ storageMode: detection.storageMode,
379
+ context,
380
+ channel,
381
+ session: null
382
+ };
383
+ }
384
+ }
385
+
200
386
  async beginSceneSession(options = {}) {
201
387
  const sceneId = `${options.sceneId || ''}`.trim();
202
388
  if (!sceneId) {
@@ -24,6 +24,7 @@ const DEFAULT_MANIFEST = Object.freeze({
24
24
  codex: '>=0.0.0',
25
25
  claude: '>=0.0.0',
26
26
  cursor: '>=0.0.0',
27
+ 'sce-native': '*',
27
28
  generic: '*',
28
29
  },
29
30
  session: {
@@ -58,6 +59,11 @@ const TOOL_RUNTIME_PROFILES = Object.freeze({
58
59
  default_permission_args: [],
59
60
  notes: 'Provide runtime permission args in the target agent adapter.',
60
61
  },
62
+ 'sce-native': {
63
+ recommended_command: 'sce native start',
64
+ default_permission_args: [],
65
+ notes: 'Native SCE semantic runtime with no embedded host dependency.',
66
+ },
61
67
  });
62
68
 
63
69
  function normalizeToolName(value) {
@@ -68,7 +74,7 @@ function normalizeToolName(value) {
68
74
  if (raw === 'claude-code') {
69
75
  return 'claude';
70
76
  }
71
- if (raw === 'codex' || raw === 'claude' || raw === 'cursor') {
77
+ if (raw === 'codex' || raw === 'claude' || raw === 'cursor' || raw === 'sce-native') {
72
78
  return raw;
73
79
  }
74
80
  return 'generic';