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.
@@ -11,9 +11,13 @@ const { ContextCollector } = require('../spec/bootstrap/context-collector');
11
11
  const { QuestionnaireEngine } = require('../spec/bootstrap/questionnaire-engine');
12
12
  const { DraftGenerator } = require('../spec/bootstrap/draft-generator');
13
13
  const { TraceEmitter } = require('../spec/bootstrap/trace-emitter');
14
+ const { SessionStore } = require('../runtime/session-store');
15
+ const { resolveSpecSceneBinding } = require('../runtime/scene-session-binding');
16
+ const { bindMultiSpecSceneSession } = require('../runtime/multi-spec-scene-session');
14
17
 
15
18
  async function runSpecBootstrap(options = {}, dependencies = {}) {
16
19
  const projectPath = dependencies.projectPath || process.cwd();
20
+ const sessionStore = dependencies.sessionStore || new SessionStore(projectPath);
17
21
 
18
22
  const specTargets = parseSpecTargets({
19
23
  spec: options.spec || options.name,
@@ -22,13 +26,24 @@ async function runSpecBootstrap(options = {}, dependencies = {}) {
22
26
 
23
27
  if (specTargets.length > 1) {
24
28
  const executeOrchestration = dependencies.runOrchestration || runOrchestration;
25
- return runMultiSpecViaOrchestrate({
29
+ return bindMultiSpecSceneSession({
26
30
  specTargets,
27
- projectPath,
28
- commandOptions: options,
29
- runOrchestration: executeOrchestration,
31
+ sceneId: options.scene,
32
+ commandName: 'spec-bootstrap',
30
33
  commandLabel: 'Multi-spec bootstrap',
31
- nextActionLabel: 'Multi-spec bootstrap defaulted to orchestrate mode.'
34
+ commandOptions: options,
35
+ runViaOrchestrate: () => runMultiSpecViaOrchestrate({
36
+ specTargets,
37
+ projectPath,
38
+ commandOptions: options,
39
+ runOrchestration: executeOrchestration,
40
+ commandLabel: 'Multi-spec bootstrap',
41
+ nextActionLabel: 'Multi-spec bootstrap defaulted to orchestrate mode.'
42
+ })
43
+ }, {
44
+ projectPath,
45
+ fileSystem: dependencies.fileSystem || fs,
46
+ sessionStore
32
47
  });
33
48
  }
34
49
 
@@ -57,61 +72,120 @@ async function runSpecBootstrap(options = {}, dependencies = {}) {
57
72
  throw new Error('Spec name is required');
58
73
  }
59
74
 
60
- const draft = draftGenerator.generate({
61
- specName,
62
- profile: options.profile || 'general',
63
- template: options.template || 'default',
64
- context,
65
- answers
75
+ const sceneBinding = await resolveSpecSceneBinding({
76
+ sceneId: options.scene,
77
+ allowNoScene: false
78
+ }, {
79
+ projectPath,
80
+ fileSystem: dependencies.fileSystem || fs,
81
+ sessionStore
66
82
  });
67
83
 
68
- const specPath = path.join(projectPath, '.sce', 'specs', specName);
69
- const files = {
70
- requirements: path.join(specPath, 'requirements.md'),
71
- design: path.join(specPath, 'design.md'),
72
- tasks: path.join(specPath, 'tasks.md')
73
- };
74
-
75
- if (!options.dryRun) {
76
- await fs.ensureDir(specPath);
77
- await fs.writeFile(files.requirements, draft.requirements, 'utf8');
78
- await fs.writeFile(files.design, draft.design, 'utf8');
79
- await fs.writeFile(files.tasks, draft.tasks, 'utf8');
84
+ let specSession = null;
85
+ if (sceneBinding && !options.dryRun) {
86
+ const linked = await sessionStore.startSpecSession({
87
+ sceneId: sceneBinding.scene_id,
88
+ specId: specName,
89
+ objective: `Spec bootstrap: ${specName}`
90
+ });
91
+ specSession = linked.spec_session;
80
92
  }
81
93
 
82
- const result = {
83
- success: true,
84
- specName,
85
- specPath: path.relative(projectPath, specPath),
86
- dryRun: !!options.dryRun,
87
- files: {
88
- requirements: path.relative(projectPath, files.requirements),
89
- design: path.relative(projectPath, files.design),
90
- tasks: path.relative(projectPath, files.tasks)
91
- },
92
- trace: {
93
- template: options.template || 'default',
94
+ try {
95
+ const draft = draftGenerator.generate({
96
+ specName,
94
97
  profile: options.profile || 'general',
95
- parameters: {
96
- nonInteractive: !!options.nonInteractive,
97
- dryRun: !!options.dryRun,
98
- json: !!options.json
98
+ template: options.template || 'default',
99
+ context,
100
+ answers
101
+ });
102
+
103
+ const specPath = path.join(projectPath, '.sce', 'specs', specName);
104
+ const files = {
105
+ requirements: path.join(specPath, 'requirements.md'),
106
+ design: path.join(specPath, 'design.md'),
107
+ tasks: path.join(specPath, 'tasks.md')
108
+ };
109
+
110
+ if (!options.dryRun) {
111
+ await fs.ensureDir(specPath);
112
+ await fs.writeFile(files.requirements, draft.requirements, 'utf8');
113
+ await fs.writeFile(files.design, draft.design, 'utf8');
114
+ await fs.writeFile(files.tasks, draft.tasks, 'utf8');
115
+ }
116
+
117
+ const result = {
118
+ success: true,
119
+ specName,
120
+ specPath: path.relative(projectPath, specPath),
121
+ dryRun: !!options.dryRun,
122
+ files: {
123
+ requirements: path.relative(projectPath, files.requirements),
124
+ design: path.relative(projectPath, files.design),
125
+ tasks: path.relative(projectPath, files.tasks)
126
+ },
127
+ trace: {
128
+ template: options.template || 'default',
129
+ profile: options.profile || 'general',
130
+ parameters: {
131
+ nonInteractive: !!options.nonInteractive,
132
+ dryRun: !!options.dryRun,
133
+ json: !!options.json
134
+ },
135
+ context: {
136
+ totalSpecs: context.totalSpecs,
137
+ preferredLanguage: context.preferredLanguage
138
+ },
139
+ mapping: draft.metadata.mapping
99
140
  },
100
- context: {
101
- totalSpecs: context.totalSpecs,
102
- preferredLanguage: context.preferredLanguage
141
+ preview: {
142
+ requirements: draft.requirements,
143
+ design: draft.design,
144
+ tasks: draft.tasks
103
145
  },
104
- mapping: draft.metadata.mapping
105
- },
106
- preview: {
107
- requirements: draft.requirements,
108
- design: draft.design,
109
- tasks: draft.tasks
146
+ scene_session: sceneBinding
147
+ ? {
148
+ bound: true,
149
+ scene_id: sceneBinding.scene_id,
150
+ scene_cycle: sceneBinding.scene_cycle,
151
+ scene_session_id: sceneBinding.scene_session_id,
152
+ spec_session_id: specSession ? specSession.session_id : null,
153
+ binding_source: sceneBinding.source
154
+ }
155
+ : {
156
+ bound: false
157
+ }
158
+ };
159
+
160
+ if (specSession) {
161
+ await sessionStore.completeSpecSession({
162
+ specSessionRef: specSession.session_id,
163
+ status: 'completed',
164
+ summary: `Spec bootstrap completed: ${specName}`,
165
+ payload: {
166
+ command: 'spec-bootstrap',
167
+ spec: specName
168
+ }
169
+ });
110
170
  }
111
- };
112
171
 
113
- traceEmitter.emit(result, { json: options.json });
114
- return result;
172
+ traceEmitter.emit(result, { json: options.json });
173
+ return result;
174
+ } catch (error) {
175
+ if (specSession) {
176
+ await sessionStore.completeSpecSession({
177
+ specSessionRef: specSession.session_id,
178
+ status: 'failed',
179
+ summary: `Spec bootstrap failed: ${specName}`,
180
+ payload: {
181
+ command: 'spec-bootstrap',
182
+ spec: specName,
183
+ error: error.message
184
+ }
185
+ });
186
+ }
187
+ throw error;
188
+ }
115
189
  }
116
190
 
117
191
  function registerSpecBootstrapCommand(program) {
@@ -123,6 +197,7 @@ function registerSpecBootstrapCommand(program) {
123
197
  .option('--specs <names>', 'Comma-separated Spec identifiers (multi-spec defaults to orchestrate mode)')
124
198
  .option('--template <template-id>', 'Template hint for draft generation')
125
199
  .option('--profile <profile-id>', 'Profile for default generation strategy')
200
+ .option('--scene <scene-id>', 'Bind this spec bootstrap as a child session of an active scene')
126
201
  .option('--non-interactive', 'Disable prompts and use arguments/defaults only')
127
202
  .option('--dry-run', 'Preview generation result without writing files')
128
203
  .option('--json', 'Output machine-readable JSON')
@@ -12,9 +12,13 @@ const { RuleRegistry } = require('../spec-gate/rules/rule-registry');
12
12
  const { createDefaultRules } = require('../spec-gate/rules/default-rules');
13
13
  const { GateEngine } = require('../spec-gate/engine/gate-engine');
14
14
  const { ResultEmitter } = require('../spec-gate/result-emitter');
15
+ const { SessionStore } = require('../runtime/session-store');
16
+ const { resolveSpecSceneBinding } = require('../runtime/scene-session-binding');
17
+ const { bindMultiSpecSceneSession } = require('../runtime/multi-spec-scene-session');
15
18
 
16
19
  async function runSpecGate(options = {}, dependencies = {}) {
17
20
  const projectPath = dependencies.projectPath || process.cwd();
21
+ const sessionStore = dependencies.sessionStore || new SessionStore(projectPath);
18
22
  const specTargets = parseSpecTargets(options);
19
23
 
20
24
  if (specTargets.length === 0) {
@@ -23,13 +27,24 @@ async function runSpecGate(options = {}, dependencies = {}) {
23
27
 
24
28
  if (specTargets.length > 1) {
25
29
  const executeOrchestration = dependencies.runOrchestration || runOrchestration;
26
- return runMultiSpecViaOrchestrate({
30
+ return bindMultiSpecSceneSession({
27
31
  specTargets,
28
- projectPath,
29
- commandOptions: options,
30
- runOrchestration: executeOrchestration,
32
+ sceneId: options.scene,
33
+ commandName: 'spec-gate',
31
34
  commandLabel: 'Multi-spec gate',
32
- nextActionLabel: 'Multi-spec gate execution defaulted to orchestrate mode.'
35
+ commandOptions: options,
36
+ runViaOrchestrate: () => runMultiSpecViaOrchestrate({
37
+ specTargets,
38
+ projectPath,
39
+ commandOptions: options,
40
+ runOrchestration: executeOrchestration,
41
+ commandLabel: 'Multi-spec gate',
42
+ nextActionLabel: 'Multi-spec gate execution defaulted to orchestrate mode.'
43
+ })
44
+ }, {
45
+ projectPath,
46
+ fileSystem: dependencies.fileSystem || fs,
47
+ sessionStore
33
48
  });
34
49
  }
35
50
 
@@ -40,30 +55,81 @@ async function runSpecGate(options = {}, dependencies = {}) {
40
55
  throw new Error(`Spec not found: ${specId}`);
41
56
  }
42
57
 
43
- const policyLoader = dependencies.policyLoader || new PolicyLoader(projectPath);
44
- const policy = dependencies.policy || await policyLoader.load({
45
- policy: options.policy,
46
- strict: options.strict
58
+ const sceneBinding = await resolveSpecSceneBinding({
59
+ sceneId: options.scene,
60
+ allowNoScene: false
61
+ }, {
62
+ projectPath,
63
+ fileSystem: dependencies.fileSystem || fs,
64
+ sessionStore
47
65
  });
48
-
49
- const registry = dependencies.registry || new RuleRegistry(createDefaultRules(projectPath));
50
- const engine = dependencies.engine || new GateEngine({
51
- registry,
52
- policy
66
+ const linked = await sessionStore.startSpecSession({
67
+ sceneId: sceneBinding.scene_id,
68
+ specId,
69
+ objective: `Spec gate: ${specId}`
53
70
  });
71
+ const specSession = linked.spec_session;
54
72
 
55
- const result = await engine.evaluate({ specId });
56
- const emitter = dependencies.emitter || new ResultEmitter(projectPath);
57
- const emitted = await emitter.emit(result, {
58
- json: options.json,
59
- out: options.out,
60
- silent: options.silent
61
- });
73
+ const policyLoader = dependencies.policyLoader || new PolicyLoader(projectPath);
74
+ try {
75
+ const policy = dependencies.policy || await policyLoader.load({
76
+ policy: options.policy,
77
+ strict: options.strict
78
+ });
62
79
 
63
- return {
64
- ...result,
65
- report_path: emitted.outputPath
66
- };
80
+ const registry = dependencies.registry || new RuleRegistry(createDefaultRules(projectPath));
81
+ const engine = dependencies.engine || new GateEngine({
82
+ registry,
83
+ policy
84
+ });
85
+
86
+ const result = await engine.evaluate({ specId });
87
+ const emitter = dependencies.emitter || new ResultEmitter(projectPath);
88
+ const emitted = await emitter.emit(result, {
89
+ json: options.json,
90
+ out: options.out,
91
+ silent: options.silent
92
+ });
93
+
94
+ const decisionStatus = result.decision === 'no-go' ? 'failed' : 'completed';
95
+ await sessionStore.completeSpecSession({
96
+ specSessionRef: specSession.session_id,
97
+ status: decisionStatus,
98
+ summary: `Spec gate ${result.decision}: ${specId}`,
99
+ payload: {
100
+ command: 'spec-gate',
101
+ spec: specId,
102
+ decision: result.decision,
103
+ score: result.score,
104
+ report_path: emitted.outputPath || null
105
+ }
106
+ });
107
+
108
+ return {
109
+ ...result,
110
+ report_path: emitted.outputPath,
111
+ scene_session: {
112
+ bound: true,
113
+ scene_id: sceneBinding.scene_id,
114
+ scene_cycle: sceneBinding.scene_cycle,
115
+ scene_session_id: sceneBinding.scene_session_id,
116
+ spec_session_id: specSession.session_id,
117
+ binding_source: sceneBinding.source
118
+ }
119
+ };
120
+ } catch (error) {
121
+ await sessionStore.completeSpecSession({
122
+ specSessionRef: specSession.session_id,
123
+ status: 'failed',
124
+ summary: `Spec gate failed: ${specId}`,
125
+ payload: {
126
+ command: 'spec-gate',
127
+ spec: specId,
128
+ error: error.message
129
+ }
130
+ });
131
+ throw error;
132
+ }
67
133
  }
68
134
 
69
135
  async function generateSpecGatePolicyTemplate(options = {}, dependencies = {}) {
@@ -99,6 +165,7 @@ function registerSpecGateCommand(program) {
99
165
  .description('Execute gate checks for one or more Specs')
100
166
  .option('--spec <name>', 'Single Spec identifier')
101
167
  .option('--specs <names>', 'Comma-separated Spec identifiers (multi-spec defaults to orchestrate mode)')
168
+ .option('--scene <scene-id>', 'Bind this spec gate run as a child session of an active scene')
102
169
  .option('--policy <path>', 'Policy JSON path')
103
170
  .option('--strict', 'Enable strict mode override')
104
171
  .option('--json', 'Output machine-readable JSON')
@@ -10,9 +10,13 @@ const {
10
10
  const { PipelineStateStore } = require('../spec/pipeline/state-store');
11
11
  const { StageRunner } = require('../spec/pipeline/stage-runner');
12
12
  const { createDefaultStageAdapters } = require('../spec/pipeline/stage-adapters');
13
+ const { SessionStore } = require('../runtime/session-store');
14
+ const { resolveSpecSceneBinding } = require('../runtime/scene-session-binding');
15
+ const { bindMultiSpecSceneSession } = require('../runtime/multi-spec-scene-session');
13
16
 
14
17
  async function runSpecPipeline(options = {}, dependencies = {}) {
15
18
  const projectPath = dependencies.projectPath || process.cwd();
19
+ const sessionStore = dependencies.sessionStore || new SessionStore(projectPath);
16
20
  const specTargets = parseSpecTargets(options);
17
21
  if (specTargets.length === 0) {
18
22
  throw new Error('Either --spec or --specs is required');
@@ -20,13 +24,24 @@ async function runSpecPipeline(options = {}, dependencies = {}) {
20
24
 
21
25
  if (specTargets.length > 1) {
22
26
  const executeOrchestration = dependencies.runOrchestration || runOrchestration;
23
- return runMultiSpecViaOrchestrate({
27
+ return bindMultiSpecSceneSession({
24
28
  specTargets,
25
- projectPath,
26
- commandOptions: options,
27
- runOrchestration: executeOrchestration,
29
+ sceneId: options.scene,
30
+ commandName: 'spec-pipeline',
28
31
  commandLabel: 'Multi-spec pipeline',
29
- nextActionLabel: 'Multi-spec execution defaulted to orchestrate mode.'
32
+ commandOptions: options,
33
+ runViaOrchestrate: () => runMultiSpecViaOrchestrate({
34
+ specTargets,
35
+ projectPath,
36
+ commandOptions: options,
37
+ runOrchestration: executeOrchestration,
38
+ commandLabel: 'Multi-spec pipeline',
39
+ nextActionLabel: 'Multi-spec execution defaulted to orchestrate mode.'
40
+ })
41
+ }, {
42
+ projectPath,
43
+ fileSystem: dependencies.fileSystem || fs,
44
+ sessionStore
30
45
  });
31
46
  }
32
47
 
@@ -37,6 +52,25 @@ async function runSpecPipeline(options = {}, dependencies = {}) {
37
52
  throw new Error(`Spec not found: ${specId}`);
38
53
  }
39
54
 
55
+ const sceneBinding = await resolveSpecSceneBinding({
56
+ sceneId: options.scene,
57
+ allowNoScene: false
58
+ }, {
59
+ projectPath,
60
+ fileSystem: dependencies.fileSystem || fs,
61
+ sessionStore
62
+ });
63
+
64
+ let specSession = null;
65
+ if (sceneBinding && !options.dryRun) {
66
+ const linked = await sessionStore.startSpecSession({
67
+ sceneId: sceneBinding.scene_id,
68
+ specId,
69
+ objective: `Spec pipeline: ${specId}`
70
+ });
71
+ specSession = linked.spec_session;
72
+ }
73
+
40
74
  const stateStore = dependencies.stateStore || new PipelineStateStore(projectPath);
41
75
  const adapters = dependencies.adapters || createDefaultStageAdapters(projectPath);
42
76
  const stageRunner = dependencies.stageRunner || new StageRunner({
@@ -70,7 +104,24 @@ async function runSpecPipeline(options = {}, dependencies = {}) {
70
104
  state
71
105
  };
72
106
 
73
- const execution = await stageRunner.run(runContext);
107
+ let execution;
108
+ try {
109
+ execution = await stageRunner.run(runContext);
110
+ } catch (error) {
111
+ if (specSession) {
112
+ await sessionStore.completeSpecSession({
113
+ specSessionRef: specSession.session_id,
114
+ status: 'failed',
115
+ summary: `Spec pipeline failed: ${specId}`,
116
+ payload: {
117
+ command: 'spec-pipeline',
118
+ spec: specId,
119
+ error: error.message
120
+ }
121
+ });
122
+ }
123
+ throw error;
124
+ }
74
125
  await stateStore.markFinished(state, execution.status);
75
126
 
76
127
  const result = {
@@ -80,9 +131,36 @@ async function runSpecPipeline(options = {}, dependencies = {}) {
80
131
  stage_results: execution.stageResults,
81
132
  failure: execution.failure,
82
133
  next_actions: buildNextActions(execution),
83
- state_file: path.relative(projectPath, stateStore.getRunPath(specId, state.run_id))
134
+ state_file: path.relative(projectPath, stateStore.getRunPath(specId, state.run_id)),
135
+ scene_session: sceneBinding
136
+ ? {
137
+ bound: true,
138
+ scene_id: sceneBinding.scene_id,
139
+ scene_cycle: sceneBinding.scene_cycle,
140
+ scene_session_id: sceneBinding.scene_session_id,
141
+ spec_session_id: specSession ? specSession.session_id : null,
142
+ binding_source: sceneBinding.source
143
+ }
144
+ : {
145
+ bound: false
146
+ }
84
147
  };
85
148
 
149
+ if (specSession) {
150
+ await sessionStore.completeSpecSession({
151
+ specSessionRef: specSession.session_id,
152
+ status: execution.status === 'completed' ? 'completed' : 'failed',
153
+ summary: `Spec pipeline ${execution.status}: ${specId}`,
154
+ payload: {
155
+ command: 'spec-pipeline',
156
+ spec: specId,
157
+ run_id: state.run_id,
158
+ pipeline_status: execution.status,
159
+ failure: execution.failure || null
160
+ }
161
+ });
162
+ }
163
+
86
164
  if (options.out) {
87
165
  const outPath = path.isAbsolute(options.out)
88
166
  ? options.out
@@ -111,6 +189,7 @@ function registerSpecPipelineCommand(program) {
111
189
  .description('Execute pipeline stages for one or more Specs')
112
190
  .option('--spec <name>', 'Single Spec identifier')
113
191
  .option('--specs <names>', 'Comma-separated Spec identifiers (multi-spec defaults to orchestrate mode)')
192
+ .option('--scene <scene-id>', 'Bind this spec pipeline run as a child session of an active scene')
114
193
  .option('--from-stage <stage>', 'Start stage (requirements/design/tasks/gate)')
115
194
  .option('--to-stage <stage>', 'End stage (requirements/design/tasks/gate)')
116
195
  .option('--resume', 'Resume from latest unfinished stage state')