iterate-plugin 3.6.0 → 3.8.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 +70 -7
- package/README.zh-CN.md +50 -6
- package/dist/config-loader.js +7 -1
- package/dist/file-lock.js +110 -21
- package/dist/git-scope.js +22 -2
- package/dist/index.js +7 -0
- package/dist/live.js +2 -1
- package/dist/session-hooks.js +6 -1
- package/dist/skill-prompt.js +2 -2
- package/dist/tools/checkpoint.js +24 -17
- package/dist/tools/config.js +42 -33
- package/dist/tools/decision-log.js +13 -99
- package/dist/tools/transcript.js +74 -44
- package/dist/tools/triage.js +52 -45
- package/dist/transcript.js +2 -2
- package/dist/working-directory.js +114 -0
- package/lib/client.js +1128 -468
- package/lib/parse.js +280 -62
- package/package.json +34 -11
- package/src/client/i18n.ts +517 -0
- package/src/client/index.ts +553 -344
- package/src/client/session-cache.ts +54 -0
- package/src/config-loader.ts +7 -1
- package/src/file-lock.ts +115 -18
- package/src/git-scope.ts +21 -2
- package/src/index.ts +7 -0
- package/src/live.ts +2 -1
- package/src/session-hooks.ts +6 -1
- package/src/skill-prompt.ts +2 -2
- package/src/tools/checkpoint.ts +23 -16
- package/src/tools/config.ts +40 -31
- package/src/tools/decision-log.ts +12 -82
- package/src/tools/transcript.ts +72 -43
- package/src/tools/triage.ts +47 -40
- package/src/transcript.ts +2 -2
- package/src/types.ts +1 -1
- package/src/working-directory.ts +126 -0
package/lib/parse.js
CHANGED
|
@@ -207,9 +207,15 @@ export function countSessionImages(session) {
|
|
|
207
207
|
|
|
208
208
|
/**
|
|
209
209
|
* Check whether `obj` is a valid ReviewReport-like object.
|
|
210
|
-
* The minimum requirement: an object with `convergence` (
|
|
210
|
+
* The minimum requirement: an object with `convergence` (a RECORD),
|
|
211
211
|
* `findings` (array), and `rounds` (array).
|
|
212
212
|
*
|
|
213
|
+
* The convergence guard is also the TranscriptManifest discriminator: a
|
|
214
|
+
* manifest carries `convergence` / `rounds` / `findings` too, but its
|
|
215
|
+
* convergence is the per-round count SERIES (an array). Since
|
|
216
|
+
* `typeof [] === 'object'` in JS, only an explicit array rejection keeps the
|
|
217
|
+
* two shapes apart — see `isTranscriptManifest`.
|
|
218
|
+
*
|
|
213
219
|
* @param {unknown} obj
|
|
214
220
|
* @returns {obj is Record<string, unknown>}
|
|
215
221
|
*/
|
|
@@ -217,12 +223,8 @@ export function isReviewReport(obj) {
|
|
|
217
223
|
if (!obj || typeof obj !== 'object') return false
|
|
218
224
|
const o = /** @type {Record<string, unknown>} */ (obj)
|
|
219
225
|
const convergence = safeGet(o, 'convergence')
|
|
220
|
-
return
|
|
221
|
-
|
|
222
|
-
convergence !== null &&
|
|
223
|
-
Array.isArray(safeGet(o, 'findings')) &&
|
|
224
|
-
Array.isArray(safeGet(o, 'rounds'))
|
|
225
|
-
)
|
|
226
|
+
if (!convergence || typeof convergence !== 'object' || Array.isArray(convergence)) return false
|
|
227
|
+
return Array.isArray(safeGet(o, 'findings')) && Array.isArray(safeGet(o, 'rounds'))
|
|
226
228
|
}
|
|
227
229
|
|
|
228
230
|
/**
|
|
@@ -585,7 +587,13 @@ export function scanSessionForTranscript(session) {
|
|
|
585
587
|
}
|
|
586
588
|
}
|
|
587
589
|
|
|
588
|
-
|
|
590
|
+
// Fallback: a host snapshot without the tool-call envelope (a restored
|
|
591
|
+
// transcript, a rehydrated turn, a shell that surfaces artifacts directly)
|
|
592
|
+
// may still carry a manifest somewhere in the tree. Deep-scan for it so the
|
|
593
|
+
// dashboard and the observatory keep filling instead of degrading to the
|
|
594
|
+
// first-run empty state. The manifest is returned RAW — callers normalize.
|
|
595
|
+
const loose = findFirstInObject(session, isTranscriptManifest)
|
|
596
|
+
return loose ? /** @type {Record<string, unknown>} */ (loose) : null
|
|
589
597
|
}
|
|
590
598
|
|
|
591
599
|
/**
|
|
@@ -975,10 +983,16 @@ export function normalizeQualityGateSnapshot(raw) {
|
|
|
975
983
|
*/
|
|
976
984
|
export function scanSessionForQualityGate(session) {
|
|
977
985
|
const node = latestToolResultNode(session, 'iterate_quality_gate', findQualityGateInObject)
|
|
978
|
-
if (node
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
986
|
+
if (node !== null) {
|
|
987
|
+
const found = findQualityGateInObject(node)
|
|
988
|
+
if (found) return normalizeQualityGateSnapshot(found)
|
|
989
|
+
}
|
|
990
|
+
// Fallback: a host snapshot without the tool-call envelope (a restored
|
|
991
|
+
// transcript, a rehydrated turn, a shell that surfaces artifacts directly)
|
|
992
|
+
// still carries the snapshot somewhere in the tree — deep-scan for it so
|
|
993
|
+
// the panel fills instead of claiming "not pulled yet".
|
|
994
|
+
const loose = findFirstInObject(session, isQualityGateSnapshot)
|
|
995
|
+
return loose ? normalizeQualityGateSnapshot(loose) : null
|
|
982
996
|
}
|
|
983
997
|
|
|
984
998
|
/**
|
|
@@ -1034,10 +1048,13 @@ export function normalizeExperienceBankResult(raw) {
|
|
|
1034
1048
|
*/
|
|
1035
1049
|
export function scanSessionForExperienceBank(session) {
|
|
1036
1050
|
const node = latestToolResultNode(session, 'iterate_experience', findExperienceResultInObject)
|
|
1037
|
-
if (node
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1051
|
+
if (node !== null) {
|
|
1052
|
+
const found = findExperienceResultInObject(node)
|
|
1053
|
+
if (found) return normalizeExperienceBankResult(found)
|
|
1054
|
+
}
|
|
1055
|
+
// Same deep-scan fallback as the quality-gate scanner (see above).
|
|
1056
|
+
const loose = findFirstInObject(session, isExperienceBankResult)
|
|
1057
|
+
return loose ? normalizeExperienceBankResult(loose) : null
|
|
1041
1058
|
}
|
|
1042
1059
|
|
|
1043
1060
|
/**
|
|
@@ -1098,10 +1115,13 @@ export function normalizeDefenseEventsResult(raw) {
|
|
|
1098
1115
|
*/
|
|
1099
1116
|
export function scanSessionForDefenseEvents(session) {
|
|
1100
1117
|
const node = latestToolResultNode(session, 'iterate_defense_events', findDefenseResultInObject)
|
|
1101
|
-
if (node
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1118
|
+
if (node !== null) {
|
|
1119
|
+
const found = findDefenseResultInObject(node)
|
|
1120
|
+
if (found) return normalizeDefenseEventsResult(found)
|
|
1121
|
+
}
|
|
1122
|
+
// Same deep-scan fallback as the quality-gate scanner (see above).
|
|
1123
|
+
const loose = findFirstInObject(session, isDefenseEventsResult)
|
|
1124
|
+
return loose ? normalizeDefenseEventsResult(loose) : null
|
|
1105
1125
|
}
|
|
1106
1126
|
|
|
1107
1127
|
// ─── Run-summary / meta-review verdict detection ─────────────────────────────
|
|
@@ -1963,10 +1983,16 @@ export function toolCalledInSession(session, toolName) {
|
|
|
1963
1983
|
*
|
|
1964
1984
|
* @param {boolean} pulled whether the backing tool was called this session
|
|
1965
1985
|
* @param {string} topic what is missing (e.g. "质量门禁证书")
|
|
1986
|
+
* @param {'zh'|'en'} [lang] UI language (the caller localizes `topic` itself)
|
|
1966
1987
|
* @returns {string}
|
|
1967
1988
|
*/
|
|
1968
|
-
export function diskEmptyStateText(pulled, topic) {
|
|
1969
|
-
const subject = topic || '数据'
|
|
1989
|
+
export function diskEmptyStateText(pulled, topic, lang = 'zh') {
|
|
1990
|
+
const subject = topic || (lang === 'en' ? 'data' : '数据')
|
|
1991
|
+
if (lang === 'en') {
|
|
1992
|
+
return pulled
|
|
1993
|
+
? `Pulled from disk: ${subject} has no records yet (the disk is empty).`
|
|
1994
|
+
: `Not pulled from disk yet: ${subject} lives under the project's .iterate/ — copy the "Pull disk snapshot" instruction back into the session and it will fill in.`
|
|
1995
|
+
}
|
|
1970
1996
|
return pulled
|
|
1971
1997
|
? `已从磁盘拉取:${subject}暂无记录(磁盘无数据)。`
|
|
1972
1998
|
: `尚未从磁盘拉取${subject}:数据存放在项目 .iterate/ 下,点「拉取磁盘快照」复制指令发回会话即可回填。`
|
|
@@ -2223,27 +2249,49 @@ export function trendMax(points) {
|
|
|
2223
2249
|
//
|
|
2224
2250
|
// Every value the iterate loop can report as `stoppedReason` — including the
|
|
2225
2251
|
// schema-retry batch (schema_invalid / no_usable_reviewer_output / inconclusive)
|
|
2226
|
-
// — maps to a concrete
|
|
2227
|
-
// default instead of a bare "已结束". Kept in parse.js so both the
|
|
2228
|
-
// observatory badge and Node unit tests share one source of truth.
|
|
2252
|
+
// — maps to a concrete label in zh and en. Unknown values still get a readable
|
|
2253
|
+
// default instead of a bare "已结束" / "Done". Kept in parse.js so both the
|
|
2254
|
+
// client observatory badge and Node unit tests share one source of truth.
|
|
2229
2255
|
|
|
2230
2256
|
/**
|
|
2231
|
-
*
|
|
2257
|
+
* Badge label for a run's `stoppedReason`, or null/undefined-safe.
|
|
2258
|
+
*
|
|
2259
|
+
* @param {unknown} reason
|
|
2260
|
+
* @param {'zh'|'en'} [lang] UI language (default zh, the model-facing default)
|
|
2232
2261
|
*
|
|
2233
2262
|
* @param {string | null | undefined} reason
|
|
2234
2263
|
* @returns {string}
|
|
2235
2264
|
*/
|
|
2236
|
-
export function stoppedReasonLabel(reason) {
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2265
|
+
export function stoppedReasonLabel(reason, lang = 'zh') {
|
|
2266
|
+
const zh = {
|
|
2267
|
+
converged: '已结束 · 已收敛(无新发现)',
|
|
2268
|
+
max_rounds_reached: '已结束 · 达到轮数上限',
|
|
2269
|
+
aborted_by_validation: '已结束 · 验证失败后回滚停止',
|
|
2270
|
+
aborted_by_config: '已结束 · 验证命令不在白名单(配置需修复)',
|
|
2271
|
+
inconclusive: '已结束 · 轮次结论不明(审查输出无效)',
|
|
2272
|
+
schema_invalid: '已结束 · 审查输出连续 schema 校验失败',
|
|
2273
|
+
no_usable_reviewer_output: '已结束 · 无可用审查输出',
|
|
2274
|
+
}
|
|
2275
|
+
const en = {
|
|
2276
|
+
converged: 'Done \u00b7 converged (no new findings)',
|
|
2277
|
+
max_rounds_reached: 'Done \u00b7 reached the round limit',
|
|
2278
|
+
aborted_by_validation: 'Done \u00b7 rolled back after a validation failure',
|
|
2279
|
+
aborted_by_config: 'Done \u00b7 validation command not allow-listed (config needs fixing)',
|
|
2280
|
+
inconclusive: 'Done \u00b7 inconclusive rounds (invalid review output)',
|
|
2281
|
+
schema_invalid: 'Done \u00b7 repeated schema validation failures in review output',
|
|
2282
|
+
no_usable_reviewer_output: 'Done \u00b7 no usable review output',
|
|
2283
|
+
}
|
|
2284
|
+
// Object.hasOwn, NOT `zh[reason]`: a model/hand-edited `stoppedReason` of
|
|
2285
|
+
// `__proto__` / `constructor` / `toString` would otherwise resolve through
|
|
2286
|
+
// the PROTOTYPE CHAIN to Object.prototype or a function (never a string) and
|
|
2287
|
+
// crash React ("Objects are not valid as a React child") when the badge
|
|
2288
|
+
// renders it. An own-key hit is the only way to return a table value; any
|
|
2289
|
+
// other value degrades to a plain text label.
|
|
2290
|
+
const table = lang === 'en' ? en : zh
|
|
2291
|
+
if (typeof reason === 'string' && Object.hasOwn(table, reason)) return table[reason]
|
|
2292
|
+
const text = typeof reason === 'string' && reason !== '' ? reason : ''
|
|
2293
|
+
if (lang === 'en') return text ? `Done \u00b7 ${text}` : 'Done'
|
|
2294
|
+
return text ? `已结束 · ${text}` : '已结束'
|
|
2247
2295
|
}
|
|
2248
2296
|
|
|
2249
2297
|
// ─── Completion notification ────────────────────────────────────────────────
|
|
@@ -2252,48 +2300,93 @@ export function stoppedReasonLabel(reason) {
|
|
|
2252
2300
|
* One-line completion summary for notifications ("已收敛 / 已达最大轮数").
|
|
2253
2301
|
*
|
|
2254
2302
|
* @param {Record<string, unknown>} report
|
|
2303
|
+
* @param {'zh'|'en'} [lang] UI language
|
|
2255
2304
|
* @returns {string}
|
|
2256
2305
|
*/
|
|
2257
|
-
export function buildCompletionSummary(report) {
|
|
2306
|
+
export function buildCompletionSummary(report, lang = 'zh') {
|
|
2258
2307
|
const conv = /** @type {Record<string, unknown>} */ (report.convergence ?? {})
|
|
2259
2308
|
const rounds = getCurrentRound(report)
|
|
2260
2309
|
const total = getTotalRounds(report)
|
|
2261
2310
|
const stats = severityStats(report)
|
|
2262
2311
|
const converged = conv.converged === true
|
|
2263
|
-
const reason = converged ? '已收敛' : `已达最大轮数 ${total}`
|
|
2264
2312
|
const totalFindings = stats.critical + stats.high + stats.medium + stats.low
|
|
2313
|
+
if (lang === 'en') {
|
|
2314
|
+
const reason = converged ? 'converged' : `reached the round limit ${total}`
|
|
2315
|
+
return `iterate review complete \u00b7 ${rounds}/${total} rounds \u00b7 ${totalFindings} findings \u00b7 ${reason}`
|
|
2316
|
+
}
|
|
2317
|
+
const reason = converged ? '已收敛' : `已达最大轮数 ${total}`
|
|
2265
2318
|
return `iterate 评审完成 · ${rounds}/${total} 轮 · ${totalFindings} 项发现 · ${reason}`
|
|
2266
2319
|
}
|
|
2267
2320
|
|
|
2268
2321
|
// ─── Config edit guidance ───────────────────────────────────────────────────
|
|
2269
2322
|
|
|
2270
2323
|
/**
|
|
2271
|
-
* Editable config fields
|
|
2272
|
-
*
|
|
2324
|
+
* Editable config fields, used by the settings picker (rendered in the UI) and
|
|
2325
|
+
* by the copy-paste guide / instruction builders (model-facing, always zh).
|
|
2326
|
+
* `label`/`hint` are `{ zh, en }` pairs — the UI localizes them with
|
|
2327
|
+
* `field.label[getLang()]`, while the guide builders below read `.zh`.
|
|
2328
|
+
* @type {Array<{ key: string, label: { zh: string, en: string }, hint: { zh: string, en: string } }>}
|
|
2273
2329
|
*/
|
|
2274
2330
|
export const CONFIG_EDIT_FIELDS = [
|
|
2275
|
-
{
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2331
|
+
{
|
|
2332
|
+
key: 'goal',
|
|
2333
|
+
label: { zh: '目标', en: 'Goal' },
|
|
2334
|
+
hint: { zh: '一句话描述本次迭代目标(字符串)', en: "One-sentence description of this iteration's goal (a string)" },
|
|
2335
|
+
},
|
|
2336
|
+
{
|
|
2337
|
+
key: 'dimensions',
|
|
2338
|
+
label: { zh: '审查维度', en: 'Review dimensions' },
|
|
2339
|
+
hint: { zh: '数组,如 ["correctness","security"]', en: 'Array, e.g. ["correctness","security"]' },
|
|
2340
|
+
},
|
|
2341
|
+
{
|
|
2342
|
+
key: 'max_rounds',
|
|
2343
|
+
label: { zh: '最大轮数', en: 'Max rounds' },
|
|
2344
|
+
hint: { zh: '正整数', en: 'Positive integer' },
|
|
2345
|
+
},
|
|
2346
|
+
{
|
|
2347
|
+
key: 'review.scope',
|
|
2348
|
+
label: { zh: '审查范围', en: 'Review scope' },
|
|
2349
|
+
hint: { zh: '"full" 或 "changed-only"', en: '"full" or "changed-only"' },
|
|
2350
|
+
},
|
|
2351
|
+
{
|
|
2352
|
+
key: 'reasoning_effort',
|
|
2353
|
+
label: { zh: '审查推理强度', en: 'Review reasoning effort' },
|
|
2354
|
+
hint: { zh: '"low" / "medium" / "high",缺省跟随模型默认', en: '"low" / "medium" / "high"; omitted = follow the model default' },
|
|
2355
|
+
},
|
|
2280
2356
|
{
|
|
2281
2357
|
key: 'validation.commands',
|
|
2282
|
-
label: '验证命令',
|
|
2283
|
-
hint:
|
|
2358
|
+
label: { zh: '验证命令', en: 'Validation commands' },
|
|
2359
|
+
hint: {
|
|
2360
|
+
zh: '数组,每项是一条白名单命令字符串,如 ["npm test","npm run lint"];每轮验证逐条执行。留空数组 = 不验证(本轮迭代不受任何测试保护)',
|
|
2361
|
+
en: 'Array of allow-listed command strings, e.g. ["npm test","npm run lint"]; run one by one every round. An empty array = no verification (this iteration runs with no test protection)',
|
|
2362
|
+
},
|
|
2284
2363
|
},
|
|
2285
2364
|
{
|
|
2286
2365
|
key: 'language',
|
|
2287
|
-
label: '界面语言',
|
|
2288
|
-
hint:
|
|
2366
|
+
label: { zh: '界面语言', en: 'Interface language' },
|
|
2367
|
+
hint: {
|
|
2368
|
+
zh: '"zh" 或 "en"(默认 "en"):控制审批理由、防御事件标签等面向人文案的语言',
|
|
2369
|
+
en: '"zh" or "en" (default "en"): language of human-facing copy such as approval rationales and defense-event labels',
|
|
2370
|
+
},
|
|
2289
2371
|
},
|
|
2290
2372
|
{
|
|
2291
2373
|
key: 'personalization.known_intentional',
|
|
2292
|
-
label: '已知有意问题',
|
|
2293
|
-
hint:
|
|
2374
|
+
label: { zh: '已知有意问题', en: 'Known intentional issues' },
|
|
2375
|
+
hint: {
|
|
2376
|
+
zh: '数组,每项 {file, line?, dimension, reason};命中条目在评审中被跳过。可用分诊面板的「回读已写入条目」核对',
|
|
2377
|
+
en: 'Array of {file, line?, dimension, reason}; findings hitting an entry are skipped in review. Verify via the triage panel\'s "read back written entries"',
|
|
2378
|
+
},
|
|
2379
|
+
},
|
|
2380
|
+
{
|
|
2381
|
+
key: 'atomic.max_lines',
|
|
2382
|
+
label: { zh: '原子修复上限行数', en: 'Atomic fix line cap' },
|
|
2383
|
+
hint: { zh: '正整数', en: 'Positive integer' },
|
|
2384
|
+
},
|
|
2385
|
+
{
|
|
2386
|
+
key: 'git.push_per_round',
|
|
2387
|
+
label: { zh: '每轮推送', en: 'Push per round' },
|
|
2388
|
+
hint: { zh: 'true / false', en: 'true / false' },
|
|
2294
2389
|
},
|
|
2295
|
-
{ key: 'atomic.max_lines', label: '原子修复上限行数', hint: '正整数' },
|
|
2296
|
-
{ key: 'git.push_per_round', label: '每轮推送', hint: 'true / false' },
|
|
2297
2390
|
]
|
|
2298
2391
|
|
|
2299
2392
|
/**
|
|
@@ -2307,12 +2400,13 @@ export function buildConfigEditGuide() {
|
|
|
2307
2400
|
'---------------------',
|
|
2308
2401
|
'配置文件:项目根目录 iterate.config.yaml。',
|
|
2309
2402
|
'',
|
|
2310
|
-
'⚠ 未配置 validation.commands
|
|
2311
|
-
'
|
|
2403
|
+
'⚠ 未配置 validation.commands 时迭代不被保护:该轮会直接中止(fail-closed),',
|
|
2404
|
+
' 空验证结果/验证未执行绝不当作“验证通过”,运行以 converged_unverified /',
|
|
2405
|
+
' aborted_by_config 收尾而不是 plain converged。第一次启动迭代前,',
|
|
2312
2406
|
' 请先给 validation.commands 配上至少一条真实命令(如 npm test)。',
|
|
2313
2407
|
'',
|
|
2314
2408
|
'可编辑字段:',
|
|
2315
|
-
...CONFIG_EDIT_FIELDS.map((f) => `- ${f.key}(${f.label}):${f.hint}`),
|
|
2409
|
+
...CONFIG_EDIT_FIELDS.map((f) => `- ${f.key}(${f.label.zh}):${f.hint.zh}`),
|
|
2316
2410
|
'',
|
|
2317
2411
|
'让模型帮你改:',
|
|
2318
2412
|
'1. 调用 iterate_config({ operation: "read" }) 查看当前配置;',
|
|
@@ -2344,7 +2438,7 @@ export const CONFIG_VALUE_PLACEHOLDER = '<由模型填写:该字段的新值>'
|
|
|
2344
2438
|
* Look up an editable config field by its dotted key (gap #8).
|
|
2345
2439
|
*
|
|
2346
2440
|
* @param {string} key
|
|
2347
|
-
* @returns {{ key: string, label: string, hint: string } | null}
|
|
2441
|
+
* @returns {{ key: string, label: { zh: string, en: string }, hint: { zh: string, en: string } } | null}
|
|
2348
2442
|
*/
|
|
2349
2443
|
export function configFieldByKey(key) {
|
|
2350
2444
|
if (typeof key !== 'string' || !key) return null
|
|
@@ -2363,8 +2457,9 @@ export function configFieldByKey(key) {
|
|
|
2363
2457
|
*/
|
|
2364
2458
|
export function buildConfigFieldInstruction(key, value) {
|
|
2365
2459
|
const field = configFieldByKey(key)
|
|
2366
|
-
|
|
2367
|
-
const
|
|
2460
|
+
// Model-facing instruction text: always zh, like every other builder here.
|
|
2461
|
+
const label = field ? field.label.zh : key
|
|
2462
|
+
const hint = field ? field.hint.zh : '见 iterate.config.yaml'
|
|
2368
2463
|
const updates = { [key]: value === undefined ? CONFIG_VALUE_PLACEHOLDER : value }
|
|
2369
2464
|
// Delegate the payload to `buildConfigEditInstruction` so the multi-field
|
|
2370
2465
|
// builder stays a live code path (it used to be test-only dead code).
|
|
@@ -2379,6 +2474,116 @@ export function buildConfigFieldInstruction(key, value) {
|
|
|
2379
2474
|
].join('\n')
|
|
2380
2475
|
}
|
|
2381
2476
|
|
|
2477
|
+
// ─── Config read-back (write → read-back loop) ──────────────────────────────
|
|
2478
|
+
//
|
|
2479
|
+
// The triage panel got a read-back instruction (#7); the config editor only
|
|
2480
|
+
// generated writes, so there was no way to SEE what the file actually says
|
|
2481
|
+
// after pasting an instruction. These three pieces close that loop: an
|
|
2482
|
+
// instruction the settings card copies, a session scanner that picks the
|
|
2483
|
+
// echoed result back up, and a dotted-path resolver that surfaces the value
|
|
2484
|
+
// of the field the user currently has selected.
|
|
2485
|
+
|
|
2486
|
+
/**
|
|
2487
|
+
* Copy-paste instruction that makes the model call `iterate_config` read and
|
|
2488
|
+
* echo the full result (including `config`) into the session, where the
|
|
2489
|
+
* settings card can display it.
|
|
2490
|
+
*
|
|
2491
|
+
* @returns {string}
|
|
2492
|
+
*/
|
|
2493
|
+
export function buildConfigReadInstruction() {
|
|
2494
|
+
return (
|
|
2495
|
+
'请调用 `iterate_config` 回读当前生效配置,并把完整结果(含 config 字段)原样回显,' +
|
|
2496
|
+
'供我核对界面上展示的当前值:\n\n' +
|
|
2497
|
+
'```json\n' +
|
|
2498
|
+
`${JSON.stringify({ operation: 'read' }, null, 2)}\n` +
|
|
2499
|
+
'```'
|
|
2500
|
+
)
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
/**
|
|
2504
|
+
* Top-level keys of an iterate config, derived from the editable field list
|
|
2505
|
+
* so a newly editable section is covered by the predicate automatically.
|
|
2506
|
+
* @type {string[]}
|
|
2507
|
+
*/
|
|
2508
|
+
const CONFIG_TOP_LEVEL_KEYS = [...new Set(CONFIG_EDIT_FIELDS.map((f) => f.key.split('.')[0]))]
|
|
2509
|
+
|
|
2510
|
+
/**
|
|
2511
|
+
* Is `v` an effective iterate config? Requires several known top-level keys:
|
|
2512
|
+
* `loadEffectiveConfig` always merges the built-in defaults, so a real config
|
|
2513
|
+
* is complete — while a stray `{ goal: ... }` record elsewhere in the
|
|
2514
|
+
* transcript cannot impersonate one.
|
|
2515
|
+
*
|
|
2516
|
+
* @param {unknown} v
|
|
2517
|
+
* @returns {boolean}
|
|
2518
|
+
*/
|
|
2519
|
+
export function isEffectiveConfig(v) {
|
|
2520
|
+
if (!v || typeof v !== 'object' || Array.isArray(v)) return false
|
|
2521
|
+
const rec = /** @type {Record<string, unknown>} */ (v)
|
|
2522
|
+
let hits = 0
|
|
2523
|
+
for (const key of CONFIG_TOP_LEVEL_KEYS) {
|
|
2524
|
+
if (Object.hasOwn(rec, key)) hits += 1
|
|
2525
|
+
if (hits >= 3) return true
|
|
2526
|
+
}
|
|
2527
|
+
return false
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
/**
|
|
2531
|
+
* Locate the config inside one `iterate_config` tool result. Reads and writes
|
|
2532
|
+
* both report it under `config`; a section read (`data`) is deliberately not
|
|
2533
|
+
* accepted because it is only a slice.
|
|
2534
|
+
*
|
|
2535
|
+
* @param {unknown} v
|
|
2536
|
+
* @returns {Record<string, unknown> | null}
|
|
2537
|
+
*/
|
|
2538
|
+
export function findConfigInResult(v) {
|
|
2539
|
+
if (!v || typeof v !== 'object' || Array.isArray(v)) return null
|
|
2540
|
+
const rec = /** @type {Record<string, unknown>} */ (v)
|
|
2541
|
+
const cfg = safeGet(rec, 'config')
|
|
2542
|
+
if (isEffectiveConfig(cfg)) return /** @type {Record<string, unknown>} */ (cfg)
|
|
2543
|
+
return isEffectiveConfig(v) ? rec : null
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
/**
|
|
2547
|
+
* Latest effective config echoed into the session: the newest full
|
|
2548
|
+
* `iterate_config` result (read or post-write), falling back to a deep scan
|
|
2549
|
+
* for a config that reached the stream without the tool envelope (restored
|
|
2550
|
+
* transcript, host that surfaces artifacts directly).
|
|
2551
|
+
*
|
|
2552
|
+
* @param {unknown} session
|
|
2553
|
+
* @returns {Record<string, unknown> | null}
|
|
2554
|
+
*/
|
|
2555
|
+
export function scanSessionForConfigRead(session) {
|
|
2556
|
+
const node = latestToolResultNode(session, 'iterate_config', findConfigInResult)
|
|
2557
|
+
if (node !== null) {
|
|
2558
|
+
const cfg = findConfigInResult(node)
|
|
2559
|
+
if (cfg) return cfg
|
|
2560
|
+
}
|
|
2561
|
+
const loose = findFirstInObject(session, isEffectiveConfig)
|
|
2562
|
+
return loose ? /** @type {Record<string, unknown>} */ (loose) : null
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
/**
|
|
2566
|
+
* Resolve a dotted editable-field key (`validation.commands`, `review.scope`)
|
|
2567
|
+
* against a read-back config.
|
|
2568
|
+
*
|
|
2569
|
+
* @param {Record<string, unknown> | null | undefined} config
|
|
2570
|
+
* @param {string} key dotted field key from CONFIG_EDIT_FIELDS
|
|
2571
|
+
* @returns {{ resolved: boolean, value: unknown }} `value` is undefined when unresolved
|
|
2572
|
+
*/
|
|
2573
|
+
export function configValueByPath(config, key) {
|
|
2574
|
+
if (!config || typeof config !== 'object' || typeof key !== 'string' || !key) {
|
|
2575
|
+
return { resolved: false, value: undefined }
|
|
2576
|
+
}
|
|
2577
|
+
let node = /** @type {unknown} */ (config)
|
|
2578
|
+
for (const part of key.split('.')) {
|
|
2579
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return { resolved: false, value: undefined }
|
|
2580
|
+
const rec = /** @type {Record<string, unknown>} */ (node)
|
|
2581
|
+
if (!Object.hasOwn(rec, part)) return { resolved: false, value: undefined }
|
|
2582
|
+
node = rec[part]
|
|
2583
|
+
}
|
|
2584
|
+
return { resolved: true, value: node }
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2382
2587
|
/**
|
|
2383
2588
|
* Keyboard shortcut → triage verdict mapping (used by the triage panel).
|
|
2384
2589
|
* @type {Record<string, 'keep' | 'skip' | 'ignore'>}
|
|
@@ -2594,14 +2799,19 @@ function pickExportExtras(extra) {
|
|
|
2594
2799
|
* One row per round: findings raised, findings fixed, severity mix and the
|
|
2595
2800
|
* validation command outcomes recorded on the manifest.
|
|
2596
2801
|
*
|
|
2802
|
+
* `fixed` counts SUCCESSFUL fixes applied in that round, taken from the
|
|
2803
|
+
* transcript's `fixes` (each `{ round, success }`) — the review `findings`
|
|
2804
|
+
* schema carries no `status`, so reading `f.status` here always yielded 0.
|
|
2805
|
+
*
|
|
2597
2806
|
* Cross-*session* comparison is intentionally out of scope — that is what the
|
|
2598
2807
|
* #4 pull instruction is for (see `buildDiskSnapshotInstruction`).
|
|
2599
2808
|
*
|
|
2600
2809
|
* @param {unknown} report
|
|
2601
2810
|
* @param {unknown} validations
|
|
2811
|
+
* @param {unknown} [fixes] transcript fixes (`{round, success}[]`) for the fixed counts
|
|
2602
2812
|
* @returns {Array<{round: number, findings: number, fixed: number, severities: Record<string, number>, validations: Array<{command: string, exitCode: number|null, allowed: boolean}>}>}
|
|
2603
2813
|
*/
|
|
2604
|
-
export function buildRoundComparison(report, validations) {
|
|
2814
|
+
export function buildRoundComparison(report, validations, fixes) {
|
|
2605
2815
|
const rounds = new Map()
|
|
2606
2816
|
const rowsFor = (round) => {
|
|
2607
2817
|
const n = Number(round)
|
|
@@ -2619,7 +2829,15 @@ export function buildRoundComparison(report, validations) {
|
|
|
2619
2829
|
row.findings += 1
|
|
2620
2830
|
const sev = typeof f.severity === 'string' && f.severity ? f.severity : 'unknown'
|
|
2621
2831
|
row.severities[sev] = (row.severities[sev] || 0) + 1
|
|
2622
|
-
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
// Fixed counts come from applied fixes (real data), not a finding `status`
|
|
2835
|
+
// the findings schema never produces.
|
|
2836
|
+
const fixList = Array.isArray(fixes) ? fixes : []
|
|
2837
|
+
for (const fx of fixList) {
|
|
2838
|
+
if (!fx || typeof fx !== 'object') continue
|
|
2839
|
+
if ((/** @type {Record<string, unknown>} */ (fx)).success !== true) continue
|
|
2840
|
+
rowsFor((/** @type {Record<string, unknown>} */ (fx)).round).fixed += 1
|
|
2623
2841
|
}
|
|
2624
2842
|
|
|
2625
2843
|
const vals = Array.isArray(validations) ? validations : []
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate-plugin",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.0",
|
|
4
4
|
"description": "dsh plugin that turns the iterate skill into an autonomous closed-loop harness with quality command center and experience bank (v3.3). Features: plan -> parallel review xN -> atomic fixes -> validate -> loop -> auto-stop, plus dry-run pure-review mode, quality gate compute/persist, writable experience bank, defense events stream (record + bilingual labels), and native command buttons — with a live quality command center (F8/F9/F10 render real session data), §8 assign-fix instruction, and task_mode indicator wired end to end.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/jingzhao-l/iterate-plugin.git"
|
|
10
10
|
},
|
|
11
|
-
"homepage": "https://github.com/jingzhao-l/iterate-plugin",
|
|
11
|
+
"homepage": "https://github.com/jingzhao-l/iterate-plugin#readme",
|
|
12
12
|
"bugs": {
|
|
13
13
|
"url": "https://github.com/jingzhao-l/iterate-plugin/issues"
|
|
14
14
|
},
|
|
@@ -45,7 +45,10 @@
|
|
|
45
45
|
"0.1.7-rc.2": "compatible",
|
|
46
46
|
"0.2.0-rc.1": "compatible",
|
|
47
47
|
"0.2.0-rc.2": "compatible",
|
|
48
|
-
"0.2.1-alpha.1": "compatible"
|
|
48
|
+
"0.2.1-alpha.1": "compatible",
|
|
49
|
+
"0.2.1-alpha.2": "compatible",
|
|
50
|
+
"0.2.1-alpha.3": "compatible",
|
|
51
|
+
"0.2.1-alpha.4": "compatible"
|
|
49
52
|
}
|
|
50
53
|
},
|
|
51
54
|
"client": {
|
|
@@ -76,29 +79,49 @@
|
|
|
76
79
|
"build": "tsc -p tsconfig.build.json",
|
|
77
80
|
"build:client": "node scripts/build-client.mjs",
|
|
78
81
|
"prepublishOnly": "npm run build && npm run build:client",
|
|
79
|
-
"typecheck": "tsc --noEmit",
|
|
82
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json && tsc --noEmit -p tsconfig.client.json",
|
|
80
83
|
"typecheck:client": "tsc --noEmit -p tsconfig.client.json",
|
|
81
84
|
"test": "tsx --test test/*.test.ts",
|
|
82
85
|
"test:validate": "tsx --test test/validate.test.ts"
|
|
83
86
|
},
|
|
84
87
|
"dependencies": {
|
|
85
|
-
"@deepseek-ai/cordis": "4.0.5-alpha.1",
|
|
86
|
-
"@deepseek-ai/dsh-tools": "0.2.1-alpha.1",
|
|
87
|
-
"@deepseek-ai/dsh-util-values": "0.2.1-alpha.1",
|
|
88
88
|
"js-yaml": "4.3.2"
|
|
89
89
|
},
|
|
90
|
+
"peerDependencies": {
|
|
91
|
+
"@deepseek-ai/cordis": "^4.0.5-alpha.2",
|
|
92
|
+
"@deepseek-ai/dsh-tools": "0.1.1-rc.1 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4",
|
|
93
|
+
"@deepseek-ai/dsh-util-values": "0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4"
|
|
94
|
+
},
|
|
95
|
+
"peerDependenciesMeta": {
|
|
96
|
+
"@deepseek-ai/cordis": {
|
|
97
|
+
"optional": true
|
|
98
|
+
},
|
|
99
|
+
"@deepseek-ai/dsh-tools": {
|
|
100
|
+
"optional": true
|
|
101
|
+
},
|
|
102
|
+
"@deepseek-ai/dsh-util-values": {
|
|
103
|
+
"optional": true
|
|
104
|
+
}
|
|
105
|
+
},
|
|
90
106
|
"devDependencies": {
|
|
91
|
-
"@deepseek-ai/
|
|
92
|
-
"@deepseek-ai/dsh-
|
|
93
|
-
"@deepseek-ai/dsh-
|
|
107
|
+
"@deepseek-ai/cordis": "4.0.5-alpha.2",
|
|
108
|
+
"@deepseek-ai/dsh-agent": "0.2.1-alpha.4",
|
|
109
|
+
"@deepseek-ai/dsh-jobs": "0.2.1-alpha.4",
|
|
110
|
+
"@deepseek-ai/dsh-session": "0.2.1-alpha.4",
|
|
111
|
+
"@deepseek-ai/dsh-tools": "0.2.1-alpha.4",
|
|
112
|
+
"@deepseek-ai/dsh-util-values": "0.2.1-alpha.4",
|
|
94
113
|
"@types/js-yaml": "4.0.9",
|
|
95
114
|
"@types/node": "22.15.0",
|
|
96
115
|
"@types/react": "19.2.2",
|
|
97
116
|
"esbuild": "0.25.12",
|
|
117
|
+
"jsdom": "26.1.0",
|
|
118
|
+
"react": "19.2.2",
|
|
119
|
+
"react-dom": "19.2.2",
|
|
98
120
|
"tsx": "4.20.3",
|
|
99
121
|
"typescript": "5.9.3"
|
|
100
122
|
},
|
|
101
123
|
"engines": {
|
|
102
|
-
"node": ">=20"
|
|
124
|
+
"node": ">=20",
|
|
125
|
+
"dsh": ">=0.1.1-rc.1"
|
|
103
126
|
}
|
|
104
127
|
}
|