scene-capability-engine 3.6.26 → 3.6.28

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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.6.28] - 2026-03-06
11
+
12
+ ### Added
13
+ - Added shared `mb_status` language across studio task feedback and capability inventory scene advice for Magicball UI rendering.
14
+ - Magicball docs now define the shared status-language contract for task cards and capability cards.
15
+
16
+ ## [3.6.27] - 2026-03-06
17
+
18
+ ### Added
19
+ - Studio task payloads now expose `task.feedback_model` for human-facing task cards.
20
+ - Timeline list/show payloads now expose `view_model` for Magicball timeline panels.
21
+ - Added `docs/magicball-task-feedback-timeline-guide.md`.
22
+
10
23
  ## [3.6.26] - 2026-03-06
11
24
 
12
25
  ### Added
package/README.md CHANGED
@@ -218,5 +218,5 @@ MIT. See [LICENSE](LICENSE).
218
218
 
219
219
  ---
220
220
 
221
- **Version**: 3.6.26
221
+ **Version**: 3.6.28
222
222
  **Last Updated**: 2026-03-05
package/README.zh.md CHANGED
@@ -218,5 +218,5 @@ MIT,见 [LICENSE](LICENSE)。
218
218
 
219
219
  ---
220
220
 
221
- **版本**:3.6.26
221
+ **版本**:3.6.28
222
222
  **最后更新**:2026-03-05
@@ -627,6 +627,7 @@ Studio JSON output now includes a stable UI-oriented task stream contract (in ad
627
627
  - `task.commands[]`: `cmd`, `exit_code`, `stdout`, `stderr`, `log_path`
628
628
  - `task.errors[]`: `message`, `error_bundle` (copy-ready diagnostic bundle)
629
629
  - `task.evidence[]`: structured evidence references
630
+ - `task.feedback_model`: human-facing task feedback (`problem`, `execution`, `diagnosis`, `evidence`, `next_step`, `mb_status`)
630
631
  - `event[]`: raw audit event stream (`studio events` also keeps legacy `events[]` for compatibility)
631
632
  - `studio events --openhands-events <path>` switches `source_stream=openhands` and maps OpenHands raw events to the same task contract fields.
632
633
  - hierarchical task reference lookup/rerun:
@@ -227,3 +227,9 @@ sce capability register --input <template.json> --json
227
227
 
228
228
  - `summary_recommendations[]`:顶部全局建议
229
229
  - `quick_filters[]`:推荐快捷筛选
230
+
231
+ ## 15. Magicball 统一状态语言
232
+
233
+ - Scene 卡片统一消费 `mb_status`
234
+ - 任务卡统一消费 `task.feedback_model.mb_status`
235
+ - 颜色建议:`danger / warning / info / success`
@@ -179,3 +179,14 @@ sce capability use --template <template-id> --spec <spec-id> --apply --json
179
179
  ]
180
180
  }
181
181
  ```
182
+
183
+ ### mb_status(统一状态语言)
184
+ ```json
185
+ {
186
+ "attention_level": "critical",
187
+ "status_tone": "danger",
188
+ "status_label": "blocked",
189
+ "blocking_summary": "缺决策策略,暂不可发布",
190
+ "recommended_action": "补齐决策策略"
191
+ }
192
+ ```
@@ -0,0 +1,134 @@
1
+ # Magicball Task Feedback + Timeline Integration Guide
2
+
3
+ > 目标:让 Magicball 在不直接消费底层事件流/快照原始结构的情况下,使用 SCE 的稳定任务反馈模型与时间线视图模型。
4
+
5
+ ## 1. 任务反馈模型(来源:`sce studio events --json`)
6
+
7
+ SCE 现在在 `task` 下增加:
8
+ - `feedback_model.version`
9
+ - `feedback_model.problem`
10
+ - `feedback_model.execution`
11
+ - `feedback_model.diagnosis`
12
+ - `feedback_model.evidence`
13
+ - `feedback_model.next_step`
14
+
15
+ ### 1.1 字段说明
16
+
17
+ - `problem.component`:当前问题所属组件/scene
18
+ - `problem.action`:当前动作或阶段
19
+ - `problem.expected`:期望结果
20
+ - `problem.actual`:实际结果
21
+
22
+ - `execution.stage`:当前阶段(plan/generate/apply/verify/release/rollback/events)
23
+ - `execution.status`:当前状态
24
+ - `execution.summary[]`:3 行以内摘要
25
+ - `execution.blocking_summary`:阻断摘要
26
+
27
+ - `diagnosis.hypothesis`:当前根因假设/错误摘要
28
+ - `diagnosis.chain_checkpoint`:定位链路检查点
29
+ - `diagnosis.root_cause_confidence`:根因判断置信度(low/medium/high)
30
+
31
+ - `evidence.file_count` / `evidence.file_paths[]`
32
+ - `evidence.command_count` / `evidence.error_count`
33
+ - `evidence.verification_result`
34
+ - `evidence.regression_scope[]`
35
+
36
+ - `next_step.recommended_action`:面向人的下一步建议
37
+ - `next_step.next_action`:动作 key
38
+ - `next_step.next_command`:推荐命令
39
+
40
+ ### 1.2 Magicball UI 建议
41
+
42
+ 任务卡默认显示:
43
+ 1. `problem.expected / actual`
44
+ 2. `execution.summary`
45
+ 3. `execution.blocking_summary`
46
+ 4. `next_step.recommended_action`
47
+ 5. `evidence.file_paths`(最多前 3-5 个)
48
+
49
+ 事件流 `event[]` 保留为“展开查看”,不再作为主视图。
50
+
51
+ ## 2. 时间线视图模型(来源:`sce timeline list/show --json`)
52
+
53
+ SCE 现在在时间线命令中增加:
54
+ - `view_model.summary`
55
+ - `view_model.entries[]`(list)
56
+ - `view_model.snapshot`(show)
57
+ - `view_model.files_preview[]`(show)
58
+
59
+ ### 2.1 timeline list
60
+
61
+ 关键字段:
62
+ - `view_model.summary.total`
63
+ - `view_model.summary.latest_snapshot_id`
64
+ - `view_model.summary.latest_created_at`
65
+ - `view_model.summary.dirty_snapshot_count`
66
+ - `view_model.summary.scene_count`
67
+ - `view_model.summary.trigger_counts`
68
+ - `view_model.entries[]`
69
+
70
+ 每个 entry:
71
+ - `snapshot_id`
72
+ - `title`
73
+ - `subtitle`
74
+ - `created_at`
75
+ - `scene_id`
76
+ - `file_count`
77
+ - `attention_level`
78
+ - `show_command`
79
+ - `restore_command`
80
+
81
+ ### 2.2 timeline show
82
+
83
+ 关键字段:
84
+ - `view_model.snapshot`
85
+ - `view_model.files_preview[]`
86
+ - `view_model.file_total`
87
+ - `view_model.restore_command`
88
+
89
+ ### 2.3 Magicball UI 建议
90
+
91
+ 时间线面板建议两层:
92
+ - 列表层:使用 `view_model.entries[]` 渲染时间线卡片
93
+ - 详情层:使用 `view_model.snapshot + files_preview` 渲染快照详情
94
+
95
+ 默认展示:
96
+ - 快照标题
97
+ - 创建时间
98
+ - 关联 scene
99
+ - 文件数
100
+ - attention level
101
+ - “查看详情 / 恢复” 按钮
102
+
103
+ ## 3. 推荐对接顺序
104
+
105
+ 1. 任务面板先接 `task.feedback_model`
106
+ 2. 再把 `event[]` 放到“高级模式”
107
+ 3. 时间线首页接 `timeline list.view_model`
108
+ 4. 时间线详情页接 `timeline show.view_model`
109
+
110
+ ## 4. 最小接口清单
111
+
112
+ - `sce studio events --job <job-id> --json`
113
+ - `sce studio events --job <job-id> --openhands-events <path> --json`
114
+ - `sce timeline list --limit 20 --json`
115
+ - `sce timeline show <snapshot-id> --json`
116
+
117
+ ## 5. 设计原则
118
+
119
+ - 主视图优先展示“人可判断信息”,不是原始事件流
120
+ - 事件流保留为审计层,不作为第一视图
121
+ - 时间线优先展示“可恢复、可比较、可追踪”的节点信息
122
+ - 所有字段都以 SCE 输出为准,Magicball 不自行推断
123
+
124
+
125
+ ## 6. Magicball 统一状态语言
126
+
127
+ SCE 现在会在任务反馈模型中提供 `mb_status`:
128
+ - `attention_level`
129
+ - `status_tone`
130
+ - `status_label`
131
+ - `blocking_summary`
132
+ - `recommended_action`
133
+
134
+ Magicball 可直接用这组字段控制颜色、图标、提示文案。
@@ -836,6 +836,17 @@ function resolveCapabilityTriadPriority(entry) {
836
836
  return 3;
837
837
  }
838
838
 
839
+ function buildCapabilityMagicballStatus(input = {}) {
840
+ const attention = normalizeText(input.attention_level) || 'medium';
841
+ return {
842
+ attention_level: attention,
843
+ status_tone: attention === 'critical' ? 'danger' : (attention === 'high' ? 'warning' : (attention === 'low' ? 'success' : 'info')),
844
+ status_label: normalizeText(input.status_label) || null,
845
+ blocking_summary: normalizeText(input.blocking_summary) || null,
846
+ recommended_action: normalizeText(input.recommended_action) || null
847
+ };
848
+ }
849
+
839
850
  function buildCapabilityInventorySceneAdvice(entry) {
840
851
  const sceneId = String(entry && entry.scene_id || 'scene.unknown');
841
852
  const releaseUi = entry && entry.release_readiness_ui ? entry.release_readiness_ui : { publish_ready: true, blocking_missing: [] };
@@ -886,7 +897,13 @@ function buildCapabilityInventorySceneAdvice(entry) {
886
897
  recommended_action: recommendedAction,
887
898
  blocking_summary: blockingSummary,
888
899
  next_action: nextAction,
889
- next_command: 'sce capability extract --scene ' + sceneId + ' --json'
900
+ next_command: 'sce capability extract --scene ' + sceneId + ' --json',
901
+ mb_status: buildCapabilityMagicballStatus({
902
+ attention_level: attentionLevel,
903
+ status_label: releaseUi.publish_ready ? 'publish_ready' : 'blocked',
904
+ blocking_summary: blockingSummary,
905
+ recommended_action: recommendedAction
906
+ })
890
907
  };
891
908
  }
892
909
 
@@ -1854,6 +1871,7 @@ module.exports = {
1854
1871
  filterCapabilityCatalogEntries,
1855
1872
  filterCapabilityInventoryEntries,
1856
1873
  sortCapabilityInventoryEntries,
1874
+ buildCapabilityMagicballStatus,
1857
1875
  buildCapabilityInventorySceneAdvice,
1858
1876
  buildCapabilityInventorySummaryStats,
1859
1877
  buildCapabilityInventorySummaryRecommendations,
@@ -1460,6 +1460,109 @@ function buildTaskAcceptanceCriteria(stageName = '', job = {}, nextAction = '')
1460
1460
  ];
1461
1461
  }
1462
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
+ }
1463
1566
  function buildTaskEnvelope(mode, job, options = {}) {
1464
1567
  const stageName = resolveTaskStage(mode, job, options.stageName);
1465
1568
  const stageState = stageName && job && job.stages && job.stages[stageName]
@@ -2039,13 +2142,13 @@ async function buildCommandPayload(mode, job, options = {}) {
2039
2142
  next_action: resolveNextAction(job),
2040
2143
  artifacts: { ...job.artifacts }
2041
2144
  };
2042
- return {
2145
+ return attachTaskFeedbackModel({
2043
2146
  ...base,
2044
2147
  ...buildTaskEnvelope(mode, job, {
2045
2148
  ...options,
2046
2149
  taskRef
2047
2150
  })
2048
- };
2151
+ });
2049
2152
  }
2050
2153
 
2051
2154
  function buildJobDomainChainMetadata(job = {}) {
@@ -3308,7 +3411,9 @@ async function runStudioEventsCommand(options = {}, dependencies = {}) {
3308
3411
  : null;
3309
3412
  }
3310
3413
  payload.events = events;
3311
- printStudioEventsPayload(payload, options);
3414
+ const normalizedPayload = attachTaskFeedbackModel(payload);
3415
+ printStudioEventsPayload(normalizedPayload, options);
3416
+ return normalizedPayload;
3312
3417
  return payload;
3313
3418
  }
3314
3419
 
@@ -3636,6 +3741,7 @@ module.exports = {
3636
3741
  runStudioReleaseCommand,
3637
3742
  runStudioRollbackCommand,
3638
3743
  runStudioEventsCommand,
3744
+ buildMagicballStatusLanguage,
3639
3745
  runStudioPortfolioCommand,
3640
3746
  runStudioBackfillSpecScenesCommand,
3641
3747
  runStudioResumeCommand,
@@ -44,6 +44,88 @@ function createStore(dependencies = {}) {
44
44
  return dependencies.timelineStore || new ProjectTimelineStore(projectPath, fileSystem);
45
45
  }
46
46
 
47
+ function summarizeTimelineAttention(entry = {}) {
48
+ const trigger = normalizeText(entry.trigger).toLowerCase();
49
+ const git = entry && typeof entry.git === 'object' ? entry.git : {};
50
+ if (trigger === 'restore') {
51
+ return 'high';
52
+ }
53
+ if (trigger === 'push') {
54
+ return 'medium';
55
+ }
56
+ if (Number(git.dirty_count || 0) > 0) {
57
+ return 'medium';
58
+ }
59
+ return 'low';
60
+ }
61
+
62
+ function buildTimelineEntryViewModel(entry = {}) {
63
+ const title = normalizeText(entry.summary) || ((normalizeText(entry.trigger) || 'timeline') + ' checkpoint');
64
+ const subtitleParts = [
65
+ normalizeText(entry.event),
66
+ entry.scene_id ? ('scene=' + entry.scene_id) : '',
67
+ Number.isFinite(Number(entry.file_count)) ? ('files=' + Number(entry.file_count)) : ''
68
+ ].filter(Boolean);
69
+ return {
70
+ snapshot_id: normalizeText(entry.snapshot_id) || null,
71
+ title,
72
+ subtitle: subtitleParts.join(' | '),
73
+ trigger: normalizeText(entry.trigger) || null,
74
+ event: normalizeText(entry.event) || null,
75
+ created_at: normalizeText(entry.created_at) || null,
76
+ scene_id: normalizeText(entry.scene_id) || null,
77
+ session_id: normalizeText(entry.session_id) || null,
78
+ file_count: Number.isFinite(Number(entry.file_count)) ? Number(entry.file_count) : 0,
79
+ branch: entry && entry.git ? normalizeText(entry.git.branch) || null : null,
80
+ head: entry && entry.git ? normalizeText(entry.git.head) || null : null,
81
+ dirty_count: entry && entry.git && Number.isFinite(Number(entry.git.dirty_count)) ? Number(entry.git.dirty_count) : 0,
82
+ attention_level: summarizeTimelineAttention(entry),
83
+ show_command: normalizeText(entry.snapshot_id) ? ('sce timeline show ' + entry.snapshot_id + ' --json') : null,
84
+ restore_command: normalizeText(entry.snapshot_id) ? ('sce timeline restore ' + entry.snapshot_id + ' --json') : null
85
+ };
86
+ }
87
+
88
+ function buildTimelineListViewModel(payload = {}) {
89
+ const snapshots = Array.isArray(payload.snapshots) ? payload.snapshots : [];
90
+ const trigger_counts = {};
91
+ let dirty_snapshot_count = 0;
92
+ const sceneIds = new Set();
93
+ for (const item of snapshots) {
94
+ const trigger = normalizeText(item.trigger) || 'unknown';
95
+ trigger_counts[trigger] = Number(trigger_counts[trigger] || 0) + 1;
96
+ if (item.scene_id) {
97
+ sceneIds.add(item.scene_id);
98
+ }
99
+ if (item.git && item.git.dirty) {
100
+ dirty_snapshot_count += 1;
101
+ }
102
+ }
103
+ return {
104
+ summary: {
105
+ total: Number(payload.total || snapshots.length || 0),
106
+ latest_snapshot_id: snapshots[0] ? normalizeText(snapshots[0].snapshot_id) || null : null,
107
+ latest_created_at: snapshots[0] ? normalizeText(snapshots[0].created_at) || null : null,
108
+ dirty_snapshot_count,
109
+ scene_count: sceneIds.size,
110
+ trigger_counts
111
+ },
112
+ entries: snapshots.map((item) => buildTimelineEntryViewModel(item))
113
+ };
114
+ }
115
+
116
+ function buildTimelineShowViewModel(payload = {}) {
117
+ const snapshot = payload.snapshot && typeof payload.snapshot === 'object' ? payload.snapshot : {};
118
+ const filesPayload = payload.files && typeof payload.files === 'object' ? payload.files : {};
119
+ const files = Array.isArray(filesPayload.files) ? filesPayload.files : [];
120
+ return {
121
+ snapshot: buildTimelineEntryViewModel(snapshot),
122
+ files_preview: files.slice(0, 20),
123
+ file_preview_count: Math.min(files.length, 20),
124
+ file_total: Number(filesPayload.file_count || files.length || 0),
125
+ restore_command: snapshot.snapshot_id ? ('sce timeline restore ' + snapshot.snapshot_id + ' --json') : null
126
+ };
127
+ }
128
+
47
129
  function printPayload(payload, asJson = false, title = 'Timeline') {
48
130
  if (asJson) {
49
131
  console.log(JSON.stringify(payload, null, 2));
@@ -112,6 +194,7 @@ async function runTimelineListCommand(options = {}, dependencies = {}) {
112
194
  limit: normalizePositiveInteger(options.limit, 20, 2000),
113
195
  trigger: normalizeText(options.trigger)
114
196
  });
197
+ payload.view_model = buildTimelineListViewModel(payload);
115
198
  printPayload(payload, options.json, 'Timeline List');
116
199
  return payload;
117
200
  }
@@ -119,6 +202,7 @@ async function runTimelineListCommand(options = {}, dependencies = {}) {
119
202
  async function runTimelineShowCommand(snapshotId, options = {}, dependencies = {}) {
120
203
  const store = createStore(dependencies);
121
204
  const payload = await store.getSnapshot(snapshotId);
205
+ payload.view_model = buildTimelineShowViewModel(payload);
122
206
  printPayload(payload, options.json, 'Timeline Show');
123
207
  return payload;
124
208
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scene-capability-engine",
3
- "version": "3.6.26",
3
+ "version": "3.6.28",
4
4
  "description": "SCE (Scene Capability Engine) - A CLI tool and npm package for spec-driven development with AI coding assistants.",
5
5
  "main": "index.js",
6
6
  "bin": {