iterate-plugin 3.6.0 → 3.7.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 +37 -6
- package/README.zh-CN.md +29 -5
- package/dist/config-loader.js +7 -1
- package/dist/file-lock.js +40 -12
- package/dist/index.js +7 -0
- package/dist/live.js +2 -1
- package/dist/session-hooks.js +6 -1
- package/dist/tools/decision-log.js +4 -10
- package/dist/working-directory.js +114 -0
- package/lib/client.js +1097 -461
- package/lib/parse.js +253 -58
- package/package.json +16 -11
- package/src/client/i18n.ts +517 -0
- package/src/client/index.ts +531 -340
- package/src/client/session-cache.ts +54 -0
- package/src/config-loader.ts +7 -1
- package/src/file-lock.ts +36 -9
- package/src/index.ts +7 -0
- package/src/live.ts +2 -1
- package/src/session-hooks.ts +6 -1
- package/src/tools/decision-log.ts +4 -6
- 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,40 @@ 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
|
+
if (lang === 'en') return en[reason] ?? (reason ? `Done \u00b7 ${reason}` : 'Done')
|
|
2285
|
+
return zh[reason] ?? (reason ? `已结束 · ${reason}` : '已结束')
|
|
2247
2286
|
}
|
|
2248
2287
|
|
|
2249
2288
|
// ─── Completion notification ────────────────────────────────────────────────
|
|
@@ -2252,48 +2291,93 @@ export function stoppedReasonLabel(reason) {
|
|
|
2252
2291
|
* One-line completion summary for notifications ("已收敛 / 已达最大轮数").
|
|
2253
2292
|
*
|
|
2254
2293
|
* @param {Record<string, unknown>} report
|
|
2294
|
+
* @param {'zh'|'en'} [lang] UI language
|
|
2255
2295
|
* @returns {string}
|
|
2256
2296
|
*/
|
|
2257
|
-
export function buildCompletionSummary(report) {
|
|
2297
|
+
export function buildCompletionSummary(report, lang = 'zh') {
|
|
2258
2298
|
const conv = /** @type {Record<string, unknown>} */ (report.convergence ?? {})
|
|
2259
2299
|
const rounds = getCurrentRound(report)
|
|
2260
2300
|
const total = getTotalRounds(report)
|
|
2261
2301
|
const stats = severityStats(report)
|
|
2262
2302
|
const converged = conv.converged === true
|
|
2263
|
-
const reason = converged ? '已收敛' : `已达最大轮数 ${total}`
|
|
2264
2303
|
const totalFindings = stats.critical + stats.high + stats.medium + stats.low
|
|
2304
|
+
if (lang === 'en') {
|
|
2305
|
+
const reason = converged ? 'converged' : `reached the round limit ${total}`
|
|
2306
|
+
return `iterate review complete \u00b7 ${rounds}/${total} rounds \u00b7 ${totalFindings} findings \u00b7 ${reason}`
|
|
2307
|
+
}
|
|
2308
|
+
const reason = converged ? '已收敛' : `已达最大轮数 ${total}`
|
|
2265
2309
|
return `iterate 评审完成 · ${rounds}/${total} 轮 · ${totalFindings} 项发现 · ${reason}`
|
|
2266
2310
|
}
|
|
2267
2311
|
|
|
2268
2312
|
// ─── Config edit guidance ───────────────────────────────────────────────────
|
|
2269
2313
|
|
|
2270
2314
|
/**
|
|
2271
|
-
* Editable config fields
|
|
2272
|
-
*
|
|
2315
|
+
* Editable config fields, used by the settings picker (rendered in the UI) and
|
|
2316
|
+
* by the copy-paste guide / instruction builders (model-facing, always zh).
|
|
2317
|
+
* `label`/`hint` are `{ zh, en }` pairs — the UI localizes them with
|
|
2318
|
+
* `field.label[getLang()]`, while the guide builders below read `.zh`.
|
|
2319
|
+
* @type {Array<{ key: string, label: { zh: string, en: string }, hint: { zh: string, en: string } }>}
|
|
2273
2320
|
*/
|
|
2274
2321
|
export const CONFIG_EDIT_FIELDS = [
|
|
2275
|
-
{
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2322
|
+
{
|
|
2323
|
+
key: 'goal',
|
|
2324
|
+
label: { zh: '目标', en: 'Goal' },
|
|
2325
|
+
hint: { zh: '一句话描述本次迭代目标(字符串)', en: "One-sentence description of this iteration's goal (a string)" },
|
|
2326
|
+
},
|
|
2327
|
+
{
|
|
2328
|
+
key: 'dimensions',
|
|
2329
|
+
label: { zh: '审查维度', en: 'Review dimensions' },
|
|
2330
|
+
hint: { zh: '数组,如 ["correctness","security"]', en: 'Array, e.g. ["correctness","security"]' },
|
|
2331
|
+
},
|
|
2332
|
+
{
|
|
2333
|
+
key: 'max_rounds',
|
|
2334
|
+
label: { zh: '最大轮数', en: 'Max rounds' },
|
|
2335
|
+
hint: { zh: '正整数', en: 'Positive integer' },
|
|
2336
|
+
},
|
|
2337
|
+
{
|
|
2338
|
+
key: 'review.scope',
|
|
2339
|
+
label: { zh: '审查范围', en: 'Review scope' },
|
|
2340
|
+
hint: { zh: '"full" 或 "changed-only"', en: '"full" or "changed-only"' },
|
|
2341
|
+
},
|
|
2342
|
+
{
|
|
2343
|
+
key: 'reasoning_effort',
|
|
2344
|
+
label: { zh: '审查推理强度', en: 'Review reasoning effort' },
|
|
2345
|
+
hint: { zh: '"low" / "medium" / "high",缺省跟随模型默认', en: '"low" / "medium" / "high"; omitted = follow the model default' },
|
|
2346
|
+
},
|
|
2280
2347
|
{
|
|
2281
2348
|
key: 'validation.commands',
|
|
2282
|
-
label: '验证命令',
|
|
2283
|
-
hint:
|
|
2349
|
+
label: { zh: '验证命令', en: 'Validation commands' },
|
|
2350
|
+
hint: {
|
|
2351
|
+
zh: '数组,每项是一条白名单命令字符串,如 ["npm test","npm run lint"];每轮验证逐条执行。留空数组 = 不验证(本轮迭代不受任何测试保护)',
|
|
2352
|
+
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)',
|
|
2353
|
+
},
|
|
2284
2354
|
},
|
|
2285
2355
|
{
|
|
2286
2356
|
key: 'language',
|
|
2287
|
-
label: '界面语言',
|
|
2288
|
-
hint:
|
|
2357
|
+
label: { zh: '界面语言', en: 'Interface language' },
|
|
2358
|
+
hint: {
|
|
2359
|
+
zh: '"zh" 或 "en"(默认 "en"):控制审批理由、防御事件标签等面向人文案的语言',
|
|
2360
|
+
en: '"zh" or "en" (default "en"): language of human-facing copy such as approval rationales and defense-event labels',
|
|
2361
|
+
},
|
|
2289
2362
|
},
|
|
2290
2363
|
{
|
|
2291
2364
|
key: 'personalization.known_intentional',
|
|
2292
|
-
label: '已知有意问题',
|
|
2293
|
-
hint:
|
|
2365
|
+
label: { zh: '已知有意问题', en: 'Known intentional issues' },
|
|
2366
|
+
hint: {
|
|
2367
|
+
zh: '数组,每项 {file, line?, dimension, reason};命中条目在评审中被跳过。可用分诊面板的「回读已写入条目」核对',
|
|
2368
|
+
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"',
|
|
2369
|
+
},
|
|
2370
|
+
},
|
|
2371
|
+
{
|
|
2372
|
+
key: 'atomic.max_lines',
|
|
2373
|
+
label: { zh: '原子修复上限行数', en: 'Atomic fix line cap' },
|
|
2374
|
+
hint: { zh: '正整数', en: 'Positive integer' },
|
|
2375
|
+
},
|
|
2376
|
+
{
|
|
2377
|
+
key: 'git.push_per_round',
|
|
2378
|
+
label: { zh: '每轮推送', en: 'Push per round' },
|
|
2379
|
+
hint: { zh: 'true / false', en: 'true / false' },
|
|
2294
2380
|
},
|
|
2295
|
-
{ key: 'atomic.max_lines', label: '原子修复上限行数', hint: '正整数' },
|
|
2296
|
-
{ key: 'git.push_per_round', label: '每轮推送', hint: 'true / false' },
|
|
2297
2381
|
]
|
|
2298
2382
|
|
|
2299
2383
|
/**
|
|
@@ -2312,7 +2396,7 @@ export function buildConfigEditGuide() {
|
|
|
2312
2396
|
' 请先给 validation.commands 配上至少一条真实命令(如 npm test)。',
|
|
2313
2397
|
'',
|
|
2314
2398
|
'可编辑字段:',
|
|
2315
|
-
...CONFIG_EDIT_FIELDS.map((f) => `- ${f.key}(${f.label}):${f.hint}`),
|
|
2399
|
+
...CONFIG_EDIT_FIELDS.map((f) => `- ${f.key}(${f.label.zh}):${f.hint.zh}`),
|
|
2316
2400
|
'',
|
|
2317
2401
|
'让模型帮你改:',
|
|
2318
2402
|
'1. 调用 iterate_config({ operation: "read" }) 查看当前配置;',
|
|
@@ -2344,7 +2428,7 @@ export const CONFIG_VALUE_PLACEHOLDER = '<由模型填写:该字段的新值>'
|
|
|
2344
2428
|
* Look up an editable config field by its dotted key (gap #8).
|
|
2345
2429
|
*
|
|
2346
2430
|
* @param {string} key
|
|
2347
|
-
* @returns {{ key: string, label: string, hint: string } | null}
|
|
2431
|
+
* @returns {{ key: string, label: { zh: string, en: string }, hint: { zh: string, en: string } } | null}
|
|
2348
2432
|
*/
|
|
2349
2433
|
export function configFieldByKey(key) {
|
|
2350
2434
|
if (typeof key !== 'string' || !key) return null
|
|
@@ -2363,8 +2447,9 @@ export function configFieldByKey(key) {
|
|
|
2363
2447
|
*/
|
|
2364
2448
|
export function buildConfigFieldInstruction(key, value) {
|
|
2365
2449
|
const field = configFieldByKey(key)
|
|
2366
|
-
|
|
2367
|
-
const
|
|
2450
|
+
// Model-facing instruction text: always zh, like every other builder here.
|
|
2451
|
+
const label = field ? field.label.zh : key
|
|
2452
|
+
const hint = field ? field.hint.zh : '见 iterate.config.yaml'
|
|
2368
2453
|
const updates = { [key]: value === undefined ? CONFIG_VALUE_PLACEHOLDER : value }
|
|
2369
2454
|
// Delegate the payload to `buildConfigEditInstruction` so the multi-field
|
|
2370
2455
|
// builder stays a live code path (it used to be test-only dead code).
|
|
@@ -2379,6 +2464,116 @@ export function buildConfigFieldInstruction(key, value) {
|
|
|
2379
2464
|
].join('\n')
|
|
2380
2465
|
}
|
|
2381
2466
|
|
|
2467
|
+
// ─── Config read-back (write → read-back loop) ──────────────────────────────
|
|
2468
|
+
//
|
|
2469
|
+
// The triage panel got a read-back instruction (#7); the config editor only
|
|
2470
|
+
// generated writes, so there was no way to SEE what the file actually says
|
|
2471
|
+
// after pasting an instruction. These three pieces close that loop: an
|
|
2472
|
+
// instruction the settings card copies, a session scanner that picks the
|
|
2473
|
+
// echoed result back up, and a dotted-path resolver that surfaces the value
|
|
2474
|
+
// of the field the user currently has selected.
|
|
2475
|
+
|
|
2476
|
+
/**
|
|
2477
|
+
* Copy-paste instruction that makes the model call `iterate_config` read and
|
|
2478
|
+
* echo the full result (including `config`) into the session, where the
|
|
2479
|
+
* settings card can display it.
|
|
2480
|
+
*
|
|
2481
|
+
* @returns {string}
|
|
2482
|
+
*/
|
|
2483
|
+
export function buildConfigReadInstruction() {
|
|
2484
|
+
return (
|
|
2485
|
+
'请调用 `iterate_config` 回读当前生效配置,并把完整结果(含 config 字段)原样回显,' +
|
|
2486
|
+
'供我核对界面上展示的当前值:\n\n' +
|
|
2487
|
+
'```json\n' +
|
|
2488
|
+
`${JSON.stringify({ operation: 'read' }, null, 2)}\n` +
|
|
2489
|
+
'```'
|
|
2490
|
+
)
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
/**
|
|
2494
|
+
* Top-level keys of an iterate config, derived from the editable field list
|
|
2495
|
+
* so a newly editable section is covered by the predicate automatically.
|
|
2496
|
+
* @type {string[]}
|
|
2497
|
+
*/
|
|
2498
|
+
const CONFIG_TOP_LEVEL_KEYS = [...new Set(CONFIG_EDIT_FIELDS.map((f) => f.key.split('.')[0]))]
|
|
2499
|
+
|
|
2500
|
+
/**
|
|
2501
|
+
* Is `v` an effective iterate config? Requires several known top-level keys:
|
|
2502
|
+
* `loadEffectiveConfig` always merges the built-in defaults, so a real config
|
|
2503
|
+
* is complete — while a stray `{ goal: ... }` record elsewhere in the
|
|
2504
|
+
* transcript cannot impersonate one.
|
|
2505
|
+
*
|
|
2506
|
+
* @param {unknown} v
|
|
2507
|
+
* @returns {boolean}
|
|
2508
|
+
*/
|
|
2509
|
+
export function isEffectiveConfig(v) {
|
|
2510
|
+
if (!v || typeof v !== 'object' || Array.isArray(v)) return false
|
|
2511
|
+
const rec = /** @type {Record<string, unknown>} */ (v)
|
|
2512
|
+
let hits = 0
|
|
2513
|
+
for (const key of CONFIG_TOP_LEVEL_KEYS) {
|
|
2514
|
+
if (Object.hasOwn(rec, key)) hits += 1
|
|
2515
|
+
if (hits >= 3) return true
|
|
2516
|
+
}
|
|
2517
|
+
return false
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
/**
|
|
2521
|
+
* Locate the config inside one `iterate_config` tool result. Reads and writes
|
|
2522
|
+
* both report it under `config`; a section read (`data`) is deliberately not
|
|
2523
|
+
* accepted because it is only a slice.
|
|
2524
|
+
*
|
|
2525
|
+
* @param {unknown} v
|
|
2526
|
+
* @returns {Record<string, unknown> | null}
|
|
2527
|
+
*/
|
|
2528
|
+
export function findConfigInResult(v) {
|
|
2529
|
+
if (!v || typeof v !== 'object' || Array.isArray(v)) return null
|
|
2530
|
+
const rec = /** @type {Record<string, unknown>} */ (v)
|
|
2531
|
+
const cfg = safeGet(rec, 'config')
|
|
2532
|
+
if (isEffectiveConfig(cfg)) return /** @type {Record<string, unknown>} */ (cfg)
|
|
2533
|
+
return isEffectiveConfig(v) ? rec : null
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
/**
|
|
2537
|
+
* Latest effective config echoed into the session: the newest full
|
|
2538
|
+
* `iterate_config` result (read or post-write), falling back to a deep scan
|
|
2539
|
+
* for a config that reached the stream without the tool envelope (restored
|
|
2540
|
+
* transcript, host that surfaces artifacts directly).
|
|
2541
|
+
*
|
|
2542
|
+
* @param {unknown} session
|
|
2543
|
+
* @returns {Record<string, unknown> | null}
|
|
2544
|
+
*/
|
|
2545
|
+
export function scanSessionForConfigRead(session) {
|
|
2546
|
+
const node = latestToolResultNode(session, 'iterate_config', findConfigInResult)
|
|
2547
|
+
if (node !== null) {
|
|
2548
|
+
const cfg = findConfigInResult(node)
|
|
2549
|
+
if (cfg) return cfg
|
|
2550
|
+
}
|
|
2551
|
+
const loose = findFirstInObject(session, isEffectiveConfig)
|
|
2552
|
+
return loose ? /** @type {Record<string, unknown>} */ (loose) : null
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
/**
|
|
2556
|
+
* Resolve a dotted editable-field key (`validation.commands`, `review.scope`)
|
|
2557
|
+
* against a read-back config.
|
|
2558
|
+
*
|
|
2559
|
+
* @param {Record<string, unknown> | null | undefined} config
|
|
2560
|
+
* @param {string} key dotted field key from CONFIG_EDIT_FIELDS
|
|
2561
|
+
* @returns {{ resolved: boolean, value: unknown }} `value` is undefined when unresolved
|
|
2562
|
+
*/
|
|
2563
|
+
export function configValueByPath(config, key) {
|
|
2564
|
+
if (!config || typeof config !== 'object' || typeof key !== 'string' || !key) {
|
|
2565
|
+
return { resolved: false, value: undefined }
|
|
2566
|
+
}
|
|
2567
|
+
let node = /** @type {unknown} */ (config)
|
|
2568
|
+
for (const part of key.split('.')) {
|
|
2569
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return { resolved: false, value: undefined }
|
|
2570
|
+
const rec = /** @type {Record<string, unknown>} */ (node)
|
|
2571
|
+
if (!Object.hasOwn(rec, part)) return { resolved: false, value: undefined }
|
|
2572
|
+
node = rec[part]
|
|
2573
|
+
}
|
|
2574
|
+
return { resolved: true, value: node }
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2382
2577
|
/**
|
|
2383
2578
|
* Keyboard shortcut → triage verdict mapping (used by the triage panel).
|
|
2384
2579
|
* @type {Record<string, 'keep' | 'skip' | 'ignore'>}
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate-plugin",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.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",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/jingzhao-l/iterate-
|
|
9
|
+
"url": "https://github.com/jingzhao-l/iterate-skill.git",
|
|
10
|
+
"directory": "harness/iterate-plugin"
|
|
10
11
|
},
|
|
11
|
-
"homepage": "https://github.com/jingzhao-l/iterate-plugin",
|
|
12
|
+
"homepage": "https://github.com/jingzhao-l/iterate-skill/tree/main/harness/iterate-plugin",
|
|
12
13
|
"bugs": {
|
|
13
|
-
"url": "https://github.com/jingzhao-l/iterate-
|
|
14
|
+
"url": "https://github.com/jingzhao-l/iterate-skill/issues"
|
|
14
15
|
},
|
|
15
16
|
"keywords": [
|
|
16
17
|
"dsh-plugin",
|
|
@@ -45,7 +46,8 @@
|
|
|
45
46
|
"0.1.7-rc.2": "compatible",
|
|
46
47
|
"0.2.0-rc.1": "compatible",
|
|
47
48
|
"0.2.0-rc.2": "compatible",
|
|
48
|
-
"0.2.1-alpha.1": "compatible"
|
|
49
|
+
"0.2.1-alpha.1": "compatible",
|
|
50
|
+
"0.2.1-alpha.2": "compatible"
|
|
49
51
|
}
|
|
50
52
|
},
|
|
51
53
|
"client": {
|
|
@@ -76,25 +78,28 @@
|
|
|
76
78
|
"build": "tsc -p tsconfig.build.json",
|
|
77
79
|
"build:client": "node scripts/build-client.mjs",
|
|
78
80
|
"prepublishOnly": "npm run build && npm run build:client",
|
|
79
|
-
"typecheck": "tsc --noEmit",
|
|
81
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json && tsc --noEmit -p tsconfig.client.json",
|
|
80
82
|
"typecheck:client": "tsc --noEmit -p tsconfig.client.json",
|
|
81
83
|
"test": "tsx --test test/*.test.ts",
|
|
82
84
|
"test:validate": "tsx --test test/validate.test.ts"
|
|
83
85
|
},
|
|
84
86
|
"dependencies": {
|
|
85
87
|
"@deepseek-ai/cordis": "4.0.5-alpha.1",
|
|
86
|
-
"@deepseek-ai/dsh-tools": "0.2.1-alpha.
|
|
87
|
-
"@deepseek-ai/dsh-util-values": "0.2.1-alpha.
|
|
88
|
+
"@deepseek-ai/dsh-tools": "0.2.1-alpha.2",
|
|
89
|
+
"@deepseek-ai/dsh-util-values": "0.2.1-alpha.2",
|
|
88
90
|
"js-yaml": "4.3.2"
|
|
89
91
|
},
|
|
90
92
|
"devDependencies": {
|
|
91
|
-
"@deepseek-ai/dsh-agent": "0.2.1-alpha.
|
|
92
|
-
"@deepseek-ai/dsh-jobs": "0.2.1-alpha.
|
|
93
|
-
"@deepseek-ai/dsh-session": "0.2.1-alpha.
|
|
93
|
+
"@deepseek-ai/dsh-agent": "0.2.1-alpha.2",
|
|
94
|
+
"@deepseek-ai/dsh-jobs": "0.2.1-alpha.2",
|
|
95
|
+
"@deepseek-ai/dsh-session": "0.2.1-alpha.2",
|
|
94
96
|
"@types/js-yaml": "4.0.9",
|
|
95
97
|
"@types/node": "22.15.0",
|
|
96
98
|
"@types/react": "19.2.2",
|
|
97
99
|
"esbuild": "0.25.12",
|
|
100
|
+
"jsdom": "26.1.0",
|
|
101
|
+
"react": "19.2.2",
|
|
102
|
+
"react-dom": "19.2.2",
|
|
98
103
|
"tsx": "4.20.3",
|
|
99
104
|
"typescript": "5.9.3"
|
|
100
105
|
},
|