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.
- package/README.md +123 -90
- package/README.zh-CN.md +129 -80
- package/dist/approval-gate.js +0 -17
- package/dist/tools/checkpoint.js +22 -3
- package/dist/tools/defense-events.js +4 -1
- package/dist/tools/defense-store.js +9 -3
- package/dist/tools/experience-bank.js +4 -1
- package/dist/tools/experience-store.js +9 -3
- package/dist/tools/quality-gate.js +4 -1
- package/dist/tools/quality-store.js +9 -3
- package/dist/tools/transcript.js +8 -1
- package/dist/transcript.js +10 -0
- package/lib/client.js +549 -88
- package/lib/parse.js +352 -0
- package/package.json +2 -2
- package/src/approval-gate.ts +0 -26
- package/src/client/index.ts +349 -73
- package/src/tools/checkpoint.ts +22 -3
- package/src/tools/defense-events.ts +4 -1
- package/src/tools/defense-store.ts +13 -4
- package/src/tools/experience-bank.ts +4 -1
- package/src/tools/experience-store.ts +13 -4
- package/src/tools/quality-gate.ts +4 -1
- package/src/tools/quality-store.ts +13 -4
- package/src/tools/transcript.ts +8 -1
- package/src/transcript.ts +11 -0
package/src/client/index.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
2162
|
-
|
|
2163
|
-
const
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
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.
|
|
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
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
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
|
-
:
|
|
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.
|
|
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
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
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
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
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
|
-
|
|
2286
|
-
React.createElement('
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
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
|
|
package/src/tools/checkpoint.ts
CHANGED
|
@@ -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
|
|
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,
|