scene-capability-engine 3.6.29 → 3.6.32

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.
@@ -1,4 +1,4 @@
1
- const path = require('path');
1
+ const path = require('path');
2
2
  const crypto = require('crypto');
3
3
  const { spawnSync } = require('child_process');
4
4
  const fs = require('fs-extra');
@@ -20,6 +20,13 @@ const {
20
20
  runStudioSpecGovernance,
21
21
  runStudioSceneBackfill
22
22
  } = require('../studio/spec-intake-governor');
23
+ const { attachTaskFeedbackModel } = require('../magicball/task-feedback-model');
24
+ const { deriveTaskIntentShape, buildTaskSummaryLines, buildTaskAcceptanceCriteria } = require('../studio/task-intent');
25
+ const {
26
+ buildStudioTaskKey,
27
+ resolveTaskReference: resolveSharedTaskReference,
28
+ buildCommandPayload: buildSharedCommandPayload
29
+ } = require('../studio/task-envelope');
23
30
 
24
31
  const STUDIO_JOB_API_VERSION = 'sce.studio.job/v0.1';
25
32
  const STAGE_ORDER = ['plan', 'generate', 'apply', 'verify', 'release'];
@@ -1230,490 +1237,6 @@ function extractErrorsFromStageMetadata(stageState = {}, stageMetadata = {}) {
1230
1237
  return [...directErrors, ...gateStepErrors];
1231
1238
  }
1232
1239
 
1233
- function collectTaskFileChanges(job = {}, stageName = '', stageMetadata = {}) {
1234
- const fileChanges = [];
1235
- if (Array.isArray(stageMetadata && stageMetadata.file_changes)) {
1236
- fileChanges.push(...stageMetadata.file_changes);
1237
- }
1238
-
1239
- const createdSpec = job?.source?.intake?.created_spec;
1240
- const createdSpecId = createdSpec && createdSpec.created
1241
- ? normalizeString(createdSpec.spec_id)
1242
- : '';
1243
- if (stageName === 'plan' && createdSpecId) {
1244
- fileChanges.push(
1245
- { path: `.sce/specs/${createdSpecId}/requirements.md`, line: 1 },
1246
- { path: `.sce/specs/${createdSpecId}/design.md`, line: 1 },
1247
- { path: `.sce/specs/${createdSpecId}/tasks.md`, line: 1 },
1248
- { path: `.sce/specs/${createdSpecId}/custom/problem-domain-chain.json`, line: 1 },
1249
- { path: `.sce/specs/${createdSpecId}/custom/problem-contract.json`, line: 1 }
1250
- );
1251
- }
1252
-
1253
- const artifacts = job && job.artifacts ? job.artifacts : {};
1254
- if (stageName === 'plan') {
1255
- if (normalizeString(artifacts.spec_portfolio_report)) {
1256
- fileChanges.push({ path: artifacts.spec_portfolio_report, line: 1 });
1257
- }
1258
- if (normalizeString(artifacts.spec_scene_index)) {
1259
- fileChanges.push({ path: artifacts.spec_scene_index, line: 1 });
1260
- }
1261
- }
1262
- if (stageName === 'generate' && normalizeString(artifacts.generate_report)) {
1263
- fileChanges.push({ path: artifacts.generate_report, line: 1 });
1264
- }
1265
- if (stageName === 'verify' && normalizeString(artifacts.verify_report)) {
1266
- fileChanges.push({ path: artifacts.verify_report, line: 1 });
1267
- }
1268
- if (stageName === 'release' && normalizeString(artifacts.release_report)) {
1269
- fileChanges.push({ path: artifacts.release_report, line: 1 });
1270
- }
1271
- return normalizeTaskFileChanges(fileChanges);
1272
- }
1273
-
1274
- function collectTaskEvidence(job = {}, stageName = '', stageMetadata = {}) {
1275
- const evidence = [];
1276
- const stageReport = normalizeString(stageMetadata && stageMetadata.report);
1277
- if (stageReport) {
1278
- evidence.push({ type: 'stage-report', ref: stageReport, detail: stageName });
1279
- }
1280
-
1281
- const artifacts = job && job.artifacts ? job.artifacts : {};
1282
- if (stageName && artifacts.problem_eval_reports && artifacts.problem_eval_reports[stageName]) {
1283
- evidence.push({
1284
- type: 'problem-evaluation-report',
1285
- ref: artifacts.problem_eval_reports[stageName],
1286
- detail: stageName
1287
- });
1288
- }
1289
-
1290
- if (normalizeString(job?.source?.domain_chain?.chain_path)) {
1291
- evidence.push({
1292
- type: 'domain-chain',
1293
- ref: job.source.domain_chain.chain_path,
1294
- detail: stageName
1295
- });
1296
- }
1297
- if (normalizeString(job?.source?.problem_contract_path)) {
1298
- evidence.push({
1299
- type: 'problem-contract',
1300
- ref: job.source.problem_contract_path,
1301
- detail: stageName
1302
- });
1303
- }
1304
- if (Array.isArray(stageMetadata && stageMetadata.auto_errorbook_records)) {
1305
- for (const item of stageMetadata.auto_errorbook_records) {
1306
- const entryId = normalizeString(item && item.entry_id);
1307
- if (!entryId) {
1308
- continue;
1309
- }
1310
- evidence.push({
1311
- type: 'errorbook-entry',
1312
- ref: entryId,
1313
- detail: normalizeString(item && item.step_id) || null
1314
- });
1315
- }
1316
- }
1317
-
1318
- if (normalizeString(job && job.job_id)) {
1319
- evidence.push({
1320
- type: 'event-log',
1321
- ref: '.sce/state/sce-state.sqlite',
1322
- detail: `studio_event_stream:job_id=${job.job_id}`
1323
- });
1324
- }
1325
-
1326
- return normalizeTaskEvidence(evidence);
1327
- }
1328
-
1329
- function buildTaskSummaryLines(job = {}, stageName = '', taskStatus = '', nextAction = '', taskRef = '') {
1330
- const sceneId = normalizeString(job?.scene?.id) || 'scene.n/a';
1331
- const specId = normalizeString(job?.scene?.spec_id) || normalizeString(job?.source?.spec_id) || 'spec.n/a';
1332
- const progress = buildProgress(job);
1333
- return [
1334
- `Stage: ${stageName || 'plan'} | Status: ${taskStatus || 'unknown'}${taskRef ? ` | Ref: ${taskRef}` : ''}`,
1335
- `Scene: ${sceneId} | Spec: ${specId} | Progress: ${progress.completed}/${progress.total}`,
1336
- `Next: ${nextAction || 'n/a'}`
1337
- ];
1338
- }
1339
-
1340
- function truncateTaskText(value = '', maxLength = 96) {
1341
- const normalized = normalizeString(value).replace(/\s+/g, ' ');
1342
- if (!normalized) {
1343
- return '';
1344
- }
1345
- if (normalized.length <= maxLength) {
1346
- return normalized;
1347
- }
1348
- return `${normalized.slice(0, Math.max(0, maxLength - 3)).trim()}...`;
1349
- }
1350
-
1351
- function dedupeTaskList(items = [], limit = 3) {
1352
- const seen = new Set();
1353
- const result = [];
1354
- for (const item of items) {
1355
- const normalized = truncateTaskText(item, 120);
1356
- if (!normalized) {
1357
- continue;
1358
- }
1359
- const key = normalized.toLowerCase();
1360
- if (seen.has(key)) {
1361
- continue;
1362
- }
1363
- seen.add(key);
1364
- result.push(normalized);
1365
- if (result.length >= limit) {
1366
- break;
1367
- }
1368
- }
1369
- return result;
1370
- }
1371
-
1372
- function splitTaskRawRequest(rawRequest = '') {
1373
- const normalized = normalizeString(rawRequest).replace(/\s+/g, ' ');
1374
- if (!normalized) {
1375
- return [];
1376
- }
1377
- const chunks = normalized
1378
- .split(/(?:\r?\n|[;;。!?!?]|(?:\s+\band\b\s+)|(?:\s+\bthen\b\s+)|(?:\s+\balso\b\s+)|(?:\s*并且\s*)|(?:\s*同时\s*)|(?:\s*以及\s*)|(?:\s*然后\s*))/gi)
1379
- .map((item) => normalizeString(item).replace(/^(?:and|then|also)\s+/i, ''))
1380
- .filter(Boolean);
1381
- return dedupeTaskList(chunks, 3);
1382
- }
1383
-
1384
- function deriveTaskIntentShape(rawRequest = '', stageName = '') {
1385
- const normalizedRaw = normalizeString(rawRequest).replace(/\s+/g, ' ');
1386
- const clauses = splitTaskRawRequest(normalizedRaw);
1387
- const hasRaw = normalizedRaw.length > 0;
1388
- const inferredSubGoals = clauses.length > 1 ? clauses.slice(0, 3) : [];
1389
- const needsSplit = inferredSubGoals.length > 1;
1390
- const titleSource = clauses.length > 0
1391
- ? clauses[0]
1392
- : (hasRaw ? normalizedRaw : `Studio ${stageName || 'task'} execution`);
1393
-
1394
- let confidence = hasRaw ? 0.9 : 0.6;
1395
- if (needsSplit) {
1396
- confidence = 0.72;
1397
- }
1398
- if (normalizeString(stageName) && normalizeString(stageName) !== 'plan') {
1399
- confidence = Math.min(0.95, confidence + 0.03);
1400
- }
1401
-
1402
- return {
1403
- title_norm: truncateTaskText(titleSource, 96) || `Studio ${stageName || 'task'} execution`,
1404
- raw_request: hasRaw ? normalizedRaw : null,
1405
- sub_goals: inferredSubGoals,
1406
- needs_split: needsSplit,
1407
- confidence: Number(confidence.toFixed(2))
1408
- };
1409
- }
1410
-
1411
- function buildTaskAcceptanceCriteria(stageName = '', job = {}, nextAction = '') {
1412
- const normalizedStage = normalizeString(stageName) || 'task';
1413
- const artifacts = job && job.artifacts ? job.artifacts : {};
1414
- const criteriaByStage = {
1415
- plan: [
1416
- 'Scene/spec binding is resolved and persisted in studio job metadata.',
1417
- 'Plan stage problem evaluation passes with no blockers.',
1418
- `Next action is executable (${nextAction || 'sce studio generate --job <job-id>'}).`
1419
- ],
1420
- generate: [
1421
- 'Patch bundle id is produced for downstream apply stage.',
1422
- 'Generate stage report is written to artifacts.',
1423
- `Next action is executable (${nextAction || 'sce studio apply --patch-bundle <id> --job <job-id>'}).`
1424
- ],
1425
- apply: [
1426
- 'Authorization requirements are satisfied for apply stage.',
1427
- 'Apply stage completes without policy blockers.',
1428
- `Next action is executable (${nextAction || 'sce studio verify --job <job-id>'}).`
1429
- ],
1430
- verify: [
1431
- 'Verification gates finish with no required-step failures.',
1432
- `Verify report is available (${normalizeString(artifacts.verify_report) || 'artifact pending'}).`,
1433
- `Next action is executable (${nextAction || 'sce studio release --job <job-id>'}).`
1434
- ],
1435
- release: [
1436
- 'Release gates pass under configured release profile.',
1437
- `Release reference is emitted (${normalizeString(artifacts.release_ref) || 'artifact pending'}).`,
1438
- `Next action is executable (${nextAction || 'complete'}).`
1439
- ],
1440
- rollback: [
1441
- 'Rollback stage transitions job status to rolled_back.',
1442
- 'Rollback evidence is appended to studio event stream.',
1443
- `Recovery next action is executable (${nextAction || 'sce studio plan --scene <scene-id> --from-chat <session>'}).`
1444
- ],
1445
- events: [
1446
- 'Events stream payload is available for task-level audit.',
1447
- 'Task envelope preserves normalized IDs and handoff fields.',
1448
- `Next action is explicit (${nextAction || 'n/a'}).`
1449
- ],
1450
- resume: [
1451
- 'Current job status and stage progress are restored deterministically.',
1452
- 'Task envelope remains schema-compatible for downstream UI.',
1453
- `Next action is explicit (${nextAction || 'n/a'}).`
1454
- ]
1455
- };
1456
- return criteriaByStage[normalizedStage] || [
1457
- 'Task envelope contains normalized identifiers and task contract fields.',
1458
- 'Task output preserves evidence, command logs, and error bundles.',
1459
- `Next action is explicit (${nextAction || 'n/a'}).`
1460
- ];
1461
- }
1462
-
1463
- function buildTaskFeedbackModel(payload = {}) {
1464
- const task = payload && payload.task && typeof payload.task === 'object' ? payload.task : {};
1465
- const handoff = task && typeof task.handoff === 'object' ? task.handoff : {};
1466
- const errors = Array.isArray(task.errors) ? task.errors : [];
1467
- const commands = Array.isArray(task.commands) ? task.commands : [];
1468
- const fileChanges = Array.isArray(task.file_changes) ? task.file_changes : [];
1469
- const evidence = Array.isArray(task.evidence) ? task.evidence : [];
1470
- const acceptance = Array.isArray(task.acceptance_criteria) ? task.acceptance_criteria : [];
1471
- const firstError = errors[0] || {};
1472
- const stage = normalizeString(handoff.stage) || 'task';
1473
- const status = normalizeString(task.status) || 'unknown';
1474
- const problemComponent = normalizeString(handoff.component) || normalizeString(payload.sceneId) || null;
1475
- const expected = normalizeString(acceptance[0]) || ('Complete ' + stage + ' stage successfully');
1476
- const actual = normalizeString(firstError.message) || ('Current status: ' + status);
1477
-
1478
- let chainCheckpoint = 'task-envelope';
1479
- if (errors.length > 0 && commands.length > 0) {
1480
- chainCheckpoint = 'command-execution';
1481
- } else if (errors.length > 0) {
1482
- chainCheckpoint = 'stage-gate';
1483
- } else if (fileChanges.length > 0) {
1484
- chainCheckpoint = 'patch-applied';
1485
- } else if (evidence.length > 0) {
1486
- chainCheckpoint = 'evidence-collected';
1487
- }
1488
-
1489
- let confidence = 'low';
1490
- if (errors.length > 0) {
1491
- confidence = 'medium';
1492
- } else if (status === 'completed') {
1493
- confidence = 'high';
1494
- }
1495
-
1496
- let recommendedAction = '继续当前阶段';
1497
- if (errors.length > 0) {
1498
- recommendedAction = '处理阻断后重试';
1499
- } else if (status === 'completed' && normalizeString(task.next_action) && normalizeString(task.next_action) !== 'complete') {
1500
- recommendedAction = '执行下一阶段';
1501
- } else if (status === 'completed') {
1502
- recommendedAction = '任务完成';
1503
- }
1504
-
1505
- return {
1506
- version: '1.0',
1507
- problem: {
1508
- component: problemComponent,
1509
- action: normalizeString(handoff.action) || stage,
1510
- expected,
1511
- actual
1512
- },
1513
- execution: {
1514
- stage,
1515
- status,
1516
- summary: Array.isArray(task.summary) ? task.summary.slice(0, 3) : [],
1517
- blocking_summary: normalizeString(handoff.blocking_summary) || normalizeString(firstError.message) || null
1518
- },
1519
- diagnosis: {
1520
- hypothesis: normalizeString(firstError.error_bundle) || normalizeString(firstError.message) || normalizeString(handoff.reason) || null,
1521
- chain_checkpoint: chainCheckpoint,
1522
- root_cause_confidence: confidence
1523
- },
1524
- evidence: {
1525
- file_count: fileChanges.length,
1526
- file_paths: fileChanges.slice(0, 5).map((item) => normalizeString(item && item.path)).filter(Boolean),
1527
- command_count: commands.length,
1528
- error_count: errors.length,
1529
- verification_result: status === 'completed' ? 'passed-or-advanced' : (errors.length > 0 ? 'blocked' : 'in-progress'),
1530
- regression_scope: Array.isArray(handoff.regression_scope) ? handoff.regression_scope : []
1531
- },
1532
- next_step: {
1533
- recommended_action: recommendedAction,
1534
- next_action: normalizeString(task.next_action) || null,
1535
- next_command: normalizeString(task.next_action) || null
1536
- }
1537
- };
1538
- }
1539
-
1540
- function buildMagicballStatusLanguage(input = {}) {
1541
- const attention = normalizeString(input.attention_level) || 'medium';
1542
- const status = normalizeString(input.status) || 'unknown';
1543
- const blockingSummary = normalizeString(input.blocking_summary) || null;
1544
- const recommendedAction = normalizeString(input.recommended_action) || null;
1545
- return {
1546
- attention_level: attention,
1547
- status_tone: attention === 'critical' ? 'danger' : (attention === 'high' ? 'warning' : (attention === 'low' ? 'success' : 'info')),
1548
- status_label: status || 'unknown',
1549
- blocking_summary: blockingSummary,
1550
- recommended_action: recommendedAction
1551
- };
1552
- }
1553
-
1554
- function attachTaskFeedbackModel(payload = {}) {
1555
- if (!payload || typeof payload !== 'object' || !payload.task || typeof payload.task !== 'object') {
1556
- return payload;
1557
- }
1558
- return {
1559
- ...payload,
1560
- task: {
1561
- ...payload.task,
1562
- feedback_model: (() => { const model = buildTaskFeedbackModel(payload); return { ...model, mb_status: buildMagicballStatusLanguage({ status: model.execution.status, attention_level: model.diagnosis.root_cause_confidence === 'high' ? 'high' : (model.execution.status === 'completed' ? 'low' : 'medium'), blocking_summary: model.execution.blocking_summary, recommended_action: model.next_step.recommended_action }) }; })()
1563
- }
1564
- };
1565
- }
1566
- function buildTaskEnvelope(mode, job, options = {}) {
1567
- const stageName = resolveTaskStage(mode, job, options.stageName);
1568
- const stageState = stageName && job && job.stages && job.stages[stageName]
1569
- ? job.stages[stageName]
1570
- : {};
1571
- const stageMetadata = stageState && typeof stageState.metadata === 'object' && stageState.metadata
1572
- ? stageState.metadata
1573
- : {};
1574
- const nextAction = resolveNextAction(job);
1575
-
1576
- const events = Array.isArray(options.events)
1577
- ? options.events
1578
- : (options.event ? [options.event] : []);
1579
- const latestEvent = events.length > 0 ? events[events.length - 1] : null;
1580
-
1581
- const taskStatus = normalizeString(stageState && stageState.status)
1582
- || (stageName === 'rollback' && normalizeString(job && job.status) === 'rolled_back'
1583
- ? 'completed'
1584
- : normalizeString(job && job.status) || 'unknown');
1585
- const taskId = normalizeString(options.taskId)
1586
- || (normalizeString(job && job.job_id)
1587
- ? `${job.job_id}:${stageName || 'task'}`
1588
- : null);
1589
- const rawRequest = normalizeString(job?.source?.goal);
1590
- const goal = rawRequest
1591
- || `Studio ${stageName || 'task'} execution`;
1592
- const taskIntent = deriveTaskIntentShape(rawRequest, stageName);
1593
- const sessionId = normalizeString(job?.session?.scene_session_id) || null;
1594
- const sceneId = normalizeString(job?.scene?.id) || null;
1595
- const specId = normalizeString(job?.scene?.spec_id) || normalizeString(job?.source?.spec_id) || null;
1596
- const taskRef = normalizeString(options.taskRef) || null;
1597
-
1598
- const commands = normalizeTaskCommands([
1599
- ...(Array.isArray(stageMetadata.commands) ? stageMetadata.commands : []),
1600
- ...extractCommandsFromStageMetadata(stageMetadata)
1601
- ]);
1602
- const errors = normalizeTaskErrors(
1603
- extractErrorsFromStageMetadata(stageState, stageMetadata)
1604
- );
1605
- const fileChanges = collectTaskFileChanges(job, stageName, stageMetadata);
1606
- const evidence = collectTaskEvidence(job, stageName, stageMetadata);
1607
-
1608
- const handoff = stageMetadata.handoff && typeof stageMetadata.handoff === 'object'
1609
- ? stageMetadata.handoff
1610
- : {
1611
- stage: stageName,
1612
- status: taskStatus,
1613
- completed_at: normalizeString(stageState && stageState.completed_at) || null,
1614
- report: normalizeString(stageMetadata.report) || null,
1615
- release_ref: normalizeString(stageMetadata.release_ref) || normalizeString(job?.artifacts?.release_ref) || null
1616
- };
1617
-
1618
- const normalizedHandoff = {
1619
- ...handoff,
1620
- task_ref: taskRef
1621
- };
1622
-
1623
- return {
1624
- sessionId,
1625
- sceneId,
1626
- specId,
1627
- taskId,
1628
- taskRef,
1629
- eventId: normalizeString(latestEvent && latestEvent.event_id) || null,
1630
- task: {
1631
- ref: taskRef,
1632
- task_ref: taskRef,
1633
- title_norm: taskIntent.title_norm,
1634
- raw_request: taskIntent.raw_request,
1635
- goal,
1636
- sub_goals: taskIntent.sub_goals,
1637
- acceptance_criteria: buildTaskAcceptanceCriteria(stageName, job, nextAction),
1638
- needs_split: taskIntent.needs_split,
1639
- confidence: taskIntent.confidence,
1640
- status: taskStatus,
1641
- summary: buildTaskSummaryLines(job, stageName, taskStatus, nextAction, taskRef),
1642
- handoff: normalizedHandoff,
1643
- next_action: nextAction,
1644
- file_changes: fileChanges,
1645
- commands,
1646
- errors,
1647
- evidence
1648
- },
1649
- event: events
1650
- };
1651
- }
1652
-
1653
- function toRelativePosix(projectPath, absolutePath) {
1654
- return path.relative(projectPath, absolutePath).replace(/\\/g, '/');
1655
- }
1656
-
1657
- async function readSpecDomainChain(projectPath, specId, fileSystem = fs) {
1658
- const specRoot = path.join(projectPath, '.sce', 'specs', specId);
1659
- const chainPath = path.join(specRoot, DOMAIN_CHAIN_RELATIVE_PATH);
1660
- if (!await fileSystem.pathExists(chainPath)) {
1661
- return null;
1662
- }
1663
- try {
1664
- const payload = await fileSystem.readJson(chainPath);
1665
- const stat = await fileSystem.stat(chainPath);
1666
- return {
1667
- spec_id: specId,
1668
- chain_path: toRelativePosix(projectPath, chainPath),
1669
- payload,
1670
- updated_at: stat && stat.mtime ? stat.mtime.toISOString() : null,
1671
- mtime_ms: Number(stat && stat.mtimeMs) || 0
1672
- };
1673
- } catch (_error) {
1674
- return null;
1675
- }
1676
- }
1677
-
1678
- async function readSpecProblemContract(projectPath, specId, fileSystem = fs) {
1679
- const specRoot = path.join(projectPath, '.sce', 'specs', specId);
1680
- const contractPath = path.join(specRoot, DEFAULT_PROBLEM_CONTRACT_RELATIVE_PATH);
1681
- if (!await fileSystem.pathExists(contractPath)) {
1682
- return null;
1683
- }
1684
- try {
1685
- const payload = await fileSystem.readJson(contractPath);
1686
- const stat = await fileSystem.stat(contractPath);
1687
- return {
1688
- spec_id: specId,
1689
- contract_path: toRelativePosix(projectPath, contractPath),
1690
- payload,
1691
- updated_at: stat && stat.mtime ? stat.mtime.toISOString() : null,
1692
- mtime_ms: Number(stat && stat.mtimeMs) || 0
1693
- };
1694
- } catch (_error) {
1695
- return null;
1696
- }
1697
- }
1698
-
1699
- async function readGovernanceSignals(projectPath, fileSystem = fs) {
1700
- const reportPath = path.join(projectPath, DEFAULT_INTERACTIVE_GOVERNANCE_REPORT);
1701
- if (!await fileSystem.pathExists(reportPath)) {
1702
- return {
1703
- available: false,
1704
- report_path: null,
1705
- high_breach_count: 0,
1706
- medium_breach_count: 0
1707
- };
1708
- }
1709
- const payload = await fileSystem.readJson(reportPath).catch(() => null);
1710
- const summary = extractGovernanceBreachSignals(payload || {});
1711
- return {
1712
- ...summary,
1713
- report_path: toRelativePosix(projectPath, reportPath)
1714
- };
1715
- }
1716
-
1717
1240
  async function readVerifyReportSignals(projectPath, verifyReportPath = '', fileSystem = fs) {
1718
1241
  const normalized = normalizeString(verifyReportPath);
1719
1242
  if (!normalized) {
@@ -1807,6 +1330,70 @@ function normalizeProblemContract(contract = {}, context = {}) {
1807
1330
  };
1808
1331
  }
1809
1332
 
1333
+ function toRelativePosix(projectPath, absolutePath) {
1334
+ return path.relative(projectPath, absolutePath).replace(/\\/g, '/');
1335
+ }
1336
+
1337
+ async function readSpecDomainChain(projectPath, specId, fileSystem = fs) {
1338
+ const specRoot = path.join(projectPath, '.sce', 'specs', specId);
1339
+ const chainPath = path.join(specRoot, DOMAIN_CHAIN_RELATIVE_PATH);
1340
+ if (!await fileSystem.pathExists(chainPath)) {
1341
+ return null;
1342
+ }
1343
+ try {
1344
+ const payload = await fileSystem.readJson(chainPath);
1345
+ const stat = await fileSystem.stat(chainPath);
1346
+ return {
1347
+ spec_id: specId,
1348
+ chain_path: toRelativePosix(projectPath, chainPath),
1349
+ payload,
1350
+ updated_at: stat && stat.mtime ? stat.mtime.toISOString() : null,
1351
+ mtime_ms: Number(stat && stat.mtimeMs) || 0
1352
+ };
1353
+ } catch (_error) {
1354
+ return null;
1355
+ }
1356
+ }
1357
+
1358
+ async function readSpecProblemContract(projectPath, specId, fileSystem = fs) {
1359
+ const specRoot = path.join(projectPath, '.sce', 'specs', specId);
1360
+ const contractPath = path.join(specRoot, DEFAULT_PROBLEM_CONTRACT_RELATIVE_PATH);
1361
+ if (!await fileSystem.pathExists(contractPath)) {
1362
+ return null;
1363
+ }
1364
+ try {
1365
+ const payload = await fileSystem.readJson(contractPath);
1366
+ const stat = await fileSystem.stat(contractPath);
1367
+ return {
1368
+ spec_id: specId,
1369
+ contract_path: toRelativePosix(projectPath, contractPath),
1370
+ payload,
1371
+ updated_at: stat && stat.mtime ? stat.mtime.toISOString() : null,
1372
+ mtime_ms: Number(stat && stat.mtimeMs) || 0
1373
+ };
1374
+ } catch (_error) {
1375
+ return null;
1376
+ }
1377
+ }
1378
+
1379
+ async function readGovernanceSignals(projectPath, fileSystem = fs) {
1380
+ const reportPath = path.join(projectPath, DEFAULT_INTERACTIVE_GOVERNANCE_REPORT);
1381
+ if (!await fileSystem.pathExists(reportPath)) {
1382
+ return {
1383
+ available: false,
1384
+ report_path: null,
1385
+ high_breach_count: 0,
1386
+ medium_breach_count: 0
1387
+ };
1388
+ }
1389
+ const payload = await fileSystem.readJson(reportPath).catch(() => null);
1390
+ const summary = extractGovernanceBreachSignals(payload || {});
1391
+ return {
1392
+ ...summary,
1393
+ report_path: toRelativePosix(projectPath, reportPath)
1394
+ };
1395
+ }
1396
+
1810
1397
  function extractGovernanceBreachSignals(report = {}) {
1811
1398
  if (!report || typeof report !== 'object') {
1812
1399
  return {
@@ -2049,109 +1636,38 @@ async function resolveDomainChainBinding(options = {}, dependencies = {}) {
2049
1636
  };
2050
1637
  }
2051
1638
 
2052
- function printStudioPayload(payload, options = {}) {
2053
- if (options.json) {
2054
- console.log(JSON.stringify(payload, null, 2));
2055
- return;
2056
- }
2057
-
2058
- console.log(chalk.blue(`Studio job: ${payload.job_id}`));
2059
- console.log(` Status: ${payload.status}`);
2060
- console.log(` Progress: ${payload.progress.completed}/${payload.progress.total} (${payload.progress.percent}%)`);
2061
- console.log(` Next: ${payload.next_action}`);
2062
- }
2063
-
2064
- function ensureStageCompleted(job, stageName, metadata = {}) {
2065
- if (!job.stages || !job.stages[stageName]) {
2066
- job.stages = job.stages || createStageState();
2067
- job.stages[stageName] = { status: 'pending', completed_at: null, metadata: {} };
2068
- }
2069
-
2070
- job.stages[stageName] = {
2071
- status: 'completed',
2072
- completed_at: nowIso(),
2073
- metadata
2074
- };
2075
- }
2076
-
2077
- function isStageCompleted(job, stageName) {
2078
- return Boolean(job && job.stages && job.stages[stageName] && job.stages[stageName].status === 'completed');
2079
- }
2080
-
2081
- function ensureStagePrerequisite(job, stageName, prerequisiteStage) {
2082
- if (!isStageCompleted(job, prerequisiteStage)) {
2083
- throw new Error(`Cannot run studio ${stageName}: stage "${prerequisiteStage}" is not completed`);
2084
- }
2085
- }
2086
-
2087
- function ensureNotRolledBack(job, stageName) {
2088
- if (job.status === 'rolled_back') {
2089
- throw new Error(`Cannot run studio ${stageName}: job ${job.job_id} is rolled back`);
2090
- }
2091
- }
2092
-
2093
- function buildStudioTaskKey(stageName = '') {
2094
- const normalizedStage = normalizeString(stageName) || 'task';
2095
- return `studio:${normalizedStage}`;
2096
- }
2097
-
2098
- async function resolveTaskReference(mode, job, options = {}) {
2099
- const explicitTaskRef = normalizeString(options.taskRef);
2100
- if (explicitTaskRef) {
2101
- return explicitTaskRef;
2102
- }
2103
-
2104
- const sceneId = normalizeString(job?.scene?.id);
2105
- const specId = normalizeString(job?.scene?.spec_id) || normalizeString(job?.source?.spec_id);
2106
- if (!sceneId || !specId) {
2107
- return null;
2108
- }
2109
-
2110
- const stageName = resolveTaskStage(mode, job, options.stageName);
2111
- const taskKey = normalizeString(options.taskKey) || buildStudioTaskKey(stageName);
2112
- const projectPath = normalizeString(options.projectPath) || process.cwd();
2113
- const fileSystem = options.fileSystem || fs;
2114
- const taskRefRegistry = options.taskRefRegistry || new TaskRefRegistry(projectPath, { fileSystem });
2115
-
2116
- try {
2117
- const taskRef = await taskRefRegistry.resolveOrCreateRef({
2118
- sceneId,
2119
- specId,
2120
- taskKey,
2121
- source: 'studio-stage',
2122
- metadata: {
2123
- mode: normalizeString(mode) || null,
2124
- stage: stageName || null,
2125
- job_id: normalizeString(job?.job_id) || null
2126
- }
2127
- });
2128
- return taskRef.task_ref;
2129
- } catch (_error) {
2130
- return null;
2131
- }
1639
+ function resolveTaskReference(mode, job, options = {}, dependencies = {}) {
1640
+ return resolveSharedTaskReference(mode, job, options, {
1641
+ normalizeString,
1642
+ resolveTaskStage,
1643
+ TaskRefRegistry,
1644
+ fileSystem: dependencies.fileSystem || options.fileSystem || fs
1645
+ });
2132
1646
  }
2133
1647
 
2134
1648
  async function buildCommandPayload(mode, job, options = {}) {
2135
- const taskRef = await resolveTaskReference(mode, job, options);
2136
- const base = {
2137
- mode,
2138
- success: true,
2139
- job_id: job.job_id,
2140
- status: job.status,
2141
- progress: buildProgress(job),
2142
- next_action: resolveNextAction(job),
2143
- artifacts: { ...job.artifacts }
2144
- };
2145
- return attachTaskFeedbackModel({
2146
- ...base,
2147
- ...buildTaskEnvelope(mode, job, {
2148
- ...options,
2149
- taskRef
2150
- })
1649
+ return buildSharedCommandPayload(mode, job, options, {
1650
+ normalizeString,
1651
+ resolveTaskStage,
1652
+ resolveNextAction,
1653
+ normalizeTaskCommands,
1654
+ normalizeTaskErrors,
1655
+ normalizeTaskEvidence,
1656
+ normalizeTaskFileChanges,
1657
+ extractCommandsFromStageMetadata,
1658
+ extractErrorsFromStageMetadata,
1659
+ deriveTaskIntentShape,
1660
+ buildTaskSummaryLines,
1661
+ buildTaskAcceptanceCriteria,
1662
+ buildProgress,
1663
+ TaskRefRegistry,
1664
+ fileSystem: options.fileSystem || fs,
1665
+ attachTaskFeedbackModel
2151
1666
  });
2152
1667
  }
2153
1668
 
2154
1669
  function buildJobDomainChainMetadata(job = {}) {
1670
+
2155
1671
  const domainChain = job && job.source && job.source.domain_chain
2156
1672
  ? job.source.domain_chain
2157
1673
  : null;
@@ -2312,6 +1828,19 @@ async function markStudioStageBlockedByProblemEval(paths, job, stageName, evalua
2312
1828
  throw new Error(`studio ${stageName} blocked by problem evaluation: ${reason}`);
2313
1829
  }
2314
1830
 
1831
+ function printStudioPayload(payload, options = {}) {
1832
+ if (options.json) {
1833
+ console.log(JSON.stringify(payload, null, 2));
1834
+ return;
1835
+ }
1836
+
1837
+ console.log(chalk.blue(`Studio job: ${payload.job_id}`));
1838
+ console.log(` Status: ${payload.status}`);
1839
+ console.log(` Progress: ${payload.progress.completed}/${payload.progress.total} (${payload.progress.percent}%)`);
1840
+ console.log(` Next: ${payload.next_action}`);
1841
+ }
1842
+
1843
+
2315
1844
  async function runStudioPlanCommand(options = {}, dependencies = {}) {
2316
1845
  const projectPath = dependencies.projectPath || process.cwd();
2317
1846
  const fileSystem = dependencies.fileSystem || fs;
@@ -2656,6 +2185,36 @@ async function runStudioPlanCommand(options = {}, dependencies = {}) {
2656
2185
  return payload;
2657
2186
  }
2658
2187
 
2188
+ function ensureStageCompleted(job, stageName, metadata = {}) {
2189
+ if (!job.stages || !job.stages[stageName]) {
2190
+ job.stages = job.stages || createStageState();
2191
+ job.stages[stageName] = { status: 'pending', completed_at: null, metadata: {} };
2192
+ }
2193
+
2194
+ job.stages[stageName] = {
2195
+ status: 'completed',
2196
+ completed_at: nowIso(),
2197
+ metadata
2198
+ };
2199
+ }
2200
+
2201
+ function isStageCompleted(job, stageName) {
2202
+ return Boolean(job && job.stages && job.stages[stageName] && job.stages[stageName].status === 'completed');
2203
+ }
2204
+
2205
+ function ensureStagePrerequisite(job, stageName, prerequisiteStage) {
2206
+ if (!isStageCompleted(job, prerequisiteStage)) {
2207
+ throw new Error(`Cannot run studio ${stageName}: stage "${prerequisiteStage}" is not completed`);
2208
+ }
2209
+ }
2210
+
2211
+ function ensureNotRolledBack(job, stageName) {
2212
+ if (job.status === 'rolled_back') {
2213
+ throw new Error(`Cannot run studio ${stageName}: job ${job.job_id} is rolled back`);
2214
+ }
2215
+ }
2216
+
2217
+
2659
2218
  async function runStudioGenerateCommand(options = {}, dependencies = {}) {
2660
2219
  const projectPath = dependencies.projectPath || process.cwd();
2661
2220
  const fileSystem = dependencies.fileSystem || fs;
@@ -3741,9 +3300,15 @@ module.exports = {
3741
3300
  runStudioReleaseCommand,
3742
3301
  runStudioRollbackCommand,
3743
3302
  runStudioEventsCommand,
3744
- buildMagicballStatusLanguage,
3745
3303
  runStudioPortfolioCommand,
3746
3304
  runStudioBackfillSpecScenesCommand,
3747
3305
  runStudioResumeCommand,
3748
3306
  registerStudioCommands
3749
3307
  };
3308
+
3309
+
3310
+
3311
+
3312
+
3313
+
3314
+