scene-capability-engine 3.5.2 → 3.6.2

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,8 @@ 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 { TaskRefRegistry } = require('../task/task-ref-registry');
15
+ const { getSceStateStore } = require('../state/sce-state-store');
14
16
  const {
15
17
  loadStudioIntakePolicy,
16
18
  runStudioAutoIntake,
@@ -40,8 +42,7 @@ function resolveStudioPaths(projectPath = process.cwd()) {
40
42
  projectPath,
41
43
  studioDir,
42
44
  jobsDir: path.join(studioDir, 'jobs'),
43
- latestFile: path.join(studioDir, 'latest-job.json'),
44
- eventsDir: path.join(studioDir, 'events')
45
+ latestFile: path.join(studioDir, 'latest-job.json')
45
46
  };
46
47
  }
47
48
 
@@ -597,7 +598,6 @@ async function writeStudioReport(projectPath, relativePath, payload, fileSystem
597
598
 
598
599
  async function ensureStudioDirectories(paths, fileSystem = fs) {
599
600
  await fileSystem.ensureDir(paths.jobsDir);
600
- await fileSystem.ensureDir(paths.eventsDir);
601
601
  }
602
602
 
603
603
  async function writeLatestJob(paths, jobId, fileSystem = fs) {
@@ -622,10 +622,6 @@ function getJobFilePath(paths, jobId) {
622
622
  return path.join(paths.jobsDir, `${jobId}.json`);
623
623
  }
624
624
 
625
- function getEventLogFilePath(paths, jobId) {
626
- return path.join(paths.eventsDir, `${jobId}.jsonl`);
627
- }
628
-
629
625
  async function saveJob(paths, job, fileSystem = fs) {
630
626
  const jobFile = getJobFilePath(paths, job.job_id);
631
627
  await fileSystem.writeJson(jobFile, job, { spaces: 2 });
@@ -640,40 +636,28 @@ async function appendStudioEvent(paths, job, eventType, metadata = {}, fileSyste
640
636
  timestamp: nowIso(),
641
637
  metadata
642
638
  };
643
- const eventLine = `${JSON.stringify(event)}\n`;
644
- const eventFile = getEventLogFilePath(paths, job.job_id);
645
- await fileSystem.appendFile(eventFile, eventLine, 'utf8');
639
+ const sceneId = normalizeString(job?.scene?.id) || null;
640
+ const specId = normalizeString(job?.scene?.spec_id) || normalizeString(job?.source?.spec_id) || null;
641
+ const stateStore = getSceStateStore(paths.projectPath, { fileSystem });
642
+ const persisted = await stateStore.appendStudioEvent({
643
+ ...event,
644
+ scene_id: sceneId,
645
+ spec_id: specId
646
+ });
647
+ if (!persisted) {
648
+ throw new Error('Failed to persist studio event into sqlite state store');
649
+ }
646
650
  return event;
647
651
  }
648
652
 
649
653
  async function readStudioEvents(paths, jobId, options = {}, fileSystem = fs) {
650
654
  const { limit = 50 } = options;
651
- const eventFile = getEventLogFilePath(paths, jobId);
652
- const exists = await fileSystem.pathExists(eventFile);
653
- if (!exists) {
654
- return [];
655
+ const stateStore = getSceStateStore(paths.projectPath, { fileSystem });
656
+ const events = await stateStore.listStudioEvents(jobId, { limit });
657
+ if (events === null) {
658
+ throw new Error('SQLite state backend unavailable while reading studio events');
655
659
  }
656
-
657
- const content = await fileSystem.readFile(eventFile, 'utf8');
658
- const lines = content
659
- .split(/\r?\n/)
660
- .map((line) => line.trim())
661
- .filter(Boolean);
662
-
663
- const parsed = [];
664
- for (const line of lines) {
665
- try {
666
- const payload = JSON.parse(line);
667
- parsed.push(payload);
668
- } catch (_error) {
669
- // Ignore malformed lines to keep event stream robust.
670
- }
671
- }
672
-
673
- if (limit <= 0) {
674
- return parsed;
675
- }
676
- return parsed.slice(-limit);
660
+ return events;
677
661
  }
678
662
 
679
663
  async function loadJob(paths, jobId, fileSystem = fs) {
@@ -1333,25 +1317,148 @@ function collectTaskEvidence(job = {}, stageName = '', stageMetadata = {}) {
1333
1317
  if (normalizeString(job && job.job_id)) {
1334
1318
  evidence.push({
1335
1319
  type: 'event-log',
1336
- ref: `.sce/studio/events/${job.job_id}.jsonl`,
1337
- detail: 'raw-audit-stream'
1320
+ ref: '.sce/state/sce-state.sqlite',
1321
+ detail: `studio_event_stream:job_id=${job.job_id}`
1338
1322
  });
1339
1323
  }
1340
1324
 
1341
1325
  return normalizeTaskEvidence(evidence);
1342
1326
  }
1343
1327
 
1344
- function buildTaskSummaryLines(job = {}, stageName = '', taskStatus = '', nextAction = '') {
1328
+ function buildTaskSummaryLines(job = {}, stageName = '', taskStatus = '', nextAction = '', taskRef = '') {
1345
1329
  const sceneId = normalizeString(job?.scene?.id) || 'scene.n/a';
1346
1330
  const specId = normalizeString(job?.scene?.spec_id) || normalizeString(job?.source?.spec_id) || 'spec.n/a';
1347
1331
  const progress = buildProgress(job);
1348
1332
  return [
1349
- `Stage: ${stageName || 'plan'} | Status: ${taskStatus || 'unknown'}`,
1333
+ `Stage: ${stageName || 'plan'} | Status: ${taskStatus || 'unknown'}${taskRef ? ` | Ref: ${taskRef}` : ''}`,
1350
1334
  `Scene: ${sceneId} | Spec: ${specId} | Progress: ${progress.completed}/${progress.total}`,
1351
1335
  `Next: ${nextAction || 'n/a'}`
1352
1336
  ];
1353
1337
  }
1354
1338
 
1339
+ function truncateTaskText(value = '', maxLength = 96) {
1340
+ const normalized = normalizeString(value).replace(/\s+/g, ' ');
1341
+ if (!normalized) {
1342
+ return '';
1343
+ }
1344
+ if (normalized.length <= maxLength) {
1345
+ return normalized;
1346
+ }
1347
+ return `${normalized.slice(0, Math.max(0, maxLength - 3)).trim()}...`;
1348
+ }
1349
+
1350
+ function dedupeTaskList(items = [], limit = 3) {
1351
+ const seen = new Set();
1352
+ const result = [];
1353
+ for (const item of items) {
1354
+ const normalized = truncateTaskText(item, 120);
1355
+ if (!normalized) {
1356
+ continue;
1357
+ }
1358
+ const key = normalized.toLowerCase();
1359
+ if (seen.has(key)) {
1360
+ continue;
1361
+ }
1362
+ seen.add(key);
1363
+ result.push(normalized);
1364
+ if (result.length >= limit) {
1365
+ break;
1366
+ }
1367
+ }
1368
+ return result;
1369
+ }
1370
+
1371
+ function splitTaskRawRequest(rawRequest = '') {
1372
+ const normalized = normalizeString(rawRequest).replace(/\s+/g, ' ');
1373
+ if (!normalized) {
1374
+ return [];
1375
+ }
1376
+ const chunks = normalized
1377
+ .split(/(?:\r?\n|[;;。!?!?]|(?:\s+\band\b\s+)|(?:\s+\bthen\b\s+)|(?:\s+\balso\b\s+)|(?:\s*并且\s*)|(?:\s*同时\s*)|(?:\s*以及\s*)|(?:\s*然后\s*))/gi)
1378
+ .map((item) => normalizeString(item).replace(/^(?:and|then|also)\s+/i, ''))
1379
+ .filter(Boolean);
1380
+ return dedupeTaskList(chunks, 3);
1381
+ }
1382
+
1383
+ function deriveTaskIntentShape(rawRequest = '', stageName = '') {
1384
+ const normalizedRaw = normalizeString(rawRequest).replace(/\s+/g, ' ');
1385
+ const clauses = splitTaskRawRequest(normalizedRaw);
1386
+ const hasRaw = normalizedRaw.length > 0;
1387
+ const inferredSubGoals = clauses.length > 1 ? clauses.slice(0, 3) : [];
1388
+ const needsSplit = inferredSubGoals.length > 1;
1389
+ const titleSource = clauses.length > 0
1390
+ ? clauses[0]
1391
+ : (hasRaw ? normalizedRaw : `Studio ${stageName || 'task'} execution`);
1392
+
1393
+ let confidence = hasRaw ? 0.9 : 0.6;
1394
+ if (needsSplit) {
1395
+ confidence = 0.72;
1396
+ }
1397
+ if (normalizeString(stageName) && normalizeString(stageName) !== 'plan') {
1398
+ confidence = Math.min(0.95, confidence + 0.03);
1399
+ }
1400
+
1401
+ return {
1402
+ title_norm: truncateTaskText(titleSource, 96) || `Studio ${stageName || 'task'} execution`,
1403
+ raw_request: hasRaw ? normalizedRaw : null,
1404
+ sub_goals: inferredSubGoals,
1405
+ needs_split: needsSplit,
1406
+ confidence: Number(confidence.toFixed(2))
1407
+ };
1408
+ }
1409
+
1410
+ function buildTaskAcceptanceCriteria(stageName = '', job = {}, nextAction = '') {
1411
+ const normalizedStage = normalizeString(stageName) || 'task';
1412
+ const artifacts = job && job.artifacts ? job.artifacts : {};
1413
+ const criteriaByStage = {
1414
+ plan: [
1415
+ 'Scene/spec binding is resolved and persisted in studio job metadata.',
1416
+ 'Plan stage problem evaluation passes with no blockers.',
1417
+ `Next action is executable (${nextAction || 'sce studio generate --job <job-id>'}).`
1418
+ ],
1419
+ generate: [
1420
+ 'Patch bundle id is produced for downstream apply stage.',
1421
+ 'Generate stage report is written to artifacts.',
1422
+ `Next action is executable (${nextAction || 'sce studio apply --patch-bundle <id> --job <job-id>'}).`
1423
+ ],
1424
+ apply: [
1425
+ 'Authorization requirements are satisfied for apply stage.',
1426
+ 'Apply stage completes without policy blockers.',
1427
+ `Next action is executable (${nextAction || 'sce studio verify --job <job-id>'}).`
1428
+ ],
1429
+ verify: [
1430
+ 'Verification gates finish with no required-step failures.',
1431
+ `Verify report is available (${normalizeString(artifacts.verify_report) || 'artifact pending'}).`,
1432
+ `Next action is executable (${nextAction || 'sce studio release --job <job-id>'}).`
1433
+ ],
1434
+ release: [
1435
+ 'Release gates pass under configured release profile.',
1436
+ `Release reference is emitted (${normalizeString(artifacts.release_ref) || 'artifact pending'}).`,
1437
+ `Next action is executable (${nextAction || 'complete'}).`
1438
+ ],
1439
+ rollback: [
1440
+ 'Rollback stage transitions job status to rolled_back.',
1441
+ 'Rollback evidence is appended to studio event stream.',
1442
+ `Recovery next action is executable (${nextAction || 'sce studio plan --scene <scene-id> --from-chat <session>'}).`
1443
+ ],
1444
+ events: [
1445
+ 'Events stream payload is available for task-level audit.',
1446
+ 'Task envelope preserves normalized IDs and handoff fields.',
1447
+ `Next action is explicit (${nextAction || 'n/a'}).`
1448
+ ],
1449
+ resume: [
1450
+ 'Current job status and stage progress are restored deterministically.',
1451
+ 'Task envelope remains schema-compatible for downstream UI.',
1452
+ `Next action is explicit (${nextAction || 'n/a'}).`
1453
+ ]
1454
+ };
1455
+ return criteriaByStage[normalizedStage] || [
1456
+ 'Task envelope contains normalized identifiers and task contract fields.',
1457
+ 'Task output preserves evidence, command logs, and error bundles.',
1458
+ `Next action is explicit (${nextAction || 'n/a'}).`
1459
+ ];
1460
+ }
1461
+
1355
1462
  function buildTaskEnvelope(mode, job, options = {}) {
1356
1463
  const stageName = resolveTaskStage(mode, job, options.stageName);
1357
1464
  const stageState = stageName && job && job.stages && job.stages[stageName]
@@ -1375,11 +1482,14 @@ function buildTaskEnvelope(mode, job, options = {}) {
1375
1482
  || (normalizeString(job && job.job_id)
1376
1483
  ? `${job.job_id}:${stageName || 'task'}`
1377
1484
  : null);
1378
- const goal = normalizeString(job?.source?.goal)
1485
+ const rawRequest = normalizeString(job?.source?.goal);
1486
+ const goal = rawRequest
1379
1487
  || `Studio ${stageName || 'task'} execution`;
1488
+ const taskIntent = deriveTaskIntentShape(rawRequest, stageName);
1380
1489
  const sessionId = normalizeString(job?.session?.scene_session_id) || null;
1381
1490
  const sceneId = normalizeString(job?.scene?.id) || null;
1382
1491
  const specId = normalizeString(job?.scene?.spec_id) || normalizeString(job?.source?.spec_id) || null;
1492
+ const taskRef = normalizeString(options.taskRef) || null;
1383
1493
 
1384
1494
  const commands = normalizeTaskCommands([
1385
1495
  ...(Array.isArray(stageMetadata.commands) ? stageMetadata.commands : []),
@@ -1401,17 +1511,31 @@ function buildTaskEnvelope(mode, job, options = {}) {
1401
1511
  release_ref: normalizeString(stageMetadata.release_ref) || normalizeString(job?.artifacts?.release_ref) || null
1402
1512
  };
1403
1513
 
1514
+ const normalizedHandoff = {
1515
+ ...handoff,
1516
+ task_ref: taskRef
1517
+ };
1518
+
1404
1519
  return {
1405
1520
  sessionId,
1406
1521
  sceneId,
1407
1522
  specId,
1408
1523
  taskId,
1524
+ taskRef,
1409
1525
  eventId: normalizeString(latestEvent && latestEvent.event_id) || null,
1410
1526
  task: {
1527
+ ref: taskRef,
1528
+ task_ref: taskRef,
1529
+ title_norm: taskIntent.title_norm,
1530
+ raw_request: taskIntent.raw_request,
1411
1531
  goal,
1532
+ sub_goals: taskIntent.sub_goals,
1533
+ acceptance_criteria: buildTaskAcceptanceCriteria(stageName, job, nextAction),
1534
+ needs_split: taskIntent.needs_split,
1535
+ confidence: taskIntent.confidence,
1412
1536
  status: taskStatus,
1413
- summary: buildTaskSummaryLines(job, stageName, taskStatus, nextAction),
1414
- handoff,
1537
+ summary: buildTaskSummaryLines(job, stageName, taskStatus, nextAction, taskRef),
1538
+ handoff: normalizedHandoff,
1415
1539
  next_action: nextAction,
1416
1540
  file_changes: fileChanges,
1417
1541
  commands,
@@ -1862,7 +1986,49 @@ function ensureNotRolledBack(job, stageName) {
1862
1986
  }
1863
1987
  }
1864
1988
 
1865
- function buildCommandPayload(mode, job, options = {}) {
1989
+ function buildStudioTaskKey(stageName = '') {
1990
+ const normalizedStage = normalizeString(stageName) || 'task';
1991
+ return `studio:${normalizedStage}`;
1992
+ }
1993
+
1994
+ async function resolveTaskReference(mode, job, options = {}) {
1995
+ const explicitTaskRef = normalizeString(options.taskRef);
1996
+ if (explicitTaskRef) {
1997
+ return explicitTaskRef;
1998
+ }
1999
+
2000
+ const sceneId = normalizeString(job?.scene?.id);
2001
+ const specId = normalizeString(job?.scene?.spec_id) || normalizeString(job?.source?.spec_id);
2002
+ if (!sceneId || !specId) {
2003
+ return null;
2004
+ }
2005
+
2006
+ const stageName = resolveTaskStage(mode, job, options.stageName);
2007
+ const taskKey = normalizeString(options.taskKey) || buildStudioTaskKey(stageName);
2008
+ const projectPath = normalizeString(options.projectPath) || process.cwd();
2009
+ const fileSystem = options.fileSystem || fs;
2010
+ const taskRefRegistry = options.taskRefRegistry || new TaskRefRegistry(projectPath, { fileSystem });
2011
+
2012
+ try {
2013
+ const taskRef = await taskRefRegistry.resolveOrCreateRef({
2014
+ sceneId,
2015
+ specId,
2016
+ taskKey,
2017
+ source: 'studio-stage',
2018
+ metadata: {
2019
+ mode: normalizeString(mode) || null,
2020
+ stage: stageName || null,
2021
+ job_id: normalizeString(job?.job_id) || null
2022
+ }
2023
+ });
2024
+ return taskRef.task_ref;
2025
+ } catch (_error) {
2026
+ return null;
2027
+ }
2028
+ }
2029
+
2030
+ async function buildCommandPayload(mode, job, options = {}) {
2031
+ const taskRef = await resolveTaskReference(mode, job, options);
1866
2032
  const base = {
1867
2033
  mode,
1868
2034
  success: true,
@@ -1874,7 +2040,10 @@ function buildCommandPayload(mode, job, options = {}) {
1874
2040
  };
1875
2041
  return {
1876
2042
  ...base,
1877
- ...buildTaskEnvelope(mode, job, options)
2043
+ ...buildTaskEnvelope(mode, job, {
2044
+ ...options,
2045
+ taskRef
2046
+ })
1878
2047
  };
1879
2048
  }
1880
2049
 
@@ -2367,9 +2536,11 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
2367
2536
  }, fileSystem);
2368
2537
  await writeLatestJob(paths, jobId, fileSystem);
2369
2538
 
2370
- const payload = buildCommandPayload('studio-plan', job, {
2539
+ const payload = await buildCommandPayload('studio-plan', job, {
2371
2540
  stageName: 'plan',
2372
- event: planEvent
2541
+ event: planEvent,
2542
+ projectPath,
2543
+ fileSystem
2373
2544
  });
2374
2545
  payload.scene = {
2375
2546
  id: sceneId,
@@ -2465,9 +2636,11 @@ async function runStudioGenerateCommand(options = {}, dependencies = {}) {
2465
2636
  }, fileSystem);
2466
2637
  await writeLatestJob(paths, jobId, fileSystem);
2467
2638
 
2468
- const payload = buildCommandPayload('studio-generate', job, {
2639
+ const payload = await buildCommandPayload('studio-generate', job, {
2469
2640
  stageName: 'generate',
2470
- event: generateEvent
2641
+ event: generateEvent,
2642
+ projectPath,
2643
+ fileSystem
2471
2644
  });
2472
2645
  printStudioPayload(payload, options);
2473
2646
  return payload;
@@ -2535,9 +2708,11 @@ async function runStudioApplyCommand(options = {}, dependencies = {}) {
2535
2708
  }, fileSystem);
2536
2709
  await writeLatestJob(paths, jobId, fileSystem);
2537
2710
 
2538
- const payload = buildCommandPayload('studio-apply', job, {
2711
+ const payload = await buildCommandPayload('studio-apply', job, {
2539
2712
  stageName: 'apply',
2540
- event: applyEvent
2713
+ event: applyEvent,
2714
+ projectPath,
2715
+ fileSystem
2541
2716
  });
2542
2717
  printStudioPayload(payload, options);
2543
2718
  return payload;
@@ -2685,9 +2860,11 @@ async function runStudioVerifyCommand(options = {}, dependencies = {}) {
2685
2860
  }, fileSystem);
2686
2861
  await writeLatestJob(paths, jobId, fileSystem);
2687
2862
 
2688
- const payload = buildCommandPayload('studio-verify', job, {
2863
+ const payload = await buildCommandPayload('studio-verify', job, {
2689
2864
  stageName: 'verify',
2690
- event: verifyEvent
2865
+ event: verifyEvent,
2866
+ projectPath,
2867
+ fileSystem
2691
2868
  });
2692
2869
  printStudioPayload(payload, options);
2693
2870
  return payload;
@@ -2894,9 +3071,11 @@ async function runStudioReleaseCommand(options = {}, dependencies = {}) {
2894
3071
  }, fileSystem);
2895
3072
  await writeLatestJob(paths, jobId, fileSystem);
2896
3073
 
2897
- const payload = buildCommandPayload('studio-release', job, {
3074
+ const payload = await buildCommandPayload('studio-release', job, {
2898
3075
  stageName: 'release',
2899
- event: releaseEvent
3076
+ event: releaseEvent,
3077
+ projectPath,
3078
+ fileSystem
2900
3079
  });
2901
3080
  printStudioPayload(payload, options);
2902
3081
  return payload;
@@ -2916,8 +3095,10 @@ async function runStudioResumeCommand(options = {}, dependencies = {}) {
2916
3095
 
2917
3096
  const job = await loadJob(paths, jobId, fileSystem);
2918
3097
  const events = await readStudioEvents(paths, jobId, { limit: 20 }, fileSystem);
2919
- const payload = buildCommandPayload('studio-resume', job, {
2920
- events
3098
+ const payload = await buildCommandPayload('studio-resume', job, {
3099
+ events,
3100
+ projectPath,
3101
+ fileSystem
2921
3102
  });
2922
3103
  payload.success = true;
2923
3104
  printStudioPayload(payload, options);
@@ -2975,9 +3156,11 @@ async function runStudioRollbackCommand(options = {}, dependencies = {}) {
2975
3156
  }, fileSystem);
2976
3157
  await writeLatestJob(paths, jobId, fileSystem);
2977
3158
 
2978
- const payload = buildCommandPayload('studio-rollback', job, {
3159
+ const payload = await buildCommandPayload('studio-rollback', job, {
2979
3160
  stageName: 'rollback',
2980
- event: rollbackEvent
3161
+ event: rollbackEvent,
3162
+ projectPath,
3163
+ fileSystem
2981
3164
  });
2982
3165
  payload.rollback = { ...job.rollback };
2983
3166
  printStudioPayload(payload, options);
@@ -3035,7 +3218,11 @@ async function runStudioEventsCommand(options = {}, dependencies = {}) {
3035
3218
  sourceStream = 'openhands';
3036
3219
  }
3037
3220
 
3038
- const payload = buildCommandPayload('studio-events', job, { events });
3221
+ const payload = await buildCommandPayload('studio-events', job, {
3222
+ events,
3223
+ projectPath,
3224
+ fileSystem
3225
+ });
3039
3226
  payload.limit = limit;
3040
3227
  payload.source_stream = sourceStream;
3041
3228
  if (sourceStream === 'openhands') {