iterate-plugin 3.2.1 → 3.3.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.
@@ -73,6 +73,9 @@ import {
73
73
  countSessionImages,
74
74
  scanSessionForTranscript,
75
75
  normalizeTranscript,
76
+ scanSessionForQualityGate,
77
+ scanSessionForExperienceBank,
78
+ scanSessionForDefenseEvents,
76
79
  filterLiveEntries,
77
80
  filterTimelineEntries,
78
81
  serializeObservatoryExport,
@@ -228,6 +231,7 @@ interface ObsManifest {
228
231
  updatedAt?: string
229
232
  active?: boolean
230
233
  mode?: string
234
+ taskMode?: string | null
231
235
  goal?: string
232
236
  phases?: string[]
233
237
  round?: number
@@ -243,6 +247,80 @@ interface ObsManifest {
243
247
  approval?: ObsApproval
244
248
  }
245
249
 
250
+ // ─── Quality command center data types (v3.1+; mirrors src/types.ts) ────────
251
+ // These mirror the JSON the three command-center tools return so the F8/F9/F10
252
+ // tabs can render session-scanned data with full optional chaining.
253
+
254
+ interface ObsQualityGateDimension {
255
+ dimension?: string
256
+ convergenceRate?: number
257
+ findingsCount?: number
258
+ fixedCount?: number
259
+ score?: number
260
+ status?: string
261
+ }
262
+
263
+ interface ObsQualityGate {
264
+ timestamp?: string
265
+ overallStatus?: string
266
+ overallScore?: number
267
+ verificationPassRate?: number
268
+ totalChecks?: number
269
+ passedChecks?: number
270
+ failedChecks?: number
271
+ failReason?: string | null
272
+ totalFindings?: number
273
+ criticalCount?: number
274
+ highCount?: number
275
+ mediumCount?: number
276
+ lowCount?: number
277
+ dimensions?: ObsQualityGateDimension[]
278
+ }
279
+
280
+ interface ObsExperienceEntry {
281
+ id?: string
282
+ timestamp?: string
283
+ dimension?: string
284
+ pattern?: string
285
+ description?: string
286
+ verifiedFix?: string
287
+ findingSummary?: string
288
+ severity?: string
289
+ hitCount?: number
290
+ lastHitAt?: string | null
291
+ files?: string[]
292
+ tags?: string[]
293
+ }
294
+
295
+ interface ObsExperienceResult {
296
+ operation?: string
297
+ count?: number
298
+ totalHits?: number
299
+ added?: boolean
300
+ entries?: ObsExperienceEntry[]
301
+ }
302
+
303
+ interface ObsDefenseEvent {
304
+ id?: string
305
+ timestamp?: string
306
+ round?: number
307
+ type?: string
308
+ description?: string
309
+ defense?: string
310
+ outcome?: string
311
+ file?: string | null
312
+ line?: number | null
313
+ severity?: string
314
+ }
315
+
316
+ interface ObsDefenseResult {
317
+ operation?: string
318
+ count?: number
319
+ language?: string
320
+ counts?: Record<string, number>
321
+ events?: ObsDefenseEvent[]
322
+ }
323
+
246
324
  /** Opaque slot props: heterogeneous per-slot payloads. */
247
325
  type SlotProps = Record<string, unknown>
248
326
 
@@ -1143,6 +1221,33 @@ function TriagePanel(props: SlotProps) {
1143
1221
  })
1144
1222
  }
1145
1223
 
1224
+ // v3.1+ §8: assign the currently scoped findings to the model for fixing.
1225
+ // Scoping mirrors the batch buttons (visible when selectAll is off, all
1226
+ // otherwise); produces one paste-able iterate_fix instruction carrying each
1227
+ // finding's file/line/dimension/severity/summary.
1228
+ const doAssignFindings = () => {
1229
+ const targetAll = selectAll
1230
+ const scopeIndices = targetAll ? allIndices : indices
1231
+ const scopeFindings = (findings as IterateFinding[])
1232
+ .filter((f, i) => scopeIndices.includes(i))
1233
+ .map((f) => ({
1234
+ file: String(f.file || ''),
1235
+ ...(typeof f.line === 'number' && f.line > 0 ? { line: f.line } : {}),
1236
+ dimension: String(f.dimension || ''),
1237
+ severity: String(f.severity || ''),
1238
+ summary: String(f.summary || ''),
1239
+ ...(f.suggested_fix ? { suggested_fix: String(f.suggested_fix) } : {}),
1240
+ }))
1241
+ if (scopeFindings.length === 0) return
1242
+ const cmd = `请调用 \`iterate_fix\` 指派并修复以下 findings:\n\n\`\`\`json\n${JSON.stringify(scopeFindings, null, 2)}\n\`\`\``
1243
+ copyText(cmd).then((ok) => {
1244
+ if (ok) {
1245
+ setCmdCopied('assign')
1246
+ setTimeout(() => setCmdCopied(null), 1600)
1247
+ }
1248
+ })
1249
+ }
1250
+
1146
1251
  const doCopyYaml = () => {
1147
1252
  const yaml = toKnownIntentionalYaml(ignored)
1148
1253
  if (!yaml) return
@@ -1289,6 +1394,12 @@ function TriagePanel(props: SlotProps) {
1289
1394
  onClick: doTriggerNewRound,
1290
1395
  title: '触发新一轮迭代审查',
1291
1396
  }, cmdCopied === 'new-round' ? '已复制' : '触发新一轮'),
1397
+ React.createElement('button', {
1398
+ className: 'iterate-cmd', 'data-primary': '', 'data-copied': cmdCopied === 'assign' ? '' : undefined,
1399
+ onClick: doAssignFindings,
1400
+ disabled: (selectAll ? allIndices : indices).length === 0,
1401
+ title: selectAll ? `指派全部 ${allIndices.length} 个 findings 修复` : `指派当前可见 ${indices.length} 个 findings 修复`,
1402
+ }, cmdCopied === 'assign' ? '已复制' : '指派修复'),
1292
1403
  React.createElement('button', {
1293
1404
  className: 'iterate-cmd', 'data-danger': '', 'data-copied': cmdCopied === 'rollback' ? '' : undefined,
1294
1405
  onClick: doRollbackToCheckpoint,
@@ -1614,6 +1725,24 @@ function latestTranscript(session: unknown): ObsManifest | null {
1614
1725
  return raw ? normalizeTranscript(raw) as ObsManifest : null
1615
1726
  }
1616
1727
 
1728
+ /** Find the latest quality-gate snapshot inside a session snapshot. */
1729
+ function latestQualityGate(session: unknown): ObsQualityGate | null {
1730
+ if (!session) return null
1731
+ return scanSessionForQualityGate(session) as ObsQualityGate | null
1732
+ }
1733
+
1734
+ /** Find the latest experience-bank result inside a session snapshot. */
1735
+ function latestExperienceBank(session: unknown): ObsExperienceResult | null {
1736
+ if (!session) return null
1737
+ return scanSessionForExperienceBank(session) as ObsExperienceResult | null
1738
+ }
1739
+
1740
+ /** Find the latest defense-events result inside a session snapshot. */
1741
+ function latestDefenseEvents(session: unknown): ObsDefenseResult | null {
1742
+ if (!session) return null
1743
+ return scanSessionForDefenseEvents(session) as ObsDefenseResult | null
1744
+ }
1745
+
1617
1746
  /** Build a human-pasteable `iterate_fix` instruction for one finding. */
1618
1747
  function buildObsFixInstruction(f: ObsFinding): string {
1619
1748
  const payload = JSON.stringify({
@@ -1638,6 +1767,10 @@ function buildObsFixInstruction(f: ObsFinding): string {
1638
1767
  function ObservatoryPanel(props: SlotProps) {
1639
1768
  const session = props && props.session ? props.session : null
1640
1769
  const manifest = latestTranscript(session)
1770
+ // v3.1+: command-center data scanned straight out of the session stream.
1771
+ const qualityGate = latestQualityGate(session)
1772
+ const experienceBank = latestExperienceBank(session)
1773
+ const defenseEvents = latestDefenseEvents(session)
1641
1774
 
1642
1775
  const [open, setOpen] = React.useState(false)
1643
1776
  const [tab, setTab] = React.useState('live')
@@ -1660,7 +1793,9 @@ function ObservatoryPanel(props: SlotProps) {
1660
1793
  )
1661
1794
  // v3.0: Experience bank search state
1662
1795
  const [expSearch, setExpSearch] = React.useState('')
1663
- const [expResults, setExpResults] = React.useState<unknown[]>([])
1796
+ // v3.1+: session-scanned experience entries seed the list; expSearch filters
1797
+ // them client-side (matching pattern/description/dimension/tags). The "搜索"
1798
+ // button still copies a back-end iterate_experience search instruction.
1664
1799
  // v3.0: Defense events filter
1665
1800
  const [defenseFilter, setDefenseFilter] = React.useState('')
1666
1801
 
@@ -2156,58 +2291,138 @@ function ObservatoryPanel(props: SlotProps) {
2156
2291
  }
2157
2292
 
2158
2293
  // ── F8: Quality Gate ──────────────────────────────────────────────────────
2159
- // v3.0: Quality command center - shows dimension convergence, verification pass rate, and overall PASS/FAIL
2294
+ // v3.1+ renders the latest iterate_quality_gate snapshot found in the
2295
+ // session (dimension convergence, verification pass rate, overall PASS/FAIL);
2296
+ // falls back to copy-able query instructions when no snapshot exists yet.
2160
2297
  const renderQualityGate = () => {
2161
- // Quality gate data would come from manifest or be computed on-the-fly
2162
- // For now, show a placeholder with instructions
2163
- const qualityGateInstruction = '请调用 `iterate_quality_gate` 查询当前质量门禁状态'
2298
+ const gateInstruction = '请调用 `iterate_quality_gate` 查询当前质量门禁状态'
2299
+ const gate = qualityGate
2300
+ const dims = gate && gate.dimensions ? gate.dimensions : []
2301
+ const hasGate = gate !== null && Boolean(gate.overallStatus || gate.overallScore != null || dims.length > 0)
2302
+ const status = gate && gate.overallStatus ? String(gate.overallStatus) : 'pending'
2303
+ const statusLabel = status === 'pass' ? 'PASS' : status === 'fail' ? 'FAIL' : 'PENDING'
2304
+ const num = (v: number | undefined) => Number(v != null ? v : 0)
2305
+
2306
+ const headerBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
2307
+ React.createElement('b', {}, '质量门禁'),
2308
+ React.createElement('button', {
2309
+ className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'qgate' ? '' : undefined,
2310
+ onClick: () => copyInstruction('qgate', gateInstruction),
2311
+ title: '复制 iterate_quality_gate 查询指令',
2312
+ }, copiedKey === 'qgate' ? '已复制' : '查询门禁'),
2313
+ )
2314
+
2315
+ if (!hasGate) {
2316
+ return React.createElement('div', {},
2317
+ headerBar,
2318
+ React.createElement('div', { className: 'iterate-obs-block' },
2319
+ React.createElement('div', { className: 'iterate-obs-block-head' },
2320
+ React.createElement('span', {}, '质量门禁视图'),
2321
+ React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.0'),
2322
+ ),
2323
+ React.createElement('div', { className: 'iterate-obs-block-body' },
2324
+ React.createElement('div', { className: 'iterate-obs-msg' }, '质量门禁显示各维度收敛度、验证通过率和整体 PASS/FAIL 状态。'),
2325
+ React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
2326
+ React.createElement('button', {
2327
+ className: 'iterate-btn', 'data-copied': copiedKey === 'qgate-instr' ? '' : undefined,
2328
+ onClick: () => copyInstruction('qgate-instr', gateInstruction),
2329
+ title: '复制查询指令',
2330
+ }, copiedKey === 'qgate-instr' ? '已复制' : '复制查询指令'),
2331
+ ),
2332
+ ),
2333
+ ),
2334
+ )
2335
+ }
2336
+
2164
2337
  return React.createElement('div', {},
2165
- React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
2166
- React.createElement('b', {}, '质量门禁'),
2167
- React.createElement('button', {
2168
- className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'qgate' ? '' : undefined,
2169
- onClick: () => copyInstruction('qgate', qualityGateInstruction),
2170
- title: '复制 iterate_quality_gate 查询指令',
2171
- }, copiedKey === 'qgate' ? '已复制' : '查询门禁'),
2172
- ),
2338
+ headerBar,
2173
2339
  React.createElement('div', { className: 'iterate-obs-block' },
2174
2340
  React.createElement('div', { className: 'iterate-obs-block-head' },
2175
- React.createElement('span', {}, '质量门禁视图'),
2176
- React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.0'),
2341
+ React.createElement('span', { className: 'iterate-gate', 'data-status': status }, statusLabel),
2342
+ React.createElement('span', { className: 'iterate-obs-head-meta' },
2343
+ `整体评分 ${num(gate && gate.overallScore)} · 验证 ${num(gate && gate.passedChecks)}/${num(gate && gate.totalChecks)} · 通过率 ${num(gate && gate.verificationPassRate)}%`,
2344
+ ),
2177
2345
  ),
2178
2346
  React.createElement('div', { className: 'iterate-obs-block-body' },
2179
- React.createElement('div', { className: 'iterate-obs-msg' }, '质量门禁显示各维度收敛度、验证通过率和整体 PASS/FAIL 状态。'),
2180
- React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
2181
- React.createElement('button', {
2182
- className: 'iterate-btn', 'data-copied': copiedKey === 'qgate-instr' ? '' : undefined,
2183
- onClick: () => copyInstruction('qgate-instr', qualityGateInstruction),
2184
- title: '复制查询指令',
2185
- }, copiedKey === 'qgate-instr' ? '已复制' : '复制查询指令'),
2347
+ dims.length > 0
2348
+ ? React.createElement('div', { className: 'iterate-obs-bar', style: { flexWrap: 'wrap', gap: 6 } },
2349
+ ...dims.map((d, i) =>
2350
+ React.createElement('span', { key: `gdim-${i}`, className: 'iterate-obs-chip' },
2351
+ `${String(d.dimension || '?')} · 收敛 ${num(d.convergenceRate)}% · 评分 ${num(d.score)}`,
2352
+ ),
2353
+ ),
2354
+ )
2355
+ : null,
2356
+ React.createElement('div', { className: 'iterate-obs-bar', style: { flexWrap: 'wrap' } },
2357
+ ...dims.map((d, i) => {
2358
+ const fill = Math.min(100, Math.max(0, num(d.score)))
2359
+ return React.createElement('div', { key: `gbar-${i}`, className: 'iterate-dim-score' },
2360
+ React.createElement('span', {}, String(d.dimension || '?')),
2361
+ React.createElement('div', { className: 'iterate-dim-score-bar' },
2362
+ React.createElement('div', {
2363
+ className: 'iterate-dim-score-fill',
2364
+ 'data-status': String(d.status || 'warn'),
2365
+ style: { width: `${fill}%` },
2366
+ }),
2367
+ ),
2368
+ React.createElement('span', {}, `评分 ${num(d.score)} · 发现 ${num(d.findingsCount)} · 已修复 ${num(d.fixedCount)}`),
2369
+ )
2370
+ }),
2186
2371
  ),
2372
+ gate && gate.failReason
2373
+ ? React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 6 } }, `失败原因:${String(gate.failReason)}`)
2374
+ : null,
2187
2375
  ),
2188
2376
  ),
2189
2377
  )
2190
2378
  }
2191
2379
 
2192
2380
  // ── F9: Experience Bank ────────────────────────────────────────────────────
2193
- // v3.0: Browse/search historical fixes and patterns
2381
+ // v3.1+ renders experience entries scanned from the session plus a
2382
+ // client-side keyword filter; the copy buttons still reach the back end
2383
+ // (list all / search / adopt one entry).
2194
2384
  const renderExperienceBank = () => {
2195
2385
  const expInstruction = expSearch
2196
2386
  ? `请调用 \`iterate_experience\` 搜索经验:\n\n\`\`\`json\n${JSON.stringify({ operation: 'search', query: expSearch }, null, 2)}\n\`\`\``
2197
2387
  : '请调用 `iterate_experience` 列出所有经验'
2198
2388
 
2199
- const resultRows = expResults.map((entry: unknown, i: number) => {
2200
- const e = entry as { id?: string; pattern?: string; description?: string; hitCount?: number; dimension?: string }
2201
- return React.createElement('div', { key: `exp-${i}`, className: 'iterate-obs-row' },
2202
- React.createElement('span', { className: 'iterate-obs-chip' }, String(e.dimension || '')),
2203
- React.createElement('span', { className: 'iterate-obs-msg' }, String(e.pattern || '')),
2204
- React.createElement('span', { className: 'iterate-exp-hit' }, `命中 ${e.hitCount ?? 0} 次`),
2389
+ const expEntries = experienceBank && experienceBank.entries ? experienceBank.entries : []
2390
+ const query = expSearch.trim().toLowerCase()
2391
+ const visible = query
2392
+ ? expEntries.filter((e) =>
2393
+ [e.pattern, e.description, e.id, e.dimension, e.severity, ...(e.tags || [])]
2394
+ .filter(Boolean)
2395
+ .some((t) => String(t).toLowerCase().includes(query)),
2396
+ )
2397
+ : expEntries
2398
+
2399
+ const resultRows = visible.map((e, i) => {
2400
+ const adoptInstruction = `请调用 \`iterate_experience\` 采纳经验并应用已验证修法:\n\n\`\`\`json\n${JSON.stringify({ operation: 'get', id: e.id }, null, 2)}\n\`\`\``
2401
+ return React.createElement('div', { key: `exp-${i}`, className: 'iterate-obs-block' },
2402
+ React.createElement('div', { className: 'iterate-obs-block-head' },
2403
+ React.createElement('span', { className: 'iterate-obs-chip' }, String(e.dimension || '')),
2404
+ React.createElement('span', {}, String(e.pattern || '?')),
2405
+ React.createElement('span', { className: 'iterate-exp-hit' }, `命中 ${e.hitCount ?? 0} 次`),
2406
+ ),
2407
+ React.createElement('div', { className: 'iterate-obs-block-body' },
2408
+ React.createElement('div', { className: 'iterate-obs-msg' }, String(e.description || '')),
2409
+ React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 6 } },
2410
+ e.verifiedFix
2411
+ ? React.createElement('button', {
2412
+ className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === `exp-adopt-${i}` ? '' : undefined,
2413
+ onClick: () => copyInstruction(`exp-adopt-${i}`, adoptInstruction),
2414
+ title: '复制采纳指令(查询该经验并应用到当前上下文)',
2415
+ }, copiedKey === `exp-adopt-${i}` ? '已复制' : '采纳')
2416
+ : null,
2417
+ ),
2418
+ ),
2205
2419
  )
2206
2420
  })
2207
2421
 
2208
2422
  return React.createElement('div', {},
2209
2423
  React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
2210
2424
  React.createElement('b', {}, '经验银行'),
2425
+ React.createElement('span', { className: 'iterate-obs-head-meta' }, `会话内 ${expEntries.length} 条`),
2211
2426
  React.createElement('button', {
2212
2427
  className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'exp-list' ? '' : undefined,
2213
2428
  onClick: () => copyInstruction('exp-list', '请调用 `iterate_experience` 列出所有经验'),
@@ -2232,12 +2447,16 @@ function ObservatoryPanel(props: SlotProps) {
2232
2447
  ),
2233
2448
  resultRows.length > 0
2234
2449
  ? React.createElement('div', {}, ...resultRows)
2235
- : React.createElement('div', { className: 'iterate-obs-empty' }, '输入关键词搜索历史经验,或点击「列出经验」查看全部'),
2450
+ : expEntries.length > 0
2451
+ ? React.createElement('div', { className: 'iterate-obs-empty' }, '没有匹配「' + expSearch + '」的经验')
2452
+ : React.createElement('div', { className: 'iterate-obs-empty' }, '本次会话暂无经验记录,可点击「列出经验」查询历史经验'),
2236
2453
  )
2237
2454
  }
2238
2455
 
2239
2456
  // ── F10: Defense Events ────────────────────────────────────────────────────
2240
- // v3.0: Shows precondition failures, rollbacks, invariant violations, assumption falsifications
2457
+ // v3.1+ renders defense events + type counts scanned from the session
2458
+ // (precondition failures, rollbacks, invariant violations, assumption
2459
+ // falsifications); copy buttons reach the back end for the full history.
2241
2460
  const renderDefenseEvents = () => {
2242
2461
  const defenseInstruction = defenseFilter
2243
2462
  ? `请调用 \`iterate_defense_events\` 查询防御事件:\n\n\`\`\`json\n${JSON.stringify({ operation: 'list', type: defenseFilter || undefined }, null, 2)}\n\`\`\``
@@ -2250,54 +2469,111 @@ function ObservatoryPanel(props: SlotProps) {
2250
2469
  { value: 'invariant_violated', label: '不变量违反' },
2251
2470
  { value: 'assumption_falsified', label: '假设被证伪' },
2252
2471
  ]
2472
+ const TYPE_LABEL: Record<string, string> = {
2473
+ precondition_failed: '前置校验失败',
2474
+ rollback: '回滚',
2475
+ invariant_violated: '不变量违反',
2476
+ assumption_falsified: '假设被证伪',
2477
+ }
2253
2478
 
2254
- return React.createElement('div', {},
2255
- React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
2256
- React.createElement('b', {}, '防御事件'),
2257
- React.createElement('button', {
2258
- className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'defense-list' ? '' : undefined,
2259
- onClick: () => copyInstruction('defense-list', '请调用 `iterate_defense_events` 列出所有防御事件'),
2260
- title: '复制列出防御事件指令',
2261
- }, copiedKey === 'defense-list' ? '已复制' : '列出事件'),
2262
- React.createElement('button', {
2263
- className: 'iterate-btn', 'data-copied': copiedKey === 'defense-counts' ? '' : undefined,
2264
- onClick: () => copyInstruction('defense-counts', '请调用 `iterate_defense_events` 查询事件统计'),
2265
- title: '复制统计指令',
2266
- }, copiedKey === 'defense-counts' ? '已复制' : '统计'),
2267
- ),
2268
- React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
2269
- React.createElement('select', {
2270
- className: 'iterate-filter-select',
2271
- value: defenseFilter,
2272
- 'aria-label': '按事件类型筛选',
2273
- onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setDefenseFilter(e.target.value),
2274
- },
2275
- ...typeOptions.map((opt) => React.createElement('option', { key: opt.value, value: opt.value }, opt.label)),
2479
+ const counts = (defenseEvents && defenseEvents.counts) || {}
2480
+ const allEvents = defenseEvents && defenseEvents.events ? defenseEvents.events : []
2481
+ const totalCount = typeOptions.slice(1).reduce((acc, o) => acc + (counts[o.value] || 0), 0)
2482
+ const visibleEvents = defenseFilter
2483
+ ? allEvents.filter((e) => e.type === defenseFilter)
2484
+ : allEvents
2485
+
2486
+ const headerBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
2487
+ React.createElement('b', {}, '防御事件'),
2488
+ React.createElement('span', { className: 'iterate-obs-head-meta' }, `会话内 ${allEvents.length} / 累计 ${totalCount}`),
2489
+ React.createElement('button', {
2490
+ className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'defense-list' ? '' : undefined,
2491
+ onClick: () => copyInstruction('defense-list', '请调用 `iterate_defense_events` 列出所有防御事件'),
2492
+ title: '复制列出防御事件指令',
2493
+ }, copiedKey === 'defense-list' ? '已复制' : '列出事件'),
2494
+ React.createElement('button', {
2495
+ className: 'iterate-btn', 'data-copied': copiedKey === 'defense-counts' ? '' : undefined,
2496
+ onClick: () => copyInstruction('defense-counts', '请调用 `iterate_defense_events` 查询事件统计'),
2497
+ title: '复制统计指令',
2498
+ }, copiedKey === 'defense-counts' ? '已复制' : '统计'),
2499
+ )
2500
+
2501
+ if (allEvents.length === 0 && totalCount === 0) {
2502
+ return React.createElement('div', {},
2503
+ headerBar,
2504
+ React.createElement('div', { className: 'iterate-obs-block' },
2505
+ React.createElement('div', { className: 'iterate-obs-block-head' },
2506
+ React.createElement('span', {}, '防御事件流'),
2507
+ React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.0'),
2508
+ ),
2509
+ React.createElement('div', { className: 'iterate-obs-block-body' },
2510
+ React.createElement('div', { className: 'iterate-obs-msg' }, '防御事件包括:前置校验失败、回滚、不变量违反、假设被证伪。'),
2511
+ React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
2512
+ React.createElement('button', {
2513
+ className: 'iterate-btn', 'data-copied': copiedKey === 'defense-instr' ? '' : undefined,
2514
+ onClick: () => copyInstruction('defense-instr', defenseInstruction),
2515
+ title: '复制查询指令',
2516
+ }, copiedKey === 'defense-instr' ? '已复制' : '复制查询指令'),
2517
+ ),
2518
+ ),
2276
2519
  ),
2277
- defenseFilter
2278
- ? React.createElement('button', {
2279
- className: 'iterate-filter-clear',
2280
- onClick: () => setDefenseFilter(''),
2281
- title: '清除类型筛选',
2282
- }, '清除')
2283
- : null,
2520
+ )
2521
+ }
2522
+
2523
+ const filterBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
2524
+ React.createElement('select', {
2525
+ className: 'iterate-filter-select',
2526
+ value: defenseFilter,
2527
+ 'aria-label': '按事件类型筛选',
2528
+ onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setDefenseFilter(e.target.value),
2529
+ },
2530
+ ...typeOptions.map((opt) => React.createElement('option', { key: opt.value, value: opt.value }, opt.label)),
2284
2531
  ),
2285
- React.createElement('div', { className: 'iterate-obs-block' },
2286
- React.createElement('div', { className: 'iterate-obs-block-head' },
2287
- React.createElement('span', {}, '防御事件流'),
2288
- React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.0'),
2289
- ),
2290
- React.createElement('div', { className: 'iterate-obs-block-body' },
2291
- React.createElement('div', { className: 'iterate-obs-msg' }, '防御事件包括:前置校验失败、回滚、不变量违反、假设被证伪。'),
2292
- React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
2293
- React.createElement('button', {
2294
- className: 'iterate-btn', 'data-copied': copiedKey === 'defense-instr' ? '' : undefined,
2295
- onClick: () => copyInstruction('defense-instr', defenseInstruction),
2296
- title: '复制查询指令',
2297
- }, copiedKey === 'defense-instr' ? '已复制' : '复制查询指令'),
2532
+ defenseFilter
2533
+ ? React.createElement('button', {
2534
+ className: 'iterate-filter-clear',
2535
+ onClick: () => setDefenseFilter(''),
2536
+ title: '清除类型筛选',
2537
+ }, '清除')
2538
+ : null,
2539
+ )
2540
+
2541
+ let eventRows: React.ReactNode[] = []
2542
+ if (visibleEvents.length > 0) {
2543
+ eventRows = visibleEvents.map((e, i) => {
2544
+ const type = String(e.type || 'unknown')
2545
+ const loc = `${String(e.file || '?')}${typeof e.line === 'number' && e.line > 0 ? `:${e.line}` : ''}`
2546
+ return React.createElement('div', { key: `def-${i}`, className: 'iterate-obs-block' },
2547
+ React.createElement('div', { className: 'iterate-obs-block-head' },
2548
+ React.createElement('span', { className: 'iterate-defense-badge', 'data-type': type }, TYPE_LABEL[type] || type),
2549
+ typeof e.round === 'number'
2550
+ ? React.createElement('span', { className: 'iterate-obs-head-meta' }, `Round ${e.round}`)
2551
+ : null,
2552
+ React.createElement('span', { className: 'iterate-obs-head-meta' }, loc),
2553
+ ),
2554
+ React.createElement('div', { className: 'iterate-obs-block-body' },
2555
+ React.createElement('div', { className: 'iterate-obs-msg' }, String(e.description || '')),
2556
+ e.defense
2557
+ ? React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 4 } }, `防线:${String(e.defense)}`)
2558
+ : null,
2559
+ ),
2560
+ )
2561
+ })
2562
+ }
2563
+
2564
+ return React.createElement('div', {},
2565
+ headerBar,
2566
+ filterBar,
2567
+ React.createElement('div', { className: 'iterate-obs-bar', style: { flexWrap: 'wrap', gap: 6, marginBottom: 8 } },
2568
+ ...typeOptions.slice(1).map((opt) =>
2569
+ React.createElement('span', { key: `cnt-${opt.value}`, className: 'iterate-defense-badge', 'data-type': opt.value },
2570
+ `${opt.label} ${counts[opt.value] || 0}`,
2298
2571
  ),
2299
2572
  ),
2300
2573
  ),
2574
+ eventRows.length > 0
2575
+ ? React.createElement('div', {}, ...eventRows)
2576
+ : React.createElement('div', { className: 'iterate-obs-empty' }, '没有匹配该类型的防御事件'),
2301
2577
  )
2302
2578
  }
2303
2579
 
@@ -13,14 +13,14 @@ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync
13
13
  import { defineTool } from '@deepseek-ai/dsh-tools'
14
14
  import type { JsonValue } from '@deepseek-ai/dsh-util-values'
15
15
  import { resolveProjectRootForExec } from '../config-loader.ts'
16
- import { checkpointPath, iterateDir } from '../paths.ts'
16
+ import { checkpointPath, iterateDir, transcriptPath } from '../paths.ts'
17
17
  import { readRegistry } from './fix.ts'
18
18
  import { readDecisionEntries } from './decision-log.ts'
19
19
  import type { IterationCheckpoint, IterationStatus } from '../types.ts'
20
20
 
21
21
  // ─── Pure helpers (exported for unit tests) ─────────────────────────────────
22
22
 
23
- /** Read a checkpoint from disk (missing/corrupt → null). */
23
+ /** Read the current checkpoint from disk (missing/corrupt → null). */
24
24
  export function readCheckpoint(projectRoot: string): IterationCheckpoint | null {
25
25
  const file = checkpointPath(projectRoot)
26
26
  if (!existsSync(file)) return null
@@ -35,6 +35,19 @@ export function readCheckpoint(projectRoot: string): IterationCheckpoint | null
35
35
  }
36
36
  }
37
37
 
38
+ /** Read the harness task_mode from the persisted observatory transcript (code|iterate|null). */
39
+ export function readTranscriptTaskMode(projectRoot: string): 'code' | 'iterate' | null {
40
+ const file = transcriptPath(projectRoot)
41
+ if (!existsSync(file)) return null
42
+ try {
43
+ const parsed = JSON.parse(readFileSync(file, 'utf-8')) as { taskMode?: unknown }
44
+ const m = parsed && typeof parsed === 'object' ? parsed.taskMode : null
45
+ return m === 'code' || m === 'iterate' ? m : null
46
+ } catch {
47
+ return null
48
+ }
49
+ }
50
+
38
51
  /** Validate a checkpoint payload (returns error string or null). */
39
52
  export function validateCheckpoint(input: {
40
53
  mode: unknown
@@ -74,10 +87,12 @@ export function validateCheckpoint(input: {
74
87
  */
75
88
  export function computeStatus(input: {
76
89
  checkpoint: IterationCheckpoint | null
90
+ taskMode?: 'code' | 'iterate' | null
77
91
  decisionEntries: { timestamp: string; type: string; round?: number; data?: Record<string, unknown> }[]
78
92
  fixRegistry: { rounds: { round: number; fixedCount: number; failedCount: number }[] }
79
93
  }): IterationStatus {
80
94
  const checkpoint = input.checkpoint
95
+ const taskMode = input.taskMode ?? null
81
96
  const entries = input.decisionEntries
82
97
  const registry = input.fixRegistry
83
98
 
@@ -103,6 +118,7 @@ export function computeStatus(input: {
103
118
 
104
119
  return {
105
120
  mode: checkpoint?.mode ?? null,
121
+ taskMode,
106
122
  currentRound,
107
123
  totalRounds,
108
124
  fixedCount,
@@ -253,6 +269,7 @@ export function registerStatusTool(ctx: { tools: { register: (def: ReturnType<ty
253
269
  properties: {
254
270
  ok: { type: 'boolean', required: true },
255
271
  mode: { oneOf: [{ type: 'string' }, { type: 'null' }] },
272
+ taskMode: { oneOf: [{ type: 'string' }, { type: 'null' }], description: 'Harness execution mode from the observatory transcript ("code" | "iterate").' },
256
273
  currentRound: { type: 'integer' },
257
274
  totalRounds: { type: 'integer' },
258
275
  fixedCount: { type: 'integer' },
@@ -269,7 +286,7 @@ export function registerStatusTool(ctx: { tools: { register: (def: ReturnType<ty
269
286
  render: (_args, value) => {
270
287
  if (!value.ok) return [{ type: 'text', text: `status failed: ${value.error}` }]
271
288
  const lines = [
272
- `Mode: ${value.mode ?? 'none'}`,
289
+ `Mode: ${value.mode ?? 'none'}${value.taskMode ? ` (${value.taskMode})` : ''}`,
273
290
  `Round: ${value.currentRound} / ${value.totalRounds}`,
274
291
  `Fixed: ${value.fixedCount} · Architectural remaining: ${value.architecturalCount}`,
275
292
  `Findings in checkpoint: ${value.findingsCount}`,
@@ -287,12 +304,14 @@ export function registerStatusTool(ctx: { tools: { register: (def: ReturnType<ty
287
304
  const projectRoot = resolved.root
288
305
  const status = computeStatus({
289
306
  checkpoint: readCheckpoint(projectRoot),
307
+ taskMode: readTranscriptTaskMode(projectRoot),
290
308
  decisionEntries: readDecisionEntries(projectRoot),
291
309
  fixRegistry: readRegistry(projectRoot),
292
310
  })
293
311
  return {
294
312
  ok: true,
295
313
  mode: status.mode ?? null,
314
+ taskMode: status.taskMode ?? null,
296
315
  currentRound: status.currentRound,
297
316
  totalRounds: status.totalRounds,
298
317
  fixedCount: status.fixedCount,
@@ -241,7 +241,10 @@ export function registerDefenseEventsTool(ctx: { tools: { register: (def: Return
241
241
  ...(typeof args.file === 'string' && args.file.length > 0 ? { file: args.file } : {}),
242
242
  ...(typeof args.line === 'number' ? { line: args.line } : {}),
243
243
  })
244
- writeDefenseEvents(projectRoot, next)
244
+ const write = writeDefenseEvents(projectRoot, next)
245
+ if (!write.ok) {
246
+ return { ok: false, kind: 'defense_events', operation: 'record', error: write.error }
247
+ }
245
248
  const event = next.events[next.events.length - 1]
246
249
  return {
247
250
  ok: true,