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 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), or `add` a new verified fix: re-adding the same pattern+dimension bumps its hit count instead of duplicating it. 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
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 累加命中次数而非重复写入。持久化到 `.iterate/experience.json`
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)
@@ -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
@@ -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) or compute (recompute + persist). Default: read.',
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
  *
@@ -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.0",
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
@@ -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
@@ -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) or compute (recompute + persist). Default: read.',
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
  *
@@ -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
  }