scene-capability-engine 3.4.5 → 3.5.0

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,6 +11,10 @@ const {
11
11
  const { findRelatedSpecs } = require('../spec/related-specs');
12
12
  const { captureTimelineCheckpoint } = require('../runtime/project-timeline');
13
13
  const { runProblemEvaluation } = require('../problem/problem-evaluator');
14
+ const {
15
+ runStudioAutoIntake,
16
+ runStudioSpecGovernance
17
+ } = require('../studio/spec-intake-governor');
14
18
 
15
19
  const STUDIO_JOB_API_VERSION = 'sce.studio.job/v0.1';
16
20
  const STAGE_ORDER = ['plan', 'generate', 'apply', 'verify', 'release'];
@@ -19,6 +23,8 @@ const STUDIO_EVENT_API_VERSION = 'sce.studio.event/v0.1';
19
23
  const VERIFY_PROFILES = new Set(['fast', 'standard', 'strict']);
20
24
  const RELEASE_PROFILES = new Set(['standard', 'strict']);
21
25
  const STUDIO_REPORTS_DIR = '.sce/reports/studio';
26
+ const DEFAULT_INTERACTIVE_GOVERNANCE_REPORT = '.sce/reports/interactive-governance-report.json';
27
+ const DEFAULT_PROBLEM_CONTRACT_RELATIVE_PATH = path.join('custom', 'problem-contract.json');
22
28
  const MAX_OUTPUT_PREVIEW_LENGTH = 2000;
23
29
  const DEFAULT_STUDIO_SECURITY_POLICY = Object.freeze({
24
30
  enabled: false,
@@ -368,6 +374,7 @@ async function buildVerifyGateSteps(options = {}, dependencies = {}) {
368
374
  const projectPath = dependencies.projectPath || process.cwd();
369
375
  const fileSystem = dependencies.fileSystem || fs;
370
376
  const profile = normalizeString(options.profile) || 'standard';
377
+ const specId = normalizeString(options.specId || dependencies.specId);
371
378
 
372
379
  if (!VERIFY_PROFILES.has(profile)) {
373
380
  throw new Error(`Invalid verify profile "${profile}". Expected one of: ${Array.from(VERIFY_PROFILES).join(', ')}`);
@@ -403,6 +410,21 @@ async function buildVerifyGateSteps(options = {}, dependencies = {}) {
403
410
  }
404
411
 
405
412
  if (profile === 'standard' || profile === 'strict') {
413
+ const problemClosureGateScript = path.join(projectPath, 'scripts', 'problem-closure-gate.js');
414
+ const hasProblemClosureGateScript = await fileSystem.pathExists(problemClosureGateScript);
415
+ const canRunProblemClosureGate = hasProblemClosureGateScript && Boolean(specId);
416
+ steps.push({
417
+ id: 'problem-closure-gate',
418
+ name: 'problem closure gate (verify)',
419
+ command: 'node',
420
+ args: ['scripts/problem-closure-gate.js', '--stage', 'verify', '--spec', specId, '--fail-on-block', '--json'],
421
+ required: Boolean(specId),
422
+ enabled: canRunProblemClosureGate,
423
+ skip_reason: canRunProblemClosureGate
424
+ ? ''
425
+ : (specId ? 'scripts/problem-closure-gate.js not found' : 'spec id unavailable for problem-closure gate')
426
+ });
427
+
406
428
  const governanceScript = path.join(projectPath, 'scripts', 'interactive-governance-report.js');
407
429
  const hasGovernanceScript = await fileSystem.pathExists(governanceScript);
408
430
  steps.push({
@@ -435,11 +457,32 @@ async function buildReleaseGateSteps(options = {}, dependencies = {}) {
435
457
  const projectPath = dependencies.projectPath || process.cwd();
436
458
  const fileSystem = dependencies.fileSystem || fs;
437
459
  const profile = normalizeString(options.profile) || 'standard';
460
+ const specId = normalizeString(options.specId || dependencies.specId);
461
+ const verifyReportPath = normalizeString(options.verifyReportPath || dependencies.verifyReportPath);
438
462
  if (!RELEASE_PROFILES.has(profile)) {
439
463
  throw new Error(`Invalid release profile "${profile}". Expected one of: ${Array.from(RELEASE_PROFILES).join(', ')}`);
440
464
  }
441
465
 
442
466
  const steps = [];
467
+ const problemClosureGateScript = path.join(projectPath, 'scripts', 'problem-closure-gate.js');
468
+ const hasProblemClosureGateScript = await fileSystem.pathExists(problemClosureGateScript);
469
+ const canRunProblemClosureGate = hasProblemClosureGateScript && Boolean(specId);
470
+ const problemClosureArgs = ['scripts/problem-closure-gate.js', '--stage', 'release', '--spec', specId, '--fail-on-block', '--json'];
471
+ if (verifyReportPath) {
472
+ problemClosureArgs.push('--verify-report', verifyReportPath);
473
+ }
474
+ steps.push({
475
+ id: 'problem-closure-gate',
476
+ name: 'problem closure gate (release)',
477
+ command: 'node',
478
+ args: problemClosureArgs,
479
+ required: Boolean(specId),
480
+ enabled: canRunProblemClosureGate,
481
+ skip_reason: canRunProblemClosureGate
482
+ ? ''
483
+ : (specId ? 'scripts/problem-closure-gate.js not found' : 'spec id unavailable for problem-closure gate')
484
+ });
485
+
443
486
  steps.push({
444
487
  id: 'npm-pack-dry-run',
445
488
  name: 'npm pack --dry-run',
@@ -710,6 +753,77 @@ async function readSpecDomainChain(projectPath, specId, fileSystem = fs) {
710
753
  }
711
754
  }
712
755
 
756
+ async function readSpecProblemContract(projectPath, specId, fileSystem = fs) {
757
+ const specRoot = path.join(projectPath, '.sce', 'specs', specId);
758
+ const contractPath = path.join(specRoot, DEFAULT_PROBLEM_CONTRACT_RELATIVE_PATH);
759
+ if (!await fileSystem.pathExists(contractPath)) {
760
+ return null;
761
+ }
762
+ try {
763
+ const payload = await fileSystem.readJson(contractPath);
764
+ const stat = await fileSystem.stat(contractPath);
765
+ return {
766
+ spec_id: specId,
767
+ contract_path: toRelativePosix(projectPath, contractPath),
768
+ payload,
769
+ updated_at: stat && stat.mtime ? stat.mtime.toISOString() : null,
770
+ mtime_ms: Number(stat && stat.mtimeMs) || 0
771
+ };
772
+ } catch (_error) {
773
+ return null;
774
+ }
775
+ }
776
+
777
+ async function readGovernanceSignals(projectPath, fileSystem = fs) {
778
+ const reportPath = path.join(projectPath, DEFAULT_INTERACTIVE_GOVERNANCE_REPORT);
779
+ if (!await fileSystem.pathExists(reportPath)) {
780
+ return {
781
+ available: false,
782
+ report_path: null,
783
+ high_breach_count: 0,
784
+ medium_breach_count: 0
785
+ };
786
+ }
787
+ const payload = await fileSystem.readJson(reportPath).catch(() => null);
788
+ const summary = extractGovernanceBreachSignals(payload || {});
789
+ return {
790
+ ...summary,
791
+ report_path: toRelativePosix(projectPath, reportPath)
792
+ };
793
+ }
794
+
795
+ async function readVerifyReportSignals(projectPath, verifyReportPath = '', fileSystem = fs) {
796
+ const normalized = normalizeString(verifyReportPath);
797
+ if (!normalized) {
798
+ return {
799
+ available: false,
800
+ report_path: null,
801
+ passed: false,
802
+ failed_step_count: 0
803
+ };
804
+ }
805
+ const absolutePath = path.isAbsolute(normalized)
806
+ ? normalized
807
+ : path.join(projectPath, normalized);
808
+ if (!await fileSystem.pathExists(absolutePath)) {
809
+ return {
810
+ available: false,
811
+ report_path: normalized,
812
+ passed: false,
813
+ failed_step_count: 0
814
+ };
815
+ }
816
+ const payload = await fileSystem.readJson(absolutePath).catch(() => null);
817
+ const steps = Array.isArray(payload && payload.steps) ? payload.steps : [];
818
+ const failedStepCount = steps.filter((step) => normalizeString(step && step.status) === 'failed').length;
819
+ return {
820
+ available: true,
821
+ report_path: toRelativePosix(projectPath, absolutePath),
822
+ passed: payload && payload.passed === true && failedStepCount === 0,
823
+ failed_step_count: failedStepCount
824
+ };
825
+ }
826
+
713
827
  function normalizeChainList(value, limit = 5) {
714
828
  if (!Array.isArray(value)) {
715
829
  return [];
@@ -725,13 +839,97 @@ function normalizeChainList(value, limit = 5) {
725
839
  });
726
840
  }
727
841
 
842
+ function normalizeProblemContract(contract = {}, context = {}) {
843
+ const source = contract && typeof contract === 'object' ? contract : {};
844
+ const issueStatement = normalizeString(
845
+ source.issue_statement
846
+ || source.issue
847
+ || source.problem_statement
848
+ || context.problem_statement
849
+ || context.goal
850
+ );
851
+ const expectedOutcome = normalizeString(
852
+ source.expected_outcome
853
+ || source.expected
854
+ || source.success_criteria
855
+ || context.verification_plan
856
+ || (context.scene_id ? `Scene ${context.scene_id} reaches deterministic verification gates.` : '')
857
+ );
858
+ const reproductionSteps = normalizeChainList(
859
+ source.reproduction_steps || source.repro_steps || source.steps,
860
+ 20
861
+ );
862
+ const fallbackRepro = reproductionSteps.length > 0
863
+ ? reproductionSteps
864
+ : [
865
+ normalizeString(context.goal) || 'Reproduce the reported issue in the target scene.',
866
+ 'Capture logs and gate evidence for the failing path.'
867
+ ].filter(Boolean);
868
+ const forbiddenWorkarounds = normalizeChainList(
869
+ source.forbidden_workarounds || source.prohibited_workarounds || source.disallowed_workarounds,
870
+ 20
871
+ );
872
+ const fallbackForbidden = forbiddenWorkarounds.length > 0
873
+ ? forbiddenWorkarounds
874
+ : [
875
+ 'Do not bypass gates or tests.',
876
+ 'Do not silence runtime errors.'
877
+ ];
878
+
879
+ return {
880
+ issue_statement: issueStatement,
881
+ expected_outcome: expectedOutcome,
882
+ reproduction_steps: fallbackRepro,
883
+ impact_scope: normalizeString(source.impact_scope || source.scope || context.scene_id),
884
+ forbidden_workarounds: fallbackForbidden
885
+ };
886
+ }
887
+
888
+ function extractGovernanceBreachSignals(report = {}) {
889
+ if (!report || typeof report !== 'object') {
890
+ return {
891
+ available: false,
892
+ high_breach_count: 0,
893
+ medium_breach_count: 0
894
+ };
895
+ }
896
+ const alerts = Array.isArray(report.alerts) ? report.alerts : [];
897
+ let highBreachCount = 0;
898
+ let mediumBreachCount = 0;
899
+ for (const alert of alerts) {
900
+ const status = normalizeString(alert && alert.status).toLowerCase();
901
+ const severity = normalizeString(alert && alert.severity).toLowerCase();
902
+ if (status !== 'breach') {
903
+ continue;
904
+ }
905
+ if (severity === 'high') {
906
+ highBreachCount += 1;
907
+ } else if (severity === 'medium') {
908
+ mediumBreachCount += 1;
909
+ }
910
+ }
911
+ return {
912
+ available: true,
913
+ high_breach_count: highBreachCount,
914
+ medium_breach_count: mediumBreachCount
915
+ };
916
+ }
917
+
728
918
  function summarizeDomainChain(payload = {}) {
729
919
  const ontology = payload && typeof payload.ontology === 'object' ? payload.ontology : {};
920
+ const ontologyEvidence = payload && typeof payload.ontology_evidence === 'object' ? payload.ontology_evidence : {};
730
921
  const decisionPath = Array.isArray(payload.decision_execution_path) ? payload.decision_execution_path : [];
731
922
  const correctionLoop = payload && typeof payload.correction_loop === 'object' ? payload.correction_loop : {};
732
923
  const verification = payload && typeof payload.verification === 'object' ? payload.verification : {};
733
924
  const hypotheses = Array.isArray(payload.hypotheses) ? payload.hypotheses : [];
734
925
  const risks = Array.isArray(payload.risks) ? payload.risks : [];
926
+ const evidenceBindingCount = (
927
+ normalizeChainList(ontologyEvidence.entity, 50).length
928
+ + normalizeChainList(ontologyEvidence.relation, 50).length
929
+ + normalizeChainList(ontologyEvidence.business_rule, 50).length
930
+ + normalizeChainList(ontologyEvidence.decision_policy, 50).length
931
+ + normalizeChainList(ontologyEvidence.execution_flow, 50).length
932
+ );
735
933
 
736
934
  return {
737
935
  scene_id: normalizeString(payload.scene_id) || null,
@@ -747,6 +945,8 @@ function summarizeDomainChain(payload = {}) {
747
945
  hypothesis_count: hypotheses.length,
748
946
  risk_count: risks.length,
749
947
  decision_path_steps: decisionPath.length,
948
+ evidence_binding_count: evidenceBindingCount,
949
+ verification_plan: normalizeString(verification.plan) || null,
750
950
  correction_loop: {
751
951
  triggers: normalizeChainList(correctionLoop.triggers, 5),
752
952
  actions: normalizeChainList(correctionLoop.actions, 5)
@@ -781,7 +981,13 @@ function buildDomainChainRuntimeContext(payload = {}) {
781
981
  verification: {
782
982
  plan: normalizeString(payload?.verification?.plan) || null,
783
983
  gates: normalizeChainList(payload?.verification?.gates, 10)
784
- }
984
+ },
985
+ problem_contract: normalizeProblemContract(payload?.problem_contract || {}, {
986
+ scene_id: normalizeString(payload.scene_id) || '',
987
+ goal: normalizeString(payload?.problem?.statement) || '',
988
+ problem_statement: normalizeString(payload?.problem?.statement) || '',
989
+ verification_plan: normalizeString(payload?.verification?.plan) || ''
990
+ })
785
991
  };
786
992
  }
787
993
 
@@ -834,12 +1040,22 @@ async function resolveDomainChainBinding(options = {}, dependencies = {}) {
834
1040
  problemStatement: goal || `Studio scene cycle for ${sceneId}`
835
1041
  });
836
1042
  const chain = await readSpecDomainChain(projectPath, explicitSpec, fileSystem);
1043
+ const problemContract = await readSpecProblemContract(projectPath, explicitSpec, fileSystem);
837
1044
  if (!chain) {
838
1045
  return {
839
1046
  resolved: false,
840
1047
  source: 'explicit-spec',
841
1048
  spec_id: explicitSpec,
842
- reason: 'domain_chain_missing'
1049
+ reason: 'domain_chain_missing',
1050
+ problem_contract: problemContract
1051
+ ? normalizeProblemContract(problemContract.payload, {
1052
+ scene_id: sceneId,
1053
+ goal
1054
+ })
1055
+ : normalizeProblemContract({}, {
1056
+ scene_id: sceneId,
1057
+ goal
1058
+ })
843
1059
  };
844
1060
  }
845
1061
  return {
@@ -849,7 +1065,17 @@ async function resolveDomainChainBinding(options = {}, dependencies = {}) {
849
1065
  chain_path: chain.chain_path,
850
1066
  updated_at: chain.updated_at,
851
1067
  summary: summarizeDomainChain(chain.payload),
852
- context: buildDomainChainRuntimeContext(chain.payload)
1068
+ context: buildDomainChainRuntimeContext(chain.payload),
1069
+ problem_contract: normalizeProblemContract(
1070
+ problemContract && problemContract.payload ? problemContract.payload : chain.payload?.problem_contract || {},
1071
+ {
1072
+ scene_id: sceneId,
1073
+ goal,
1074
+ problem_statement: normalizeString(chain?.payload?.problem?.statement),
1075
+ verification_plan: normalizeString(chain?.payload?.verification?.plan)
1076
+ }
1077
+ ),
1078
+ problem_contract_path: problemContract ? problemContract.contract_path : null
853
1079
  };
854
1080
  }
855
1081
 
@@ -873,6 +1099,7 @@ async function resolveDomainChainBinding(options = {}, dependencies = {}) {
873
1099
  }
874
1100
 
875
1101
  const selected = candidates[0];
1102
+ const selectedContract = await readSpecProblemContract(projectPath, selected.spec_id, fileSystem);
876
1103
  return {
877
1104
  resolved: true,
878
1105
  source: candidates.length === 1 ? 'scene-auto-single' : 'scene-auto-latest',
@@ -886,7 +1113,17 @@ async function resolveDomainChainBinding(options = {}, dependencies = {}) {
886
1113
  updated_at: item.updated_at
887
1114
  })),
888
1115
  summary: summarizeDomainChain(selected.payload),
889
- context: buildDomainChainRuntimeContext(selected.payload)
1116
+ context: buildDomainChainRuntimeContext(selected.payload),
1117
+ problem_contract: normalizeProblemContract(
1118
+ selectedContract && selectedContract.payload ? selectedContract.payload : selected.payload?.problem_contract || {},
1119
+ {
1120
+ scene_id: sceneId,
1121
+ goal,
1122
+ problem_statement: normalizeString(selected?.payload?.problem?.statement),
1123
+ verification_plan: normalizeString(selected?.payload?.verification?.plan)
1124
+ }
1125
+ ),
1126
+ problem_contract_path: selectedContract ? selectedContract.contract_path : null
890
1127
  };
891
1128
  }
892
1129
 
@@ -949,6 +1186,14 @@ function buildJobDomainChainMetadata(job = {}) {
949
1186
  : null;
950
1187
  const summary = domainChain && domainChain.summary ? domainChain.summary : null;
951
1188
  const context = domainChain && domainChain.context ? domainChain.context : null;
1189
+ const problemContract = job?.source?.problem_contract && typeof job.source.problem_contract === 'object'
1190
+ ? job.source.problem_contract
1191
+ : normalizeProblemContract(context && context.problem_contract ? context.problem_contract : {}, {
1192
+ scene_id: normalizeString(job?.scene?.id),
1193
+ goal: normalizeString(job?.source?.goal),
1194
+ problem_statement: normalizeString(summary && summary.problem_statement),
1195
+ verification_plan: normalizeString(summary && summary.verification_plan)
1196
+ });
952
1197
  return {
953
1198
  resolved: domainChain && domainChain.resolved === true,
954
1199
  source: domainChain && domainChain.source ? domainChain.source : 'none',
@@ -957,12 +1202,14 @@ function buildJobDomainChainMetadata(job = {}) {
957
1202
  reason: domainChain && domainChain.reason ? domainChain.reason : null,
958
1203
  decision_path_steps: summary ? Number(summary.decision_path_steps || 0) : 0,
959
1204
  risk_count: summary ? Number(summary.risk_count || 0) : 0,
1205
+ evidence_binding_count: summary ? Number(summary.evidence_binding_count || 0) : 0,
960
1206
  correction_triggers: summary && summary.correction_loop
961
1207
  ? normalizeChainList(summary.correction_loop.triggers, 10)
962
1208
  : [],
963
1209
  verification_gates: summary
964
1210
  ? normalizeChainList(summary.verification_gates, 10)
965
1211
  : [],
1212
+ problem_contract: problemContract,
966
1213
  summary: summary || null,
967
1214
  context: context || null
968
1215
  };
@@ -975,6 +1222,18 @@ function summarizeProblemEvaluation(evaluation = {}) {
975
1222
  confidence_score: Number(evaluation.confidence_score || 0),
976
1223
  risk_level: normalizeString(evaluation?.dimensions?.risk?.level) || 'low',
977
1224
  strategy: normalizeString(evaluation?.dimensions?.strategy?.strategy) || 'direct-execution',
1225
+ contract_score: Number(evaluation?.dimensions?.problem_contract?.score || 0),
1226
+ ontology_score: Number(evaluation?.dimensions?.ontology_alignment?.score || 0),
1227
+ convergence_score: Number(evaluation?.dimensions?.convergence?.score || 0),
1228
+ contract_missing: Array.isArray(evaluation?.dimensions?.problem_contract?.missing)
1229
+ ? evaluation.dimensions.problem_contract.missing
1230
+ : [],
1231
+ ontology_missing_axes: Array.isArray(evaluation?.dimensions?.ontology_alignment?.missing_axes)
1232
+ ? evaluation.dimensions.ontology_alignment.missing_axes
1233
+ : [],
1234
+ convergence_missing: Array.isArray(evaluation?.dimensions?.convergence?.missing)
1235
+ ? evaluation.dimensions.convergence.missing
1236
+ : [],
978
1237
  blockers: Array.isArray(evaluation.blockers) ? evaluation.blockers : [],
979
1238
  warnings: Array.isArray(evaluation.warnings) ? evaluation.warnings : [],
980
1239
  recommendations: Array.isArray(evaluation.recommendations) ? evaluation.recommendations : [],
@@ -1044,6 +1303,7 @@ async function enforceProblemEvaluationForStage(job = {}, stage = '', context =
1044
1303
  goal: normalizeString(context.goal || job?.source?.goal),
1045
1304
  release_channel: normalizeString(context.release_channel || ''),
1046
1305
  domain_chain: context.domain_chain || (job?.source?.domain_chain || {}),
1306
+ problem_contract: context.problem_contract || job?.source?.problem_contract || {},
1047
1307
  related_specs_count: Number(context.related_specs_count || job?.source?.related_specs?.total_candidates || 0),
1048
1308
  stage_readiness: context.stage_readiness || buildStageReadiness(job, stage),
1049
1309
  gate_signals: context.gate_signals || {}
@@ -1087,6 +1347,9 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1087
1347
  const fromChat = normalizeString(options.fromChat);
1088
1348
  const sceneId = normalizeString(options.scene);
1089
1349
  const specId = normalizeString(options.spec);
1350
+ const goal = normalizeString(options.goal);
1351
+ const manualSpecMode = options.manualSpec === true;
1352
+ const skipSpecGovernance = options.specGovernance === false;
1090
1353
 
1091
1354
  if (!fromChat) {
1092
1355
  throw new Error('--from-chat is required');
@@ -1094,16 +1357,18 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1094
1357
  if (!sceneId) {
1095
1358
  throw new Error('--scene is required');
1096
1359
  }
1097
- const domainChainBinding = await resolveDomainChainBinding({
1360
+
1361
+ let domainChainBinding = await resolveDomainChainBinding({
1098
1362
  sceneId,
1099
1363
  specId,
1100
- goal: normalizeString(options.goal)
1364
+ goal
1101
1365
  }, {
1102
1366
  projectPath,
1103
1367
  fileSystem
1104
1368
  });
1105
- const relatedSpecLookup = await findRelatedSpecs({
1106
- query: normalizeString(options.goal),
1369
+
1370
+ let relatedSpecLookup = await findRelatedSpecs({
1371
+ query: goal,
1107
1372
  sceneId,
1108
1373
  limit: 8,
1109
1374
  excludeSpecId: domainChainBinding.spec_id || specId || null
@@ -1111,6 +1376,45 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1111
1376
  projectPath,
1112
1377
  fileSystem
1113
1378
  });
1379
+
1380
+ const intake = await runStudioAutoIntake({
1381
+ scene_id: sceneId,
1382
+ from_chat: fromChat,
1383
+ goal,
1384
+ explicit_spec_id: specId,
1385
+ domain_chain_binding: domainChainBinding,
1386
+ related_specs: relatedSpecLookup,
1387
+ apply: !manualSpecMode,
1388
+ skip: manualSpecMode
1389
+ }, {
1390
+ projectPath,
1391
+ fileSystem
1392
+ });
1393
+
1394
+ const intakeSpecId = normalizeString(intake && intake.selected_spec_id);
1395
+ const effectiveSpecId = intakeSpecId || normalizeString(domainChainBinding.spec_id) || specId || null;
1396
+
1397
+ if (effectiveSpecId && effectiveSpecId !== normalizeString(domainChainBinding.spec_id)) {
1398
+ domainChainBinding = await resolveDomainChainBinding({
1399
+ sceneId,
1400
+ specId: effectiveSpecId,
1401
+ goal
1402
+ }, {
1403
+ projectPath,
1404
+ fileSystem
1405
+ });
1406
+ }
1407
+
1408
+ relatedSpecLookup = await findRelatedSpecs({
1409
+ query: goal,
1410
+ sceneId,
1411
+ limit: 8,
1412
+ excludeSpecId: effectiveSpecId || null
1413
+ }, {
1414
+ projectPath,
1415
+ fileSystem
1416
+ });
1417
+
1114
1418
  const relatedSpecItems = Array.isArray(relatedSpecLookup.related_specs)
1115
1419
  ? relatedSpecLookup.related_specs.map((item) => ({
1116
1420
  spec_id: item.spec_id,
@@ -1127,15 +1431,26 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1127
1431
 
1128
1432
  const jobId = normalizeString(options.job) || createJobId();
1129
1433
  const now = nowIso();
1434
+ const problemContract = normalizeProblemContract(
1435
+ domainChainBinding.problem_contract || {},
1436
+ {
1437
+ scene_id: sceneId,
1438
+ goal,
1439
+ problem_statement: normalizeString(domainChainBinding?.summary?.problem_statement),
1440
+ verification_plan: normalizeString(domainChainBinding?.summary?.verification_plan)
1441
+ }
1442
+ );
1130
1443
  const planShadowJob = {
1131
1444
  job_id: jobId,
1132
1445
  scene: {
1133
1446
  id: sceneId,
1134
- spec_id: domainChainBinding.spec_id || specId || null
1447
+ spec_id: effectiveSpecId
1135
1448
  },
1136
1449
  source: {
1137
- goal: normalizeString(options.goal) || null,
1138
- spec_id: domainChainBinding.spec_id || specId || null,
1450
+ goal: goal || null,
1451
+ spec_id: effectiveSpecId,
1452
+ problem_contract: problemContract,
1453
+ problem_contract_path: domainChainBinding.problem_contract_path || null,
1139
1454
  domain_chain: {
1140
1455
  resolved: domainChainBinding.resolved === true,
1141
1456
  summary: domainChainBinding.summary || null
@@ -1148,8 +1463,9 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1148
1463
  };
1149
1464
  const planProblemEvaluation = await enforceProblemEvaluationForStage(planShadowJob, 'plan', {
1150
1465
  scene_id: sceneId,
1151
- spec_id: domainChainBinding.spec_id || specId || null,
1152
- goal: normalizeString(options.goal) || null,
1466
+ spec_id: effectiveSpecId,
1467
+ goal: goal || null,
1468
+ problem_contract: problemContract,
1153
1469
  domain_chain: {
1154
1470
  resolved: domainChainBinding.resolved === true,
1155
1471
  summary: domainChainBinding.summary || null
@@ -1176,16 +1492,35 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1176
1492
  const sessionStore = dependencies.sessionStore || new SessionStore(projectPath);
1177
1493
  const sceneSessionBinding = await sessionStore.beginSceneSession({
1178
1494
  sceneId,
1179
- objective: normalizeString(options.goal) || `Studio scene cycle for ${sceneId}`,
1495
+ objective: goal || `Studio scene cycle for ${sceneId}`,
1180
1496
  tool: normalizeString(options.tool) || 'generic'
1181
1497
  });
1498
+
1499
+ let governanceSnapshot = null;
1500
+ let governanceWarning = '';
1501
+ const autoRunGovernance = !(skipSpecGovernance)
1502
+ && (!intake || !intake.policy || !intake.policy.governance || intake.policy.governance.auto_run_on_plan !== false);
1503
+ if (autoRunGovernance) {
1504
+ try {
1505
+ governanceSnapshot = await runStudioSpecGovernance({
1506
+ apply: true,
1507
+ scene: sceneId
1508
+ }, {
1509
+ projectPath,
1510
+ fileSystem
1511
+ });
1512
+ } catch (error) {
1513
+ governanceWarning = normalizeString(error && error.message);
1514
+ }
1515
+ }
1516
+
1182
1517
  stages.plan = {
1183
1518
  status: 'completed',
1184
1519
  completed_at: now,
1185
1520
  metadata: {
1186
1521
  from_chat: fromChat,
1187
1522
  scene_id: sceneId,
1188
- spec_id: domainChainBinding.spec_id || specId || null,
1523
+ spec_id: effectiveSpecId,
1189
1524
  scene_session_id: sceneSessionBinding.session.session_id,
1190
1525
  scene_cycle: sceneSessionBinding.scene_cycle,
1191
1526
  domain_chain_resolved: domainChainBinding.resolved === true,
@@ -1194,7 +1529,19 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1194
1529
  domain_chain_path: domainChainBinding.chain_path || null,
1195
1530
  domain_chain_summary: domainChainBinding.summary || null,
1196
1531
  domain_chain_reason: domainChainBinding.reason || null,
1532
+ problem_contract: problemContract,
1533
+ intake: intake ? {
1534
+ enabled: intake.enabled === true,
1535
+ intent_type: intake.intent ? intake.intent.intent_type : null,
1536
+ decision_action: intake.decision ? intake.decision.action : null,
1537
+ decision_reason: intake.decision ? intake.decision.reason : null,
1538
+ selected_spec_id: intake.selected_spec_id || effectiveSpecId || null,
1539
+ created_spec_id: intake.created_spec && intake.created_spec.created ? intake.created_spec.spec_id : null,
1540
+ policy_path: intake.policy_path || null
1541
+ } : null,
1197
1542
  problem_evaluation: summarizeProblemEvaluation(planProblemEvaluation),
1543
+ spec_governance: governanceSnapshot ? governanceSnapshot.summary : null,
1544
+ spec_governance_warning: governanceWarning || null,
1198
1545
  related_specs_total: Number(relatedSpecLookup.total_candidates || 0),
1199
1546
  related_specs_top: relatedSpecItems
1200
1547
  }
@@ -1208,13 +1555,24 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1208
1555
  status: 'planned',
1209
1556
  source: {
1210
1557
  from_chat: fromChat,
1211
- goal: normalizeString(options.goal) || null,
1212
- spec_id: domainChainBinding.spec_id || specId || null,
1558
+ goal: goal || null,
1559
+ spec_id: effectiveSpecId,
1560
+ problem_contract: problemContract,
1561
+ problem_contract_path: domainChainBinding.problem_contract_path || null,
1562
+ intake: intake ? {
1563
+ enabled: intake.enabled === true,
1564
+ policy_path: intake.policy_path || null,
1565
+ policy_loaded_from: intake.policy_loaded_from || null,
1566
+ intent: intake.intent || null,
1567
+ decision: intake.decision || null,
1568
+ selected_spec_id: intake.selected_spec_id || effectiveSpecId || null,
1569
+ created_spec: intake.created_spec || null
1570
+ } : null,
1213
1571
  domain_chain: {
1214
1572
  resolved: domainChainBinding.resolved === true,
1215
1573
  source: domainChainBinding.source || 'none',
1216
1574
  reason: domainChainBinding.reason || null,
1217
- spec_id: domainChainBinding.spec_id || null,
1575
+ spec_id: effectiveSpecId || domainChainBinding.spec_id || null,
1218
1576
  chain_path: domainChainBinding.chain_path || null,
1219
1577
  candidate_count: Number.isFinite(Number(domainChainBinding.candidate_count))
1220
1578
  ? Number(domainChainBinding.candidate_count)
@@ -1229,11 +1587,20 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1229
1587
  scene_id: relatedSpecLookup.scene_id || null,
1230
1588
  total_candidates: Number(relatedSpecLookup.total_candidates || 0),
1231
1589
  items: relatedSpecItems
1232
- }
1590
+ },
1591
+ spec_governance: governanceSnapshot
1592
+ ? {
1593
+ status: governanceSnapshot.summary ? governanceSnapshot.summary.status : null,
1594
+ alert_count: governanceSnapshot.summary ? Number(governanceSnapshot.summary.alert_count || 0) : 0,
1595
+ report_file: governanceSnapshot.report_file || null,
1596
+ scene_index_file: governanceSnapshot.scene_index_file || null
1597
+ }
1598
+ : null,
1599
+ spec_governance_warning: governanceWarning || null
1233
1600
  },
1234
1601
  scene: {
1235
1602
  id: sceneId,
1236
- spec_id: domainChainBinding.spec_id || specId || null,
1603
+ spec_id: effectiveSpecId,
1237
1604
  related_spec_ids: relatedSpecItems.map((item) => item.spec_id)
1238
1605
  },
1239
1606
  session: {
@@ -1249,6 +1616,12 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1249
1616
  patch_bundle_id: null,
1250
1617
  verify_report: null,
1251
1618
  release_ref: null,
1619
+ spec_portfolio_report: governanceSnapshot && governanceSnapshot.report_file
1620
+ ? governanceSnapshot.report_file
1621
+ : null,
1622
+ spec_scene_index: governanceSnapshot && governanceSnapshot.scene_index_file
1623
+ ? governanceSnapshot.scene_index_file
1624
+ : null,
1252
1625
  problem_eval_reports: {
1253
1626
  plan: normalizeString(planProblemEvaluation.report_file) || null
1254
1627
  }
@@ -1259,7 +1632,7 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1259
1632
  await appendStudioEvent(paths, job, 'stage.plan.completed', {
1260
1633
  from_chat: fromChat,
1261
1634
  scene_id: sceneId,
1262
- spec_id: domainChainBinding.spec_id || specId || null,
1635
+ spec_id: effectiveSpecId,
1263
1636
  scene_session_id: sceneSessionBinding.session.session_id,
1264
1637
  scene_cycle: sceneSessionBinding.scene_cycle,
1265
1638
  target: job.target,
@@ -1267,13 +1640,28 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
1267
1640
  domain_chain_source: domainChainBinding.source || 'none',
1268
1641
  domain_chain_spec_id: domainChainBinding.spec_id || null,
1269
1642
  domain_chain_path: domainChainBinding.chain_path || null,
1643
+ problem_contract: problemContract,
1644
+ intake_action: intake && intake.decision ? intake.decision.action : null,
1645
+ intake_reason: intake && intake.decision ? intake.decision.reason : null,
1646
+ intake_selected_spec_id: intake ? intake.selected_spec_id || effectiveSpecId || null : effectiveSpecId,
1647
+ intake_created_spec_id: intake && intake.created_spec && intake.created_spec.created
1648
+ ? intake.created_spec.spec_id
1649
+ : null,
1270
1650
  problem_evaluation: summarizeProblemEvaluation(planProblemEvaluation),
1651
+ spec_governance: governanceSnapshot ? governanceSnapshot.summary : null,
1652
+ spec_governance_warning: governanceWarning || null,
1271
1653
  related_specs_total: Number(relatedSpecLookup.total_candidates || 0),
1272
1654
  related_spec_ids: relatedSpecItems.map((item) => item.spec_id)
1273
1655
  }, fileSystem);
1274
1656
  await writeLatestJob(paths, jobId, fileSystem);
1275
1657
 
1276
1658
  const payload = buildCommandPayload('studio-plan', job);
1659
+ payload.scene = {
1660
+ id: sceneId,
1661
+ spec_id: effectiveSpecId
1662
+ };
1663
+ payload.intake = job.source && job.source.intake ? job.source.intake : null;
1664
+ payload.spec_governance = governanceSnapshot ? governanceSnapshot.summary : null;
1277
1665
  printStudioPayload(payload, options);
1278
1666
  return payload;
1279
1667
  }
@@ -1457,16 +1845,19 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
1457
1845
  const autoErrorbookRecords = [];
1458
1846
  const gateSteps = await buildVerifyGateSteps({ profile }, {
1459
1847
  projectPath,
1460
- fileSystem
1848
+ fileSystem,
1849
+ specId: normalizeString(domainChainMetadata.spec_id) || null
1461
1850
  });
1462
1851
  const verifyProblemEvaluation = await enforceProblemEvaluationForStage(job, 'verify', {
1463
1852
  scene_id: normalizeString(job?.scene?.id),
1464
1853
  spec_id: normalizeString(domainChainMetadata.spec_id) || normalizeString(job?.source?.spec_id),
1465
1854
  goal: normalizeString(job?.source?.goal),
1466
1855
  domain_chain: job?.source?.domain_chain || {},
1856
+ problem_contract: job?.source?.problem_contract || {},
1467
1857
  related_specs_count: Number(job?.source?.related_specs?.total_candidates || 0),
1468
1858
  stage_readiness: buildStageReadiness(job, 'verify', {
1469
- gate_required_ready: deriveGateSignals(gateSteps).required_missing === 0
1859
+ gate_required_ready: deriveGateSignals(gateSteps).required_missing === 0,
1860
+ convergence_strict: profile === 'strict'
1470
1861
  }),
1471
1862
  gate_signals: deriveGateSignals(gateSteps)
1472
1863
  }, {
@@ -1609,9 +2000,17 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
1609
2000
  const releaseStartedAt = nowIso();
1610
2001
  const domainChainMetadata = buildJobDomainChainMetadata(job);
1611
2002
  const autoErrorbookRecords = [];
1612
- const gateSteps = await buildReleaseGateSteps({ profile }, {
2003
+ const verifyReportSignals = await readVerifyReportSignals(
1613
2004
  projectPath,
2005
+ normalizeString(job?.artifacts?.verify_report),
1614
2006
  fileSystem
2007
+ );
2008
+ const governanceSignals = await readGovernanceSignals(projectPath, fileSystem);
2009
+ const gateSteps = await buildReleaseGateSteps({ profile }, {
2010
+ projectPath,
2011
+ fileSystem,
2012
+ specId: normalizeString(domainChainMetadata.spec_id) || null,
2013
+ verifyReportPath: normalizeString(job?.artifacts?.verify_report) || null
1615
2014
  });
1616
2015
  const releaseGateSignals = deriveGateSignals(gateSteps);
1617
2016
  const releaseProblemEvaluation = await enforceProblemEvaluationForStage(job, 'release', {
@@ -1620,9 +2019,17 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
1620
2019
  goal: normalizeString(job?.source?.goal),
1621
2020
  release_channel: channel,
1622
2021
  domain_chain: job?.source?.domain_chain || {},
2022
+ problem_contract: job?.source?.problem_contract || {},
1623
2023
  related_specs_count: Number(job?.source?.related_specs?.total_candidates || 0),
1624
2024
  stage_readiness: buildStageReadiness(job, 'release', {
1625
- gate_required_ready: releaseGateSignals.required_missing === 0
2025
+ gate_required_ready: releaseGateSignals.required_missing === 0,
2026
+ convergence_strict: profile === 'strict',
2027
+ verify_stage_passed: isStageCompleted(job, 'verify'),
2028
+ verify_report_ready: verifyReportSignals.available,
2029
+ verify_report_passed: verifyReportSignals.passed,
2030
+ regression_passed: verifyReportSignals.passed && verifyReportSignals.failed_step_count === 0,
2031
+ governance_report_ready: governanceSignals.available,
2032
+ high_alert_count: Number(governanceSignals.high_breach_count || 0)
1626
2033
  }),
1627
2034
  gate_signals: releaseGateSignals
1628
2035
  }, {
@@ -1672,6 +2079,8 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
1672
2079
  passed: gateResult.passed,
1673
2080
  steps: gateResult.steps,
1674
2081
  domain_chain: domainChainMetadata,
2082
+ verify_signals: verifyReportSignals,
2083
+ governance_signals: governanceSignals,
1675
2084
  problem_evaluation: summarizeProblemEvaluation(releaseProblemEvaluation),
1676
2085
  auto_errorbook_records: autoErrorbookRecords
1677
2086
  };
@@ -1885,6 +2294,116 @@ async function runStudioEventsCommand(options = {}, dependencies = {}) {
1885
2294
  return payload;
1886
2295
  }
1887
2296
 
2297
+ function printStudioIntakePayload(payload, options = {}) {
2298
+ if (options.json) {
2299
+ console.log(JSON.stringify(payload, null, 2));
2300
+ return;
2301
+ }
2302
+
2303
+ console.log(chalk.blue('Studio intake'));
2304
+ console.log(` Scene: ${payload.scene_id || 'n/a'}`);
2305
+ console.log(` Goal: ${payload.goal || '(empty)'}`);
2306
+ console.log(` Intent: ${payload.intent && payload.intent.intent_type ? payload.intent.intent_type : 'unknown'}`);
2307
+ console.log(` Decision: ${payload.decision && payload.decision.action ? payload.decision.action : 'none'}`);
2308
+ console.log(` Spec: ${payload.selected_spec_id || 'n/a'}`);
2309
+ }
2310
+
2311
+ async function runStudioIntakeCommand(options = {}, dependencies = {}) {
2312
+ const projectPath = dependencies.projectPath || process.cwd();
2313
+ const fileSystem = dependencies.fileSystem || fs;
2314
+ const sceneId = normalizeString(options.scene);
2315
+ const fromChat = normalizeString(options.fromChat);
2316
+ const goal = normalizeString(options.goal);
2317
+ const specId = normalizeString(options.spec);
2318
+
2319
+ if (!sceneId) {
2320
+ throw new Error('--scene is required');
2321
+ }
2322
+ if (!fromChat) {
2323
+ throw new Error('--from-chat is required');
2324
+ }
2325
+
2326
+ const domainChainBinding = await resolveDomainChainBinding({
2327
+ sceneId,
2328
+ specId,
2329
+ goal
2330
+ }, {
2331
+ projectPath,
2332
+ fileSystem
2333
+ });
2334
+
2335
+ const relatedSpecLookup = await findRelatedSpecs({
2336
+ query: goal,
2337
+ sceneId,
2338
+ limit: 8,
2339
+ excludeSpecId: domainChainBinding.spec_id || specId || null
2340
+ }, {
2341
+ projectPath,
2342
+ fileSystem
2343
+ });
2344
+
2345
+ const intake = await runStudioAutoIntake({
2346
+ scene_id: sceneId,
2347
+ from_chat: fromChat,
2348
+ goal,
2349
+ explicit_spec_id: specId,
2350
+ domain_chain_binding: domainChainBinding,
2351
+ related_specs: relatedSpecLookup,
2352
+ apply: options.apply === true,
2353
+ skip: options.manualSpec === true
2354
+ }, {
2355
+ projectPath,
2356
+ fileSystem
2357
+ });
2358
+
2359
+ const payload = {
2360
+ ...intake,
2361
+ domain_chain_source: domainChainBinding.source || 'none',
2362
+ domain_chain_spec_id: domainChainBinding.spec_id || null,
2363
+ related_specs_total: Number(relatedSpecLookup.total_candidates || 0)
2364
+ };
2365
+ printStudioIntakePayload(payload, options);
2366
+ return payload;
2367
+ }
2368
+
2369
+ function printStudioPortfolioPayload(payload, options = {}) {
2370
+ if (options.json) {
2371
+ console.log(JSON.stringify(payload, null, 2));
2372
+ return;
2373
+ }
2374
+ const summary = payload.summary || {};
2375
+ console.log(chalk.blue('Studio portfolio governance'));
2376
+ console.log(` Status: ${summary.status || 'unknown'}`);
2377
+ console.log(` Scenes: ${summary.scene_count || 0}`);
2378
+ console.log(` Specs: ${summary.total_specs || 0}`);
2379
+ console.log(` Active: ${summary.active_specs || 0}`);
2380
+ console.log(` Completed: ${summary.completed_specs || 0}`);
2381
+ console.log(` Stale: ${summary.stale_specs || 0}`);
2382
+ console.log(` Duplicate pairs: ${summary.duplicate_pairs || 0}`);
2383
+ console.log(` Overflow scenes: ${summary.overflow_scenes || 0}`);
2384
+ }
2385
+
2386
+ async function runStudioPortfolioCommand(options = {}, dependencies = {}) {
2387
+ const projectPath = dependencies.projectPath || process.cwd();
2388
+ const fileSystem = dependencies.fileSystem || fs;
2389
+ const payload = await runStudioSpecGovernance({
2390
+ scene: normalizeString(options.scene),
2391
+ apply: options.apply !== false
2392
+ }, {
2393
+ projectPath,
2394
+ fileSystem
2395
+ });
2396
+
2397
+ if (options.strict && payload.summary && Number(payload.summary.alert_count || 0) > 0) {
2398
+ throw new Error(
2399
+ `studio portfolio governance has alerts: ${payload.summary.alert_count} (duplicate/stale/overflow)`
2400
+ );
2401
+ }
2402
+
2403
+ printStudioPortfolioPayload(payload, options);
2404
+ return payload;
2405
+ }
2406
+
1888
2407
  async function runStudioCommand(handler, options, stageName = '') {
1889
2408
  try {
1890
2409
  const stage = normalizeString(stageName) || 'unknown';
@@ -1928,11 +2447,34 @@ function registerStudioCommands(program) {
1928
2447
  .requiredOption('--from-chat <session>', 'Chat session identifier or transcript reference')
1929
2448
  .option('--spec <spec-id>', 'Optional spec binding for domain-chain context ingestion')
1930
2449
  .option('--goal <goal>', 'Optional goal summary')
2450
+ .option('--manual-spec', 'Disable auto intake and only use explicit --spec or existing scene binding')
1931
2451
  .option('--target <target>', 'Target integration profile', 'default')
2452
+ .option('--no-spec-governance', 'Skip auto portfolio governance snapshot on plan stage')
1932
2453
  .option('--job <job-id>', 'Reuse an explicit studio job id')
1933
2454
  .option('--json', 'Print machine-readable JSON output')
1934
2455
  .action(async (options) => runStudioCommand(runStudioPlanCommand, options, 'plan'));
1935
2456
 
2457
+ studio
2458
+ .command('intake')
2459
+ .description('Analyze chat goal and auto-resolve spec binding/create decision')
2460
+ .requiredOption('--scene <scene-id>', 'Scene identifier')
2461
+ .requiredOption('--from-chat <session>', 'Chat session identifier or transcript reference')
2462
+ .option('--spec <spec-id>', 'Optional explicit spec id')
2463
+ .option('--goal <goal>', 'Goal text used for intent classification')
2464
+ .option('--apply', 'Create spec when decision is create_spec')
2465
+ .option('--manual-spec', 'Disable auto intake and keep explicit/manual binding only')
2466
+ .option('--json', 'Print machine-readable JSON output')
2467
+ .action(async (options) => runStudioCommand(runStudioIntakeCommand, options, 'intake'));
2468
+
2469
+ studio
2470
+ .command('portfolio')
2471
+ .description('Build scene-organized spec governance portfolio')
2472
+ .option('--scene <scene-id>', 'Optional scene filter')
2473
+ .option('--no-apply', 'Do not write portfolio/index artifacts to .sce/spec-governance/')
2474
+ .option('--strict', 'Fail when governance alerts are detected')
2475
+ .option('--json', 'Print machine-readable JSON output')
2476
+ .action(async (options) => runStudioCommand(runStudioPortfolioCommand, options, 'portfolio'));
2477
+
1936
2478
  studio
1937
2479
  .command('generate')
1938
2480
  .description('Generate patch bundle metadata for a planned studio job (scene inherited from plan)')
@@ -2017,12 +2559,14 @@ module.exports = {
2017
2559
  resolveNextAction,
2018
2560
  buildProgress,
2019
2561
  runStudioPlanCommand,
2562
+ runStudioIntakeCommand,
2020
2563
  runStudioGenerateCommand,
2021
2564
  runStudioApplyCommand,
2022
2565
  runStudioVerifyCommand,
2023
2566
  runStudioReleaseCommand,
2024
2567
  runStudioRollbackCommand,
2025
2568
  runStudioEventsCommand,
2569
+ runStudioPortfolioCommand,
2026
2570
  runStudioResumeCommand,
2027
2571
  registerStudioCommands
2028
2572
  };