iterate-plugin 3.5.0 → 3.5.1
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 +2 -2
- package/README.zh-CN.md +2 -2
- package/dist/live.js +5 -0
- package/dist/skill-prompt.js +2 -2
- package/dist/tools/checkpoint.js +21 -0
- package/dist/tools/experience-bank.js +39 -3
- package/dist/tools/experience-store.js +24 -0
- package/dist/tools/fix.js +6 -0
- package/dist/tools/quality-gate.js +21 -4
- package/dist/tools/quality-store.js +18 -0
- package/dist/tools/transcript.js +31 -0
- package/package.json +5 -2
- package/src/live.ts +5 -0
- package/src/skill-prompt.ts +2 -2
- package/src/tools/checkpoint.ts +27 -0
- package/src/tools/experience-bank.ts +40 -3
- package/src/tools/experience-store.ts +26 -0
- package/src/tools/fix.ts +7 -0
- package/src/tools/quality-gate.ts +22 -4
- package/src/tools/quality-store.ts +19 -0
- package/src/tools/transcript.ts +32 -0
package/README.md
CHANGED
|
@@ -260,8 +260,8 @@ validation:
|
|
|
260
260
|
- `iterate_history` — read iteration history (read-only): decision-log entries (filter by `type` / `since` / `limit`, default latest 50, cap 200) + fix-registry summary (per-round fixed/failed counts). For auditing the run, tracing logs, and inventorying fixes
|
|
261
261
|
- `iterate_prune` — clean runtime artifacts: stale decision-log entries (by `retainDays`, default 30), stale checkpoints, orphaned fix backups, empty rounds, stray temp files (crashed atomic writes). Dry-run by default (report-only); real cleanup with `dryRun:false`, each cleanup logged
|
|
262
262
|
- `iterate_transcript` — runtime observatory: persist review transcripts, threads, fixes, and nudge directions to `.iterate/transcript.json` for the client observatory
|
|
263
|
-
- `iterate_experience` — **v3.2** query the experience bank (list / search / get),
|
|
264
|
-
- `iterate_quality_gate` — **v3.2** read the quality certificate (`read`), or recompute + persist a fresh one (`compute`) from findings, validation results, `findingsByRound`, and `fixedByDimension`. Real per-dimension convergence rates
|
|
263
|
+
- `iterate_experience` — **v3.2** query the experience bank (list / search / get), `add` a new verified fix (re-adding the same pattern+dimension bumps its hit count instead of duplicating it), or **v3.5 remove a stale/incorrect entry by `id`** so bad experiences never resurface. Persists to `.iterate/experience.json`
|
|
264
|
+
- `iterate_quality_gate` — **v3.2** read the quality certificate (`read`), or recompute + persist a fresh one (`compute`) from findings, validation results, `findingsByRound`, and `fixedByDimension`. Real per-dimension convergence rates. **v3.5: `clear` removes the persisted certificate to reset a stale gate before a fresh iteration**
|
|
265
265
|
- `iterate_defense_events` — **v3.2** query defense events (list / counts), or `record` a new one. Human-readable labels follow the project language (en / zh)
|
|
266
266
|
|
|
267
267
|
---
|
package/README.zh-CN.md
CHANGED
|
@@ -259,8 +259,8 @@ validation:
|
|
|
259
259
|
- `iterate_history` — 读取迭代历史(只读):决策日志条目(可按 `type` / `since` / `limit` 过滤,默认取最新 50 条,上限 200 条)+ 修复注册表汇总(各轮 fixed/failed 计数)。用于审查运行过程、审计日志、盘点修复
|
|
260
260
|
- `iterate_prune` — 清理运行时产物:过期决策日志条目(按 `retainDays`,默认 30 天)、陈旧断点、孤儿修复备份、空轮次、崩溃原子写入残留的临时文件。默认 dry-run 只报告不删除;`dryRun:false` 才真正清理,每次清理写入决策日志
|
|
261
261
|
- `iterate_transcript` — 运行时观测台:把审查转录、线程、修复与 nudge 指令持久化到 `.iterate/transcript.json`,供客户端观测台读取
|
|
262
|
-
- `iterate_experience` — **v3.2** 查询经验银行(list / search / get),或 `add` 一条新的已验证修复:重复添加同一 pattern + dimension
|
|
263
|
-
- `iterate_quality_gate` — **v3.2** 读取质量凭证(`read`),或基于 findings、验证结果、`findingsByRound` 与 `fixedByDimension` 重新计算并持久化一份新凭证(`compute
|
|
262
|
+
- `iterate_experience` — **v3.2** 查询经验银行(list / search / get),或 `add` 一条新的已验证修复:重复添加同一 pattern + dimension 累加命中次数而非重复写入。**v3.5 新增 `remove`:按 `id` 删除过时或错误的经验条目**。持久化到 `.iterate/experience.json`
|
|
263
|
+
- `iterate_quality_gate` — **v3.2** 读取质量凭证(`read`),或基于 findings、验证结果、`findingsByRound` 与 `fixedByDimension` 重新计算并持久化一份新凭证(`compute`)。真实的逐维度收敛率。**v3.5 新增 `clear`:删除持久化的质量凭证**,用于新一轮迭代前重置陈旧的门禁
|
|
264
264
|
- `iterate_defense_events` — **v3.2** 查询防御事件(list / counts),或 `record` 记录一条新事件。可读标签跟随项目语言(en / zh)
|
|
265
265
|
|
|
266
266
|
---
|
package/dist/live.js
CHANGED
|
@@ -69,6 +69,11 @@ export function classifyTool(name, args, projectRoot) {
|
|
|
69
69
|
iterate_status: 'info',
|
|
70
70
|
iterate_config: 'info',
|
|
71
71
|
iterate_context: 'info',
|
|
72
|
+
// v3.0 quality command-center tools — surface their activity in the live
|
|
73
|
+
// feed too (previously these three never produced a live entry).
|
|
74
|
+
iterate_experience: 'info',
|
|
75
|
+
iterate_quality_gate: 'info',
|
|
76
|
+
iterate_defense_events: 'info',
|
|
72
77
|
};
|
|
73
78
|
const type = records[name];
|
|
74
79
|
if (!type)
|
package/dist/skill-prompt.js
CHANGED
|
@@ -23,8 +23,8 @@ You have the iterate plugin installed, which registers these tools:
|
|
|
23
23
|
- \`iterate_history\` — inspect the runtime state in detail: decision-log entries and applied fixes (optionally scoped to a round or a fixed file)
|
|
24
24
|
- \`iterate_prune\` — remove stale runtime artifacts (\`.iterate/\` entries). Defaults to a read-only dry-run that reports what WOULD be removed; pass \`dryRun:false\` to actually prune.
|
|
25
25
|
- \`iterate_transcript\` — runtime observatory file (\`.iterate/transcript.json\`). \`read\` fetches the persisted manifest including any steering \`nudge\` for this run's reviewers; \`capture\` (call once after the final report) persists the per-reviewer threads, convergence trend, findings, fixes, checkpoint, and timeline so the client observatory panel reflects the run; \`nudge\` sets/clears steering text the next round's reviewers read. Purely local, never touches source files.
|
|
26
|
-
- \`iterate_experience\` — experience bank (\`.iterate/experience.json\`): \`list\`/\`search\`/\`get\` recall verified fixes and patterns from past sessions (read the bank before fixing so proven fixes are applied first); \`add\` records a new verified fix — re-adding the same pattern+dimension bumps its hit count instead of duplicating it.
|
|
27
|
-
- \`iterate_quality_gate\` — quality certificate: \`read\` loads the persisted dimension convergence rates / verification pass rate / PASS-FAIL status; \`compute\` recomputes a fresh snapshot from this round's findings + validation results (supply \`findingsByRound\` for real convergence) and persists it to \`.iterate/quality-gate.json
|
|
26
|
+
- \`iterate_experience\` — experience bank (\`.iterate/experience.json\`): \`list\`/\`search\`/\`get\` recall verified fixes and patterns from past sessions (read the bank before fixing so proven fixes are applied first); \`add\` records a new verified fix — re-adding the same pattern+dimension bumps its hit count instead of duplicating it; \`remove\` deletes a stale or incorrect entry by \`id\` so bad experiences never resurface.
|
|
27
|
+
- \`iterate_quality_gate\` — quality certificate: \`read\` loads the persisted dimension convergence rates / verification pass rate / PASS-FAIL status; \`compute\` recomputes a fresh snapshot from this round's findings + validation results (supply \`findingsByRound\` for real convergence) and persists it to \`.iterate/quality-gate.json\`; \`clear\` removes the persisted certificate to reset a stale gate before a fresh iteration.
|
|
28
28
|
- \`iterate_defense_events\` — defense event stream (\`.iterate/defense-events.json\`): \`list\`/\`counts\` review precondition failures, rollbacks, invariant violations, and falsified assumptions; \`record\` logs a new event when a defense fires. Human-readable labels follow the project \`language\` (en/zh).
|
|
29
29
|
|
|
30
30
|
### When to use
|
package/dist/tools/checkpoint.js
CHANGED
|
@@ -302,6 +302,27 @@ export function registerStatusTool(ctx) {
|
|
|
302
302
|
`Checkpoint: ${value.hasCheckpoint ? 'yes' : 'no'}${value.interrupted ? ' (interrupted — resumable)' : ''}${value.resumeCount ? ` · resumed ${value.resumeCount}x` : ''}`,
|
|
303
303
|
value.lastUpdated ? `Last updated: ${value.lastUpdated}` : '',
|
|
304
304
|
];
|
|
305
|
+
// v3.0 quality command-center summaries (surfaced here so a single
|
|
306
|
+
// status read reports the whole run state in text too).
|
|
307
|
+
const gate = value.qualityGate;
|
|
308
|
+
if (gate) {
|
|
309
|
+
lines.push(`Quality gate: ${gate.overallStatus.toUpperCase()} · score ${gate.overallScore} · ` +
|
|
310
|
+
`verification ${gate.passedChecks}/${gate.totalChecks} passed` +
|
|
311
|
+
(gate.failReason ? ` · ${gate.failReason}` : ''));
|
|
312
|
+
}
|
|
313
|
+
const exp = value.experienceBank;
|
|
314
|
+
if (exp) {
|
|
315
|
+
lines.push(`Experience bank: ${exp.totalEntries ?? 0} entries · ${exp.totalHits ?? 0} cumulative hits`);
|
|
316
|
+
}
|
|
317
|
+
const def = value.defenseEvents;
|
|
318
|
+
if (def) {
|
|
319
|
+
const counts = def.counts ?? {};
|
|
320
|
+
const total = Object.values(counts).reduce((s, n) => s + (n || 0), 0);
|
|
321
|
+
lines.push(`Defense events: ${def.totalEvents ?? total} recorded` +
|
|
322
|
+
(total > 0
|
|
323
|
+
? ` (precondition_failed:${counts.precondition_failed ?? 0}, rollback:${counts.rollback ?? 0}, invariant_violated:${counts.invariant_violated ?? 0}, assumption_falsified:${counts.assumption_falsified ?? 0})`
|
|
324
|
+
: ''));
|
|
325
|
+
}
|
|
305
326
|
return [{ type: 'text', text: lines.filter(Boolean).join('\n') }];
|
|
306
327
|
},
|
|
307
328
|
},
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
9
9
|
import { resolveProjectRootForExec } from "../config-loader.js";
|
|
10
|
-
import { readExperienceBank, writeExperienceBank, searchExperienceEntries, upsertExperience } from "./experience-store.js";
|
|
10
|
+
import { readExperienceBank, writeExperienceBank, searchExperienceEntries, upsertExperience, removeExperience } from "./experience-store.js";
|
|
11
11
|
const DEFAULT_LIMIT = 50;
|
|
12
12
|
const MAX_LIMIT = 100;
|
|
13
13
|
/** Clamp a caller-supplied limit to a sane range. */
|
|
@@ -75,12 +75,13 @@ export function registerExperienceBankTool(ctx) {
|
|
|
75
75
|
'List/search/get return matching entries with hit counts, verified fixes, and related context. ' +
|
|
76
76
|
'"add" upserts an experience entry into .iterate/experience.json — a repeat of the same ' +
|
|
77
77
|
'pattern+dimension increments its hit count instead of duplicating it. ' +
|
|
78
|
+
'"remove" deletes an entry by id (useful for pruning stale or incorrect experiences). ' +
|
|
78
79
|
'Use it to remember fixes that worked so future rounds apply them first.',
|
|
79
80
|
parameters: {
|
|
80
81
|
operation: {
|
|
81
82
|
type: 'string',
|
|
82
|
-
description: 'Operation: list (browse all), search (by query), get (by id), add (add a new experience). Default: list.',
|
|
83
|
-
enum: ['list', 'search', 'get', 'add'],
|
|
83
|
+
description: 'Operation: list (browse all), search (by query), get (by id), add (add a new experience), remove (delete by id). Default: list.',
|
|
84
|
+
enum: ['list', 'search', 'get', 'add', 'remove'],
|
|
84
85
|
},
|
|
85
86
|
query: {
|
|
86
87
|
type: 'string',
|
|
@@ -147,6 +148,9 @@ export function registerExperienceBankTool(ctx) {
|
|
|
147
148
|
`Tags: ${entry.tags.join(', ')}`,
|
|
148
149
|
].join('\n') }];
|
|
149
150
|
}
|
|
151
|
+
if (value.operation === 'remove') {
|
|
152
|
+
return [{ type: 'text', text: `Removed experience entry. Bank now holds ${value.count ?? 0} entries (${value.totalHits ?? 0} cumulative hits).` }];
|
|
153
|
+
}
|
|
150
154
|
if (value.operation === 'get' && value.entry) {
|
|
151
155
|
const entry = value.entry;
|
|
152
156
|
return [{ type: 'text', text: [
|
|
@@ -204,6 +208,38 @@ export function registerExperienceBankTool(ctx) {
|
|
|
204
208
|
totalHits: next.totalHits,
|
|
205
209
|
};
|
|
206
210
|
}
|
|
211
|
+
if (operation === 'remove') {
|
|
212
|
+
const id = typeof args.id === 'string' && args.id ? args.id : '';
|
|
213
|
+
if (!id) {
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
kind: 'experience',
|
|
217
|
+
operation: 'remove',
|
|
218
|
+
error: 'id is required for remove',
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const bank = readExperienceBank(projectRoot);
|
|
222
|
+
const { bank: next, removed } = removeExperience(bank, id);
|
|
223
|
+
if (!removed) {
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
kind: 'experience',
|
|
227
|
+
operation: 'remove',
|
|
228
|
+
error: `Experience not found: ${id}`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const write = writeExperienceBank(projectRoot, next);
|
|
232
|
+
if (!write.ok) {
|
|
233
|
+
return { ok: false, kind: 'experience', operation: 'remove', error: write.error };
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
ok: true,
|
|
237
|
+
kind: 'experience',
|
|
238
|
+
operation: 'remove',
|
|
239
|
+
count: next.entries.length,
|
|
240
|
+
totalHits: next.totalHits,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
207
243
|
const bank = readExperienceBank(projectRoot);
|
|
208
244
|
if (operation === 'get' && typeof args.id === 'string') {
|
|
209
245
|
const entry = bank.entries.find((e) => e.id === args.id);
|
|
@@ -140,3 +140,27 @@ export function upsertExperience(bank, entry) {
|
|
|
140
140
|
entryId: id,
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Remove an experience entry by id.
|
|
145
|
+
* Returns the resulting bank plus whether an entry was actually removed. An
|
|
146
|
+
* unknown id is a no-op (the caller decides how to surface it). Never mutates
|
|
147
|
+
* the input bank.
|
|
148
|
+
*/
|
|
149
|
+
export function removeExperience(bank, id) {
|
|
150
|
+
const target = typeof id === 'string' && id ? id : '';
|
|
151
|
+
if (!target)
|
|
152
|
+
return { bank, removed: false };
|
|
153
|
+
const entries = bank.entries.filter((e) => e.id !== target);
|
|
154
|
+
if (entries.length === bank.entries.length)
|
|
155
|
+
return { bank, removed: false };
|
|
156
|
+
const lastUpdated = new Date().toISOString();
|
|
157
|
+
return {
|
|
158
|
+
bank: {
|
|
159
|
+
...bank,
|
|
160
|
+
entries,
|
|
161
|
+
lastUpdated,
|
|
162
|
+
totalHits: bank.totalHits ?? 0,
|
|
163
|
+
},
|
|
164
|
+
removed: true,
|
|
165
|
+
};
|
|
166
|
+
}
|
package/dist/tools/fix.js
CHANGED
|
@@ -25,6 +25,7 @@ import { runWithJob } from "../jobs.js";
|
|
|
25
25
|
import { countTouchedMethods } from "../method-scope.js";
|
|
26
26
|
import { fixBackupPath, fixRegistryPath, fixesDir } from "../paths.js";
|
|
27
27
|
import { appendDecisionEntry } from "./decision-log.js";
|
|
28
|
+
import { markFixRolledBackInTranscript } from "./transcript.js";
|
|
28
29
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
29
30
|
/** Upper bound for a single fix `content` payload (characters). */
|
|
30
31
|
export const MAX_FIX_CONTENT_CHARS = 1_000_000;
|
|
@@ -708,6 +709,11 @@ export function registerRollbackTool(ctx) {
|
|
|
708
709
|
type: 'revert',
|
|
709
710
|
data: { id, file: record.finding.file, revertedDiff: record.diffSummary },
|
|
710
711
|
});
|
|
712
|
+
// Mirror the reversal into the persisted observatory transcript (F4) so
|
|
713
|
+
// the client no longer shows a rolled-back fix as "成功". Best-effort:
|
|
714
|
+
// a missing/corrupt transcript is left untouched and never breaks the
|
|
715
|
+
// rollback flow.
|
|
716
|
+
await markFixRolledBackInTranscript(projectRoot, id);
|
|
711
717
|
return { ok: true, id, file: record.finding.file };
|
|
712
718
|
},
|
|
713
719
|
}));
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
10
10
|
import { resolveProjectRootForExec } from "../config-loader.js";
|
|
11
|
-
import { readQualityGate, writeQualityGate, computeQualityGate } from "./quality-store.js";
|
|
11
|
+
import { readQualityGate, writeQualityGate, computeQualityGate, clearQualityGate } from "./quality-store.js";
|
|
12
12
|
/** Validate a single finding object; returns true when well-formed. */
|
|
13
13
|
function isValidFinding(raw) {
|
|
14
14
|
if (!raw || typeof raw !== 'object')
|
|
@@ -66,12 +66,14 @@ export function registerQualityGateTool(ctx) {
|
|
|
66
66
|
'and overall PASS/FAIL status. ' +
|
|
67
67
|
'Operation "read" (default) returns the persisted machine-readable quality certificate. ' +
|
|
68
68
|
'Operation "compute" computes a fresh snapshot from this round\'s findings/validation results, ' +
|
|
69
|
-
'persists it to .iterate/quality-gate.json, and returns it.'
|
|
69
|
+
'persists it to .iterate/quality-gate.json, and returns it. ' +
|
|
70
|
+
'Operation "clear" removes the persisted certificate (use it to reset a stale gate before ' +
|
|
71
|
+
'starting a fresh iteration).',
|
|
70
72
|
parameters: {
|
|
71
73
|
operation: {
|
|
72
74
|
type: 'string',
|
|
73
|
-
description: 'Operation: read (load persisted certificate)
|
|
74
|
-
enum: ['read', 'compute'],
|
|
75
|
+
description: 'Operation: read (load persisted certificate), compute (recompute + persist), or clear (remove the persisted certificate). Default: read.',
|
|
76
|
+
enum: ['read', 'compute', 'clear'],
|
|
75
77
|
},
|
|
76
78
|
dimensions: {
|
|
77
79
|
type: 'array',
|
|
@@ -137,6 +139,9 @@ export function registerQualityGateTool(ctx) {
|
|
|
137
139
|
if (operation === 'compute') {
|
|
138
140
|
lines.push('', 'Quality gate snapshot computed and persisted.');
|
|
139
141
|
}
|
|
142
|
+
if (operation === 'clear') {
|
|
143
|
+
lines.push('', 'Quality gate certificate cleared — the gate will be recomputed on the next iteration.');
|
|
144
|
+
}
|
|
140
145
|
return [{ type: 'text', text: lines.join('\n') }];
|
|
141
146
|
},
|
|
142
147
|
},
|
|
@@ -146,6 +151,18 @@ export function registerQualityGateTool(ctx) {
|
|
|
146
151
|
return { ok: false, kind: 'quality_gate', error: resolved.reason };
|
|
147
152
|
const projectRoot = resolved.root;
|
|
148
153
|
const operation = typeof args.operation === 'string' ? args.operation : 'read';
|
|
154
|
+
if (operation === 'clear') {
|
|
155
|
+
const result = clearQualityGate(projectRoot);
|
|
156
|
+
if (!result.ok) {
|
|
157
|
+
return { ok: false, kind: 'quality_gate', operation: 'clear', error: result.error };
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
ok: true,
|
|
161
|
+
kind: 'quality_gate',
|
|
162
|
+
operation: 'clear',
|
|
163
|
+
snapshot: readQualityGate(projectRoot),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
149
166
|
if (operation === 'compute') {
|
|
150
167
|
const dimensions = Array.isArray(args.dimensions)
|
|
151
168
|
? args.dimensions.filter((d) => typeof d === 'string' && d.length > 0)
|
|
@@ -104,6 +104,24 @@ export function writeQualityGate(projectRoot, snapshot) {
|
|
|
104
104
|
}
|
|
105
105
|
return { ok: true };
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Clear the persisted quality gate certificate (`.iterate/quality-gate.json`).
|
|
109
|
+
* Returns whether a file existed and was removed, or a structured error when
|
|
110
|
+
* the removal fails — a stale FAIL certificate must never silently persist.
|
|
111
|
+
*/
|
|
112
|
+
export function clearQualityGate(projectRoot) {
|
|
113
|
+
const filePath = path.join(projectRoot, '.iterate', QUALITY_GATE_FILE);
|
|
114
|
+
const existed = fs.existsSync(filePath);
|
|
115
|
+
if (!existed)
|
|
116
|
+
return { ok: true, existed: false };
|
|
117
|
+
try {
|
|
118
|
+
fs.rmSync(filePath, { force: true });
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
return { ok: false, error: `unable to remove ${filePath}: ${String(err)}` };
|
|
122
|
+
}
|
|
123
|
+
return { ok: true, existed: true };
|
|
124
|
+
}
|
|
107
125
|
/**
|
|
108
126
|
* Compute the convergence rate for a dimension.
|
|
109
127
|
*
|
package/dist/tools/transcript.js
CHANGED
|
@@ -331,3 +331,34 @@ async function persist(file, manifest) {
|
|
|
331
331
|
await mkdir(dirname(file), { recursive: true });
|
|
332
332
|
await writeTextAtomicAsync(file, JSON.stringify(manifest, null, 2));
|
|
333
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* Mark one applied fix as rolled back in the PERSISTED transcript, so the
|
|
336
|
+
* client observatory (F4 fix/rollback tab) reflects the reversal instead of
|
|
337
|
+
* showing the fix as still "成功" after a rollback.
|
|
338
|
+
*
|
|
339
|
+
* Best-effort and fail-safe: a missing/corrupt transcript is ignored, and a
|
|
340
|
+
* persistence failure never throws into the caller (rollback itself is the
|
|
341
|
+
* primary operation; the transcript is a secondary UI projection).
|
|
342
|
+
*
|
|
343
|
+
* @returns true when a transcript existed and was updated.
|
|
344
|
+
*/
|
|
345
|
+
export async function markFixRolledBackInTranscript(projectRoot, id) {
|
|
346
|
+
const file = transcriptPath(projectRoot);
|
|
347
|
+
if (!existsSync(file))
|
|
348
|
+
return false;
|
|
349
|
+
try {
|
|
350
|
+
const parsed = JSON.parse(await readFile(file, 'utf-8'));
|
|
351
|
+
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.fixes))
|
|
352
|
+
return false;
|
|
353
|
+
const { config } = loadEffectiveConfig(projectRoot);
|
|
354
|
+
const approval = config.observatory?.approval ?? 'ask';
|
|
355
|
+
const builder = rehydrateBuilder(parsed, approval);
|
|
356
|
+
builder.markFixRolledBack(id);
|
|
357
|
+
await persist(file, builder.serialize());
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
// Never let the secondary transcript write break the rollback flow.
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate-plugin",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.1",
|
|
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",
|
|
@@ -33,7 +33,10 @@
|
|
|
33
33
|
"0.1.2-alpha.4": "compatible",
|
|
34
34
|
"0.1.2-alpha.5": "compatible",
|
|
35
35
|
"0.1.2-rc.1": "compatible",
|
|
36
|
-
"0.1.3-alpha.1": "compatible"
|
|
36
|
+
"0.1.3-alpha.1": "compatible",
|
|
37
|
+
"0.1.5-alpha.1": "compatible",
|
|
38
|
+
"0.1.5-alpha.2": "compatible",
|
|
39
|
+
"0.1.5-rc.1": "compatible"
|
|
37
40
|
}
|
|
38
41
|
},
|
|
39
42
|
"client": {
|
package/src/live.ts
CHANGED
|
@@ -107,6 +107,11 @@ export function classifyTool(
|
|
|
107
107
|
iterate_status: 'info',
|
|
108
108
|
iterate_config: 'info',
|
|
109
109
|
iterate_context: 'info',
|
|
110
|
+
// v3.0 quality command-center tools — surface their activity in the live
|
|
111
|
+
// feed too (previously these three never produced a live entry).
|
|
112
|
+
iterate_experience: 'info',
|
|
113
|
+
iterate_quality_gate: 'info',
|
|
114
|
+
iterate_defense_events: 'info',
|
|
110
115
|
}
|
|
111
116
|
const type = records[name]
|
|
112
117
|
if (!type) return null
|
package/src/skill-prompt.ts
CHANGED
|
@@ -24,8 +24,8 @@ You have the iterate plugin installed, which registers these tools:
|
|
|
24
24
|
- \`iterate_history\` — inspect the runtime state in detail: decision-log entries and applied fixes (optionally scoped to a round or a fixed file)
|
|
25
25
|
- \`iterate_prune\` — remove stale runtime artifacts (\`.iterate/\` entries). Defaults to a read-only dry-run that reports what WOULD be removed; pass \`dryRun:false\` to actually prune.
|
|
26
26
|
- \`iterate_transcript\` — runtime observatory file (\`.iterate/transcript.json\`). \`read\` fetches the persisted manifest including any steering \`nudge\` for this run's reviewers; \`capture\` (call once after the final report) persists the per-reviewer threads, convergence trend, findings, fixes, checkpoint, and timeline so the client observatory panel reflects the run; \`nudge\` sets/clears steering text the next round's reviewers read. Purely local, never touches source files.
|
|
27
|
-
- \`iterate_experience\` — experience bank (\`.iterate/experience.json\`): \`list\`/\`search\`/\`get\` recall verified fixes and patterns from past sessions (read the bank before fixing so proven fixes are applied first); \`add\` records a new verified fix — re-adding the same pattern+dimension bumps its hit count instead of duplicating it.
|
|
28
|
-
- \`iterate_quality_gate\` — quality certificate: \`read\` loads the persisted dimension convergence rates / verification pass rate / PASS-FAIL status; \`compute\` recomputes a fresh snapshot from this round's findings + validation results (supply \`findingsByRound\` for real convergence) and persists it to \`.iterate/quality-gate.json
|
|
27
|
+
- \`iterate_experience\` — experience bank (\`.iterate/experience.json\`): \`list\`/\`search\`/\`get\` recall verified fixes and patterns from past sessions (read the bank before fixing so proven fixes are applied first); \`add\` records a new verified fix — re-adding the same pattern+dimension bumps its hit count instead of duplicating it; \`remove\` deletes a stale or incorrect entry by \`id\` so bad experiences never resurface.
|
|
28
|
+
- \`iterate_quality_gate\` — quality certificate: \`read\` loads the persisted dimension convergence rates / verification pass rate / PASS-FAIL status; \`compute\` recomputes a fresh snapshot from this round's findings + validation results (supply \`findingsByRound\` for real convergence) and persists it to \`.iterate/quality-gate.json\`; \`clear\` removes the persisted certificate to reset a stale gate before a fresh iteration.
|
|
29
29
|
- \`iterate_defense_events\` — defense event stream (\`.iterate/defense-events.json\`): \`list\`/\`counts\` review precondition failures, rollbacks, invariant violations, and falsified assumptions; \`record\` logs a new event when a defense fires. Human-readable labels follow the project \`language\` (en/zh).
|
|
30
30
|
|
|
31
31
|
### When to use
|
package/src/tools/checkpoint.ts
CHANGED
|
@@ -335,6 +335,33 @@ export function registerStatusTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
335
335
|
`Checkpoint: ${value.hasCheckpoint ? 'yes' : 'no'}${value.interrupted ? ' (interrupted — resumable)' : ''}${value.resumeCount ? ` · resumed ${value.resumeCount}x` : ''}`,
|
|
336
336
|
value.lastUpdated ? `Last updated: ${value.lastUpdated}` : '',
|
|
337
337
|
]
|
|
338
|
+
// v3.0 quality command-center summaries (surfaced here so a single
|
|
339
|
+
// status read reports the whole run state in text too).
|
|
340
|
+
const gate = value.qualityGate as QualityGateSnapshot | null | undefined
|
|
341
|
+
if (gate) {
|
|
342
|
+
lines.push(
|
|
343
|
+
`Quality gate: ${gate.overallStatus.toUpperCase()} · score ${gate.overallScore} · ` +
|
|
344
|
+
`verification ${gate.passedChecks}/${gate.totalChecks} passed` +
|
|
345
|
+
(gate.failReason ? ` · ${gate.failReason}` : ''),
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
const exp = value.experienceBank as { totalEntries?: number; totalHits?: number } | null | undefined
|
|
349
|
+
if (exp) {
|
|
350
|
+
lines.push(
|
|
351
|
+
`Experience bank: ${exp.totalEntries ?? 0} entries · ${exp.totalHits ?? 0} cumulative hits`,
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
const def = value.defenseEvents as { totalEvents?: number; counts?: Record<DefenseEventType, number> } | null | undefined
|
|
355
|
+
if (def) {
|
|
356
|
+
const counts = def.counts ?? ({} as Record<DefenseEventType, number>)
|
|
357
|
+
const total = Object.values(counts).reduce((s, n) => s + (n || 0), 0)
|
|
358
|
+
lines.push(
|
|
359
|
+
`Defense events: ${def.totalEvents ?? total} recorded` +
|
|
360
|
+
(total > 0
|
|
361
|
+
? ` (precondition_failed:${counts.precondition_failed ?? 0}, rollback:${counts.rollback ?? 0}, invariant_violated:${counts.invariant_violated ?? 0}, assumption_falsified:${counts.assumption_falsified ?? 0})`
|
|
362
|
+
: ''),
|
|
363
|
+
)
|
|
364
|
+
}
|
|
338
365
|
return [{ type: 'text', text: lines.filter(Boolean).join('\n') }]
|
|
339
366
|
},
|
|
340
367
|
},
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
10
10
|
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
|
|
11
11
|
import { resolveProjectRootForExec } from '../config-loader.ts'
|
|
12
|
-
import { readExperienceBank, writeExperienceBank, searchExperienceEntries, upsertExperience } from './experience-store.ts'
|
|
12
|
+
import { readExperienceBank, writeExperienceBank, searchExperienceEntries, upsertExperience, removeExperience } from './experience-store.ts'
|
|
13
13
|
import type { ExperienceEntryInput } from './experience-store.ts'
|
|
14
14
|
import type { ExperienceEntry } from '../types.ts'
|
|
15
15
|
|
|
@@ -81,12 +81,13 @@ export function registerExperienceBankTool(ctx: { tools: { register: (def: Retur
|
|
|
81
81
|
'List/search/get return matching entries with hit counts, verified fixes, and related context. ' +
|
|
82
82
|
'"add" upserts an experience entry into .iterate/experience.json — a repeat of the same ' +
|
|
83
83
|
'pattern+dimension increments its hit count instead of duplicating it. ' +
|
|
84
|
+
'"remove" deletes an entry by id (useful for pruning stale or incorrect experiences). ' +
|
|
84
85
|
'Use it to remember fixes that worked so future rounds apply them first.',
|
|
85
86
|
parameters: {
|
|
86
87
|
operation: {
|
|
87
88
|
type: 'string',
|
|
88
|
-
description: 'Operation: list (browse all), search (by query), get (by id), add (add a new experience). Default: list.',
|
|
89
|
-
enum: ['list', 'search', 'get', 'add'],
|
|
89
|
+
description: 'Operation: list (browse all), search (by query), get (by id), add (add a new experience), remove (delete by id). Default: list.',
|
|
90
|
+
enum: ['list', 'search', 'get', 'add', 'remove'],
|
|
90
91
|
},
|
|
91
92
|
query: {
|
|
92
93
|
type: 'string',
|
|
@@ -154,6 +155,9 @@ export function registerExperienceBankTool(ctx: { tools: { register: (def: Retur
|
|
|
154
155
|
`Tags: ${entry.tags.join(', ')}`,
|
|
155
156
|
].join('\n') }]
|
|
156
157
|
}
|
|
158
|
+
if (value.operation === 'remove') {
|
|
159
|
+
return [{ type: 'text', text: `Removed experience entry. Bank now holds ${value.count ?? 0} entries (${value.totalHits ?? 0} cumulative hits).` }]
|
|
160
|
+
}
|
|
157
161
|
if (value.operation === 'get' && value.entry) {
|
|
158
162
|
const entry = value.entry as unknown as ExperienceEntry
|
|
159
163
|
return [{ type: 'text', text: [
|
|
@@ -214,6 +218,39 @@ export function registerExperienceBankTool(ctx: { tools: { register: (def: Retur
|
|
|
214
218
|
}
|
|
215
219
|
}
|
|
216
220
|
|
|
221
|
+
if (operation === 'remove') {
|
|
222
|
+
const id = typeof args.id === 'string' && args.id ? args.id : ''
|
|
223
|
+
if (!id) {
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
kind: 'experience',
|
|
227
|
+
operation: 'remove',
|
|
228
|
+
error: 'id is required for remove',
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const bank = readExperienceBank(projectRoot)
|
|
232
|
+
const { bank: next, removed } = removeExperience(bank, id)
|
|
233
|
+
if (!removed) {
|
|
234
|
+
return {
|
|
235
|
+
ok: false,
|
|
236
|
+
kind: 'experience',
|
|
237
|
+
operation: 'remove',
|
|
238
|
+
error: `Experience not found: ${id}`,
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const write = writeExperienceBank(projectRoot, next)
|
|
242
|
+
if (!write.ok) {
|
|
243
|
+
return { ok: false, kind: 'experience', operation: 'remove', error: write.error }
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
ok: true,
|
|
247
|
+
kind: 'experience',
|
|
248
|
+
operation: 'remove',
|
|
249
|
+
count: next.entries.length,
|
|
250
|
+
totalHits: next.totalHits,
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
217
254
|
const bank = readExperienceBank(projectRoot)
|
|
218
255
|
|
|
219
256
|
if (operation === 'get' && typeof args.id === 'string') {
|
|
@@ -171,3 +171,29 @@ export function upsertExperience(
|
|
|
171
171
|
entryId: id,
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Remove an experience entry by id.
|
|
177
|
+
* Returns the resulting bank plus whether an entry was actually removed. An
|
|
178
|
+
* unknown id is a no-op (the caller decides how to surface it). Never mutates
|
|
179
|
+
* the input bank.
|
|
180
|
+
*/
|
|
181
|
+
export function removeExperience(
|
|
182
|
+
bank: ExperienceBank,
|
|
183
|
+
id: string,
|
|
184
|
+
): { bank: ExperienceBank; removed: boolean } {
|
|
185
|
+
const target = typeof id === 'string' && id ? id : ''
|
|
186
|
+
if (!target) return { bank, removed: false }
|
|
187
|
+
const entries = bank.entries.filter((e) => e.id !== target)
|
|
188
|
+
if (entries.length === bank.entries.length) return { bank, removed: false }
|
|
189
|
+
const lastUpdated = new Date().toISOString()
|
|
190
|
+
return {
|
|
191
|
+
bank: {
|
|
192
|
+
...bank,
|
|
193
|
+
entries,
|
|
194
|
+
lastUpdated,
|
|
195
|
+
totalHits: bank.totalHits ?? 0,
|
|
196
|
+
},
|
|
197
|
+
removed: true,
|
|
198
|
+
}
|
|
199
|
+
}
|
package/src/tools/fix.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { runWithJob } from '../jobs.ts'
|
|
|
27
27
|
import { countTouchedMethods } from '../method-scope.ts'
|
|
28
28
|
import { fixBackupPath, fixRegistryPath, fixesDir } from '../paths.ts'
|
|
29
29
|
import { appendDecisionEntry } from './decision-log.ts'
|
|
30
|
+
import { markFixRolledBackInTranscript } from './transcript.ts'
|
|
30
31
|
import type { FileDiffHunk, FixRecord, FixRegistry, ReviewFinding } from '../types.ts'
|
|
31
32
|
|
|
32
33
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
@@ -723,6 +724,12 @@ export function registerRollbackTool(ctx: { tools: { register: (def: ReturnType<
|
|
|
723
724
|
data: { id, file: record.finding.file, revertedDiff: record.diffSummary },
|
|
724
725
|
})
|
|
725
726
|
|
|
727
|
+
// Mirror the reversal into the persisted observatory transcript (F4) so
|
|
728
|
+
// the client no longer shows a rolled-back fix as "成功". Best-effort:
|
|
729
|
+
// a missing/corrupt transcript is left untouched and never breaks the
|
|
730
|
+
// rollback flow.
|
|
731
|
+
await markFixRolledBackInTranscript(projectRoot, id)
|
|
732
|
+
|
|
726
733
|
return { ok: true, id, file: record.finding.file }
|
|
727
734
|
},
|
|
728
735
|
}),
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
11
11
|
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
|
|
12
12
|
import { resolveProjectRootForExec } from '../config-loader.ts'
|
|
13
|
-
import { readQualityGate, writeQualityGate, computeQualityGate } from './quality-store.ts'
|
|
13
|
+
import { readQualityGate, writeQualityGate, computeQualityGate, clearQualityGate } from './quality-store.ts'
|
|
14
14
|
import type { QualityGateSnapshot } from '../types.ts'
|
|
15
15
|
|
|
16
16
|
/** Validate a single finding object; returns true when well-formed. */
|
|
@@ -72,12 +72,14 @@ export function registerQualityGateTool(ctx: { tools: { register: (def: ReturnTy
|
|
|
72
72
|
'and overall PASS/FAIL status. ' +
|
|
73
73
|
'Operation "read" (default) returns the persisted machine-readable quality certificate. ' +
|
|
74
74
|
'Operation "compute" computes a fresh snapshot from this round\'s findings/validation results, ' +
|
|
75
|
-
'persists it to .iterate/quality-gate.json, and returns it.'
|
|
75
|
+
'persists it to .iterate/quality-gate.json, and returns it. ' +
|
|
76
|
+
'Operation "clear" removes the persisted certificate (use it to reset a stale gate before ' +
|
|
77
|
+
'starting a fresh iteration).',
|
|
76
78
|
parameters: {
|
|
77
79
|
operation: {
|
|
78
80
|
type: 'string',
|
|
79
|
-
description: 'Operation: read (load persisted certificate)
|
|
80
|
-
enum: ['read', 'compute'],
|
|
81
|
+
description: 'Operation: read (load persisted certificate), compute (recompute + persist), or clear (remove the persisted certificate). Default: read.',
|
|
82
|
+
enum: ['read', 'compute', 'clear'],
|
|
81
83
|
},
|
|
82
84
|
dimensions: {
|
|
83
85
|
type: 'array',
|
|
@@ -145,6 +147,9 @@ export function registerQualityGateTool(ctx: { tools: { register: (def: ReturnTy
|
|
|
145
147
|
if (operation === 'compute') {
|
|
146
148
|
lines.push('', 'Quality gate snapshot computed and persisted.')
|
|
147
149
|
}
|
|
150
|
+
if (operation === 'clear') {
|
|
151
|
+
lines.push('', 'Quality gate certificate cleared — the gate will be recomputed on the next iteration.')
|
|
152
|
+
}
|
|
148
153
|
return [{ type: 'text', text: lines.join('\n') }]
|
|
149
154
|
},
|
|
150
155
|
},
|
|
@@ -156,6 +161,19 @@ export function registerQualityGateTool(ctx: { tools: { register: (def: ReturnTy
|
|
|
156
161
|
|
|
157
162
|
const operation = typeof args.operation === 'string' ? args.operation : 'read'
|
|
158
163
|
|
|
164
|
+
if (operation === 'clear') {
|
|
165
|
+
const result = clearQualityGate(projectRoot)
|
|
166
|
+
if (!result.ok) {
|
|
167
|
+
return { ok: false, kind: 'quality_gate', operation: 'clear', error: result.error }
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
kind: 'quality_gate',
|
|
172
|
+
operation: 'clear',
|
|
173
|
+
snapshot: readQualityGate(projectRoot) as unknown as JsonValue,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
159
177
|
if (operation === 'compute') {
|
|
160
178
|
const dimensions = Array.isArray(args.dimensions)
|
|
161
179
|
? args.dimensions.filter((d): d is string => typeof d === 'string' && d.length > 0)
|
|
@@ -116,6 +116,25 @@ export function writeQualityGate(
|
|
|
116
116
|
return { ok: true }
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Clear the persisted quality gate certificate (`.iterate/quality-gate.json`).
|
|
121
|
+
* Returns whether a file existed and was removed, or a structured error when
|
|
122
|
+
* the removal fails — a stale FAIL certificate must never silently persist.
|
|
123
|
+
*/
|
|
124
|
+
export function clearQualityGate(
|
|
125
|
+
projectRoot: string,
|
|
126
|
+
): { ok: true; existed: boolean } | { ok: false; error: string } {
|
|
127
|
+
const filePath = path.join(projectRoot, '.iterate', QUALITY_GATE_FILE)
|
|
128
|
+
const existed = fs.existsSync(filePath)
|
|
129
|
+
if (!existed) return { ok: true, existed: false }
|
|
130
|
+
try {
|
|
131
|
+
fs.rmSync(filePath, { force: true })
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return { ok: false, error: `unable to remove ${filePath}: ${String(err)}` }
|
|
134
|
+
}
|
|
135
|
+
return { ok: true, existed: true }
|
|
136
|
+
}
|
|
137
|
+
|
|
119
138
|
/**
|
|
120
139
|
* Compute the convergence rate for a dimension.
|
|
121
140
|
*
|
package/src/tools/transcript.ts
CHANGED
|
@@ -340,4 +340,36 @@ function rehydrateBuilder(manifest: TranscriptManifest, approval: 'ask' | 'deny'
|
|
|
340
340
|
async function persist(file: string, manifest: TranscriptManifest): Promise<void> {
|
|
341
341
|
await mkdir(dirname(file), { recursive: true })
|
|
342
342
|
await writeTextAtomicAsync(file, JSON.stringify(manifest, null, 2))
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Mark one applied fix as rolled back in the PERSISTED transcript, so the
|
|
347
|
+
* client observatory (F4 fix/rollback tab) reflects the reversal instead of
|
|
348
|
+
* showing the fix as still "成功" after a rollback.
|
|
349
|
+
*
|
|
350
|
+
* Best-effort and fail-safe: a missing/corrupt transcript is ignored, and a
|
|
351
|
+
* persistence failure never throws into the caller (rollback itself is the
|
|
352
|
+
* primary operation; the transcript is a secondary UI projection).
|
|
353
|
+
*
|
|
354
|
+
* @returns true when a transcript existed and was updated.
|
|
355
|
+
*/
|
|
356
|
+
export async function markFixRolledBackInTranscript(
|
|
357
|
+
projectRoot: string,
|
|
358
|
+
id: string,
|
|
359
|
+
): Promise<boolean> {
|
|
360
|
+
const file = transcriptPath(projectRoot)
|
|
361
|
+
if (!existsSync(file)) return false
|
|
362
|
+
try {
|
|
363
|
+
const parsed = JSON.parse(await readFile(file, 'utf-8')) as unknown as TranscriptManifest
|
|
364
|
+
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.fixes)) return false
|
|
365
|
+
const { config } = loadEffectiveConfig(projectRoot)
|
|
366
|
+
const approval = config.observatory?.approval ?? 'ask'
|
|
367
|
+
const builder = rehydrateBuilder(parsed, approval)
|
|
368
|
+
builder.markFixRolledBack(id)
|
|
369
|
+
await persist(file, builder.serialize())
|
|
370
|
+
return true
|
|
371
|
+
} catch {
|
|
372
|
+
// Never let the secondary transcript write break the rollback flow.
|
|
373
|
+
return false
|
|
374
|
+
}
|
|
343
375
|
}
|