iterate-plugin 3.4.1 → 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 +4 -4
- package/README.zh-CN.md +4 -4
- package/dist/atomic-fs.js +77 -0
- package/dist/config-write.js +3 -2
- package/dist/live.js +10 -4
- package/dist/skill-prompt.js +2 -2
- package/dist/tools/checkpoint.js +31 -11
- package/dist/tools/config.js +14 -0
- package/dist/tools/context.js +3 -0
- package/dist/tools/decision-log.js +25 -10
- package/dist/tools/defense-events.js +3 -0
- package/dist/tools/defense-store.js +6 -2
- package/dist/tools/experience-bank.js +42 -3
- package/dist/tools/experience-store.js +30 -2
- package/dist/tools/fix.js +40 -4
- package/dist/tools/history.js +3 -0
- package/dist/tools/prune.js +50 -6
- package/dist/tools/quality-gate.js +24 -4
- package/dist/tools/quality-store.js +20 -1
- package/dist/tools/transcript.js +38 -5
- package/dist/tools/triage.js +6 -2
- package/dist/tools/validate.js +32 -3
- package/package.json +5 -2
- package/src/atomic-fs.ts +83 -0
- package/src/config-write.ts +3 -2
- package/src/live.ts +10 -4
- package/src/skill-prompt.ts +2 -2
- package/src/tools/checkpoint.ts +37 -11
- package/src/tools/config.ts +14 -0
- package/src/tools/context.ts +3 -0
- package/src/tools/decision-log.ts +35 -10
- package/src/tools/defense-events.ts +3 -0
- package/src/tools/defense-store.ts +6 -2
- package/src/tools/experience-bank.ts +43 -3
- package/src/tools/experience-store.ts +32 -2
- package/src/tools/fix.ts +39 -4
- package/src/tools/history.ts +3 -0
- package/src/tools/prune.ts +51 -6
- package/src/tools/quality-gate.ts +25 -4
- package/src/tools/quality-store.ts +21 -1
- package/src/tools/transcript.ts +39 -5
- package/src/tools/triage.ts +6 -2
- package/src/tools/validate.ts +32 -3
- package/src/types.ts +2 -0
package/README.md
CHANGED
|
@@ -123,7 +123,7 @@ Beyond the 17 registered tools (full reference further down), the plugin closes
|
|
|
123
123
|
- **Structured fix system** — each fix backs up first, writes a registry entry, records the diff; a failed validation can be reverted with `iterate_rollback`
|
|
124
124
|
- **Breakpoint resume** — checkpoints saved at the start of each round; interrupted long iterations can resume
|
|
125
125
|
- **History audit** — `iterate_history` reads the decision log (filtered by type / time / count) and the fix-registry summary to audit run process and fix details
|
|
126
|
-
- **Runtime cleanup** — `iterate_prune` removes stale decision-log entries, stale checkpoints, orphaned fix backups and
|
|
126
|
+
- **Runtime cleanup** — `iterate_prune` removes stale decision-log entries, stale checkpoints, orphaned fix backups, empty rounds and stray temp files left by crashed atomic writes; dry-run by default (report-only), real cleanup requires `dryRun:false`, and every cleanup is logged
|
|
127
127
|
- **Config read / write** — `iterate_config` supports validated, backed-up, rollback-capable partial writes
|
|
128
128
|
- **v3.2 Experience Bank** — `iterate_experience` queries historical fixes and patterns with search / filter / adopt, and can persist new verified fixes (`add`) — re-adding the same pattern+dimension bumps its hit count instead of duplicating it
|
|
129
129
|
- **v3.2 Quality Gate** — `iterate_quality_gate` reads quality gate status with per-dimension convergence rates and PASS/FAIL, and can recompute + persist a fresh certificate (`compute`) from this round's findings / validation results (real convergence from `findingsByRound`)
|
|
@@ -258,10 +258,10 @@ validation:
|
|
|
258
258
|
- `iterate_checkpoint` — iteration breakpoint: `save` persists progress to `.iterate/checkpoint.json`, `load` reads it back, `resume` loads + bumps the resume counter (interruption recovery), `clear` removes it. Resumable interrupted long iterations
|
|
259
259
|
- `iterate_status` — summarize current iteration state: mode, current/last round, fixes applied, remaining architectural, decision-log entry count, whether a checkpoint exists; **v3.4: also surfaces the persisted quality-gate snapshot, experience-bank summary, and defense-events summary** (`qualityGate` / `experienceBank` / `defenseEvents`)
|
|
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
|
-
- `iterate_prune` — clean runtime artifacts: stale decision-log entries (by `retainDays`, default 30), stale checkpoints, orphaned fix backups, empty rounds. Dry-run by default (report-only); real cleanup with `dryRun:false`, each cleanup logged
|
|
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
|
@@ -122,7 +122,7 @@ UI 层为**防御式设计**:`slots` / `theme` / `React` 任一不可用时自
|
|
|
122
122
|
- **结构化修复系统** — 每次修复先备份、写注册表、记录 diff,验证失败可 `iterate_rollback` 还原
|
|
123
123
|
- **断点续跑** — 长迭代在每轮开头保存 checkpoint,中断后可恢复进度
|
|
124
124
|
- **历史审计** — `iterate_history` 读取决策日志(按类型/时间/数量过滤)与修复注册表汇总,审查运行过程与修复明细
|
|
125
|
-
- **运行时清理** — `iterate_prune`
|
|
125
|
+
- **运行时清理** — `iterate_prune` 清理过期的决策日志条目、陈旧断点、孤儿修复备份、空轮次与原子写入崩溃残留的临时文件;默认 dry-run 只报告不删除,显式 `dryRun:false` 才真正清理,每次清理写入决策日志
|
|
126
126
|
- **配置读写** — `iterate_config` 支持带校验、备份、回滚的局部写入
|
|
127
127
|
- **v3.2 经验银行** — `iterate_experience` 以检索 / 过滤 / 采纳查阅历史修复与模式,并可 `add` 持久化新的已验证修复——重复添加同一 pattern + dimension 时累加命中次数而非重复写入
|
|
128
128
|
- **v3.2 质量门禁** — `iterate_quality_gate` 读取质量门禁状态(各维度收敛率 + PASS/FAIL),并可基于本轮 findings / 验证结果 `compute` 重新计算并持久化一份新的质量凭证(收敛率来自 `findingsByRound` 的真实收敛序列)
|
|
@@ -257,10 +257,10 @@ validation:
|
|
|
257
257
|
- `iterate_checkpoint` — 迭代断点:`save` 保存当前进度到 `.iterate/checkpoint.json`,`load` 读回,`resume` 加载并累加恢复计数(中断恢复),`clear` 清除。长迭代可中断续跑
|
|
258
258
|
- `iterate_status` — 汇总当前迭代状态:模式、当前轮/总轮、已修复数、剩余 architectural、决策日志条数、是否存在 checkpoint;**v3.4:同时返回持久化的质量门禁快照、经验银行摘要与防御事件摘要**(`qualityGate` / `experienceBank` / `defenseEvents`)
|
|
259
259
|
- `iterate_history` — 读取迭代历史(只读):决策日志条目(可按 `type` / `since` / `limit` 过滤,默认取最新 50 条,上限 200 条)+ 修复注册表汇总(各轮 fixed/failed 计数)。用于审查运行过程、审计日志、盘点修复
|
|
260
|
-
- `iterate_prune` — 清理运行时产物:过期决策日志条目(按 `retainDays`,默认 30
|
|
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
|
---
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/atomic-fs.ts — crash-safe file writes for all plugin state.
|
|
3
|
+
*
|
|
4
|
+
* Every JSON/YAML artifact the plugin persists (.iterate/*.json,
|
|
5
|
+
* iterate.config.yaml) is written through here: content goes to a
|
|
6
|
+
* uniquely-named temp file in the SAME directory as the target (rename is
|
|
7
|
+
* only atomic within one filesystem), then is `renameSync`ed over the
|
|
8
|
+
* destination. A crash mid-write can therefore leave a stale temp file
|
|
9
|
+
* behind, but never a truncated / half-written state file — readers
|
|
10
|
+
* (which all tolerate missing/corrupt files) see either the previous or
|
|
11
|
+
* the new content, never a mixture.
|
|
12
|
+
*/
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
import { renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { rename, rm, writeFile } from 'node:fs/promises';
|
|
16
|
+
/** Best-effort unlink of a leftover temp file (never throws). */
|
|
17
|
+
function cleanupTemp(tmp) {
|
|
18
|
+
try {
|
|
19
|
+
rmSync(tmp, { force: true });
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
/* ignore — temp files follow the prunable `.tmp-<pid>-<rand>` convention */
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Best-effort async unlink of a leftover temp file (never throws). */
|
|
26
|
+
async function cleanupTempAsync(tmp) {
|
|
27
|
+
try {
|
|
28
|
+
await rm(tmp, { force: true });
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
/* ignore — temp files follow the prunable `.tmp-<pid>-<rand>` convention */
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Unique temp file path for `filePath`, in the same directory, following the
|
|
36
|
+
* prunable `.<basename>.tmp-<pid>-<random>` convention (see prune.ts
|
|
37
|
+
* `isPrunableTemp`). The pid+random suffix makes concurrent writers safe.
|
|
38
|
+
*/
|
|
39
|
+
function tempPathFor(filePath) {
|
|
40
|
+
return join(dirname(filePath), `.${filePath.split('/').pop() ?? 'file'}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Write `data` to `filePath` atomically (temp file + rename, same dir).
|
|
44
|
+
* Temp files follow the `<name>.tmp-<pid>-<random>` convention also used by
|
|
45
|
+
* prune.ts / checkpoint.ts so they are recognizable and prunable.
|
|
46
|
+
* Throws on failure (callers own error reporting — they map failures to
|
|
47
|
+
* structured tool results).
|
|
48
|
+
*/
|
|
49
|
+
export function writeTextAtomic(filePath, data) {
|
|
50
|
+
const tmp = tempPathFor(filePath);
|
|
51
|
+
writeFileSync(tmp, data, 'utf-8');
|
|
52
|
+
try {
|
|
53
|
+
renameSync(tmp, filePath);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
// Best-effort temp cleanup so a failed write never litters the directory.
|
|
57
|
+
cleanupTemp(tmp);
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Async variant of {@link writeTextAtomic} (same temp naming convention). */
|
|
62
|
+
export async function writeTextAtomicAsync(filePath, data) {
|
|
63
|
+
const tmp = tempPathFor(filePath);
|
|
64
|
+
await writeFile(tmp, data, 'utf-8');
|
|
65
|
+
try {
|
|
66
|
+
await rename(tmp, filePath);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
// Best-effort temp cleanup so a failed write never litters the directory.
|
|
70
|
+
await cleanupTempAsync(tmp);
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** JSON.stringify (2-space indent) + atomic write in one call. */
|
|
75
|
+
export function writeJsonAtomic(filePath, value) {
|
|
76
|
+
writeTextAtomic(filePath, JSON.stringify(value, null, 2));
|
|
77
|
+
}
|
package/dist/config-write.js
CHANGED
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* The security posture mirrors the triage tool: never overwrite a malformed
|
|
10
10
|
* config, always back up before writing, roll back on failure.
|
|
11
11
|
*/
|
|
12
|
-
import { copyFileSync, existsSync, readFileSync, rmSync
|
|
12
|
+
import { copyFileSync, existsSync, readFileSync, rmSync } from 'node:fs';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import yaml from 'js-yaml';
|
|
15
|
+
import { writeTextAtomic } from "./atomic-fs.js";
|
|
15
16
|
/** Config file name (must match config-loader). */
|
|
16
17
|
export const CONFIG_FILE = 'iterate.config.yaml';
|
|
17
18
|
/** Backup suffix helper (filesystem-safe timestamp). */
|
|
@@ -185,7 +186,7 @@ export function writeConfigFile(projectRoot, config) {
|
|
|
185
186
|
}
|
|
186
187
|
}
|
|
187
188
|
try {
|
|
188
|
-
|
|
189
|
+
writeTextAtomic(configPath, yaml.dump(config, { noRefs: true }));
|
|
189
190
|
}
|
|
190
191
|
catch (err) {
|
|
191
192
|
let rollbackError = '';
|
package/dist/live.js
CHANGED
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
* - Any capture failure is swallowed (fire-and-forget) so it can never block
|
|
24
24
|
* or crash a tool call.
|
|
25
25
|
*/
|
|
26
|
-
import { mkdir, readFile,
|
|
26
|
+
import { mkdir, readFile, stat, appendFile } from 'node:fs/promises';
|
|
27
27
|
import { existsSync } from 'node:fs';
|
|
28
|
+
import { writeTextAtomicAsync } from "./atomic-fs.js";
|
|
28
29
|
import { join } from 'node:path';
|
|
29
30
|
import { resolveProjectRoot } from "./config-loader.js";
|
|
30
31
|
/** Keep at most this many live activity entries. */
|
|
@@ -68,6 +69,11 @@ export function classifyTool(name, args, projectRoot) {
|
|
|
68
69
|
iterate_status: 'info',
|
|
69
70
|
iterate_config: 'info',
|
|
70
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',
|
|
71
77
|
};
|
|
72
78
|
const type = records[name];
|
|
73
79
|
if (!type)
|
|
@@ -101,9 +107,9 @@ export async function appendLive(projectRoot, entry) {
|
|
|
101
107
|
const raw = await readFile(file, 'utf-8');
|
|
102
108
|
const lines = raw.split('\n').filter(Boolean);
|
|
103
109
|
const tail = lines.slice(-LIVE_MAX_ENTRIES);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
await
|
|
110
|
+
// Atomic rewrite (unique temp + rename) so a crash mid-trim never
|
|
111
|
+
// truncates the live feed; the temp file is prunable by iterate_prune.
|
|
112
|
+
await writeTextAtomicAsync(file, tail.join('\n') + '\n');
|
|
107
113
|
}
|
|
108
114
|
await appendFile(file, line, 'utf-8');
|
|
109
115
|
}
|
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
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Checkpoint layout: `.iterate/checkpoint.json`.
|
|
10
10
|
*/
|
|
11
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
12
12
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
13
13
|
import { resolveProjectRootForExec } from "../config-loader.js";
|
|
14
|
+
import { writeJsonAtomic } from "../atomic-fs.js";
|
|
14
15
|
import { checkpointPath, iterateDir, transcriptPath } from "../paths.js";
|
|
15
16
|
import { readRegistry } from "./fix.js";
|
|
16
17
|
import { readDecisionEntries } from "./decision-log.js";
|
|
@@ -192,10 +193,7 @@ export function registerCheckpointTool(ctx) {
|
|
|
192
193
|
};
|
|
193
194
|
try {
|
|
194
195
|
mkdirSync(iterateDir(projectRoot), { recursive: true });
|
|
195
|
-
|
|
196
|
-
const tmpPath = `${cpPath}.tmp-${Date.now()}`;
|
|
197
|
-
writeFileSync(tmpPath, JSON.stringify(resumed, null, 2), 'utf-8');
|
|
198
|
-
renameSync(tmpPath, cpPath);
|
|
196
|
+
writeJsonAtomic(checkpointPath(projectRoot), resumed);
|
|
199
197
|
}
|
|
200
198
|
catch (err) {
|
|
201
199
|
return { operation: 'resume', ok: false, error: `failed to persist resumed checkpoint: ${String(err)}` };
|
|
@@ -238,12 +236,10 @@ export function registerCheckpointTool(ctx) {
|
|
|
238
236
|
};
|
|
239
237
|
try {
|
|
240
238
|
mkdirSync(iterateDir(projectRoot), { recursive: true });
|
|
241
|
-
// Atomic write (temp + rename): a crash
|
|
242
|
-
// the checkpoint and silently lose the
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
writeFileSync(tmpPath, JSON.stringify(checkpoint, null, 2), 'utf-8');
|
|
246
|
-
renameSync(tmpPath, cpPath);
|
|
239
|
+
// Atomic write (temp + rename, via writeJsonAtomic): a crash
|
|
240
|
+
// mid-write must not corrupt the checkpoint and silently lose the
|
|
241
|
+
// interruption state.
|
|
242
|
+
writeJsonAtomic(checkpointPath(projectRoot), checkpoint);
|
|
247
243
|
}
|
|
248
244
|
catch (err) {
|
|
249
245
|
return { operation: 'save', ok: false, error: `failed to write checkpoint: ${String(err)}` };
|
|
@@ -262,6 +258,9 @@ export function registerCheckpointTool(ctx) {
|
|
|
262
258
|
export function registerStatusTool(ctx) {
|
|
263
259
|
ctx.tools.register(defineTool({
|
|
264
260
|
name: 'iterate_status',
|
|
261
|
+
// Read-only aggregation of on-disk state → safe to join a parallel
|
|
262
|
+
// dispatch group alongside other read-only sibling calls.
|
|
263
|
+
isConcurrencySafe: () => true,
|
|
265
264
|
description: 'Summarize the current iterate run: mode, current round vs total, fixes applied, architectural ' +
|
|
266
265
|
'findings remaining, decision-log size, and whether a resume checkpoint exists.',
|
|
267
266
|
parameters: {
|
|
@@ -303,6 +302,27 @@ export function registerStatusTool(ctx) {
|
|
|
303
302
|
`Checkpoint: ${value.hasCheckpoint ? 'yes' : 'no'}${value.interrupted ? ' (interrupted — resumable)' : ''}${value.resumeCount ? ` · resumed ${value.resumeCount}x` : ''}`,
|
|
304
303
|
value.lastUpdated ? `Last updated: ${value.lastUpdated}` : '',
|
|
305
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
|
+
}
|
|
306
326
|
return [{ type: 'text', text: lines.filter(Boolean).join('\n') }];
|
|
307
327
|
},
|
|
308
328
|
},
|
package/dist/tools/config.js
CHANGED
|
@@ -11,6 +11,20 @@ import { applyConfigUpdates, readRawConfig, validateConfigUpdates, writeConfigFi
|
|
|
11
11
|
export function registerConfigTool(ctx) {
|
|
12
12
|
ctx.tools.register(defineTool({
|
|
13
13
|
name: 'iterate_config',
|
|
14
|
+
// `read` never writes; `write` persists a config update → only read
|
|
15
|
+
// joins a parallel dispatch group.
|
|
16
|
+
isConcurrencySafe: (args) => args.operation !== 'write',
|
|
17
|
+
// Pending-call card: a config read vs a validated write.
|
|
18
|
+
presentCall: (args) => {
|
|
19
|
+
const a = args;
|
|
20
|
+
const isWrite = a.operation === 'write';
|
|
21
|
+
return {
|
|
22
|
+
card: 'generic',
|
|
23
|
+
title: isWrite ? 'Update iterate.config.yaml' : 'Read iterate.config.yaml',
|
|
24
|
+
kind: isWrite ? 'edit' : 'read',
|
|
25
|
+
...(typeof a.section === 'string' && a.section ? { rawInput: { section: a.section } } : {}),
|
|
26
|
+
};
|
|
27
|
+
},
|
|
14
28
|
description: 'Read or update the iterate.config.yaml configuration from the project root. ' +
|
|
15
29
|
'Returns the full parsed config, a specific section, or validation errors. ' +
|
|
16
30
|
'Use this to discover available dimensions, validation commands, git settings, and personalization rules, ' +
|
package/dist/tools/context.js
CHANGED
|
@@ -161,6 +161,9 @@ export { findSkillMd };
|
|
|
161
161
|
export function registerContextTool(ctx) {
|
|
162
162
|
ctx.tools.register(defineTool({
|
|
163
163
|
name: 'iterate_context',
|
|
164
|
+
// Read-only (reads SKILL.md / ITERATE.md) → safe to join a parallel
|
|
165
|
+
// dispatch group alongside other read-only sibling calls.
|
|
166
|
+
isConcurrencySafe: () => true,
|
|
164
167
|
description: 'Read project context files (SKILL.md and/or ITERATE.md). ' +
|
|
165
168
|
'SKILL.md contains the original iterate skill instructions; it is searched in ' +
|
|
166
169
|
'the skill directory (auto-detected), the project root, or an explicit `skillDir`. ' +
|
|
@@ -71,34 +71,44 @@ export function appendDecisionEntry(projectRoot, entry) {
|
|
|
71
71
|
return { count, path: filePath };
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
|
-
* Read all entries from the decision log.
|
|
74
|
+
* Read all entries from the decision log, plus the count of corrupt lines.
|
|
75
75
|
* A single corrupt line (partial write, hand-edit) is SKIPPED, not fatal —
|
|
76
|
-
* one bad line must never empty the whole history for every reader
|
|
76
|
+
* one bad line must never empty the whole history for every reader — but it
|
|
77
|
+
* is counted in `invalidLines` so it never disappears silently.
|
|
77
78
|
*/
|
|
78
|
-
export function
|
|
79
|
+
export function readDecisionLogDetailed(projectRoot) {
|
|
79
80
|
const filePath = join(projectRoot, LOG_DIR, LOG_FILE);
|
|
80
81
|
if (!existsSync(filePath))
|
|
81
|
-
return [];
|
|
82
|
+
return { entries: [], invalidLines: 0 };
|
|
82
83
|
let content;
|
|
83
84
|
try {
|
|
84
85
|
content = readFileSync(filePath, 'utf-8');
|
|
85
86
|
}
|
|
86
87
|
catch {
|
|
87
|
-
return [];
|
|
88
|
+
return { entries: [], invalidLines: 0 };
|
|
88
89
|
}
|
|
89
|
-
const
|
|
90
|
+
const entries = [];
|
|
91
|
+
let invalidLines = 0;
|
|
90
92
|
for (const line of content.split('\n')) {
|
|
91
93
|
const trimmed = line.trim();
|
|
92
94
|
if (trimmed.length === 0)
|
|
93
95
|
continue;
|
|
94
96
|
try {
|
|
95
|
-
|
|
97
|
+
entries.push(JSON.parse(trimmed));
|
|
96
98
|
}
|
|
97
99
|
catch {
|
|
98
|
-
|
|
100
|
+
invalidLines += 1;
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
|
-
return
|
|
103
|
+
return { entries, invalidLines };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Read all entries from the decision log.
|
|
107
|
+
* A single corrupt line (partial write, hand-edit) is SKIPPED, not fatal —
|
|
108
|
+
* one bad line must never empty the whole history for every reader.
|
|
109
|
+
*/
|
|
110
|
+
export function readDecisionEntries(projectRoot) {
|
|
111
|
+
return readDecisionLogDetailed(projectRoot).entries;
|
|
102
112
|
}
|
|
103
113
|
/**
|
|
104
114
|
* Register the `iterate_decision_log` tool.
|
|
@@ -108,6 +118,9 @@ export function readDecisionEntries(projectRoot) {
|
|
|
108
118
|
export function registerDecisionLogTool(ctx) {
|
|
109
119
|
ctx.tools.register(defineTool({
|
|
110
120
|
name: 'iterate_decision_log',
|
|
121
|
+
// `read` never writes; `append` extends the audit log → only read joins
|
|
122
|
+
// a parallel dispatch group.
|
|
123
|
+
isConcurrencySafe: (args) => args.operation === 'read',
|
|
111
124
|
description: 'Append-only decision log for the iterate loop. ' +
|
|
112
125
|
'Use `append` to record a round start, review finding, fix, validation result, or decision. ' +
|
|
113
126
|
'Use `read` to retrieve all entries for review. ' +
|
|
@@ -156,6 +169,7 @@ export function registerDecisionLogTool(ctx) {
|
|
|
156
169
|
properties: {
|
|
157
170
|
operation: { type: 'string', required: true },
|
|
158
171
|
entryCount: { type: 'integer' },
|
|
172
|
+
invalidLines: { type: 'integer' },
|
|
159
173
|
logPath: { type: 'string' },
|
|
160
174
|
entries: { type: 'json' },
|
|
161
175
|
success: { type: 'boolean' },
|
|
@@ -174,10 +188,11 @@ export function registerDecisionLogTool(ctx) {
|
|
|
174
188
|
}
|
|
175
189
|
const projectRoot = resolved.root;
|
|
176
190
|
if (args.operation === 'read') {
|
|
177
|
-
const entries =
|
|
191
|
+
const { entries, invalidLines } = readDecisionLogDetailed(projectRoot);
|
|
178
192
|
return {
|
|
179
193
|
operation: 'read',
|
|
180
194
|
entryCount: entries.length,
|
|
195
|
+
invalidLines,
|
|
181
196
|
logPath: join(projectRoot, LOG_DIR, LOG_FILE),
|
|
182
197
|
entries: entries,
|
|
183
198
|
};
|
|
@@ -72,6 +72,9 @@ function validateRecordInput(args) {
|
|
|
72
72
|
export function registerDefenseEventsTool(ctx) {
|
|
73
73
|
ctx.tools.register(defineTool({
|
|
74
74
|
name: 'iterate_defense_events',
|
|
75
|
+
// List/counts never write; only `record` persists an event → read shapes
|
|
76
|
+
// join a parallel dispatch group.
|
|
77
|
+
isConcurrencySafe: (args) => args.operation !== 'record',
|
|
75
78
|
description: 'Query or record defense events: precondition failures, rollbacks, invariant violations, ' +
|
|
76
79
|
'and assumption falsifications. ' +
|
|
77
80
|
'List/counts return events with descriptions, outcomes, and summary counts; ' +
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
|
+
import { iterateDir } from "../paths.js";
|
|
10
|
+
import { writeJsonAtomic } from "../atomic-fs.js";
|
|
9
11
|
const DEFENSE_EVENTS_FILE = 'defense-events.json';
|
|
10
12
|
/** Valid defense event types (must stay in sync with DefenseEventType). */
|
|
11
13
|
const VALID_EVENT_TYPES = new Set([
|
|
@@ -70,13 +72,15 @@ export function readDefenseEvents(projectRoot) {
|
|
|
70
72
|
* an event that was never persisted.
|
|
71
73
|
*/
|
|
72
74
|
export function writeDefenseEvents(projectRoot, stream) {
|
|
73
|
-
const dirPath =
|
|
75
|
+
const dirPath = iterateDir(projectRoot);
|
|
74
76
|
const filePath = path.join(dirPath, DEFENSE_EVENTS_FILE);
|
|
75
77
|
try {
|
|
76
78
|
if (!fs.existsSync(dirPath)) {
|
|
77
79
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
78
80
|
}
|
|
79
|
-
|
|
81
|
+
// Atomic (temp + rename): a crash mid-write can never leave a truncated
|
|
82
|
+
// defense event stream behind.
|
|
83
|
+
writeJsonAtomic(filePath, stream);
|
|
80
84
|
}
|
|
81
85
|
catch (err) {
|
|
82
86
|
return { ok: false, error: `unable to write ${filePath}: ${String(err)}` };
|
|
@@ -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. */
|
|
@@ -67,17 +67,21 @@ function normalizeExperienceInput(raw) {
|
|
|
67
67
|
export function registerExperienceBankTool(ctx) {
|
|
68
68
|
ctx.tools.register(defineTool({
|
|
69
69
|
name: 'iterate_experience',
|
|
70
|
+
// List/search/get never write; only `add` upserts the bank → read shapes
|
|
71
|
+
// join a parallel dispatch group.
|
|
72
|
+
isConcurrencySafe: (args) => args.operation !== 'add',
|
|
70
73
|
description: 'Query or extend the experience bank: browse/search historical fixes and patterns, ' +
|
|
71
74
|
'or record a new verified fix (operation:"add"). ' +
|
|
72
75
|
'List/search/get return matching entries with hit counts, verified fixes, and related context. ' +
|
|
73
76
|
'"add" upserts an experience entry into .iterate/experience.json — a repeat of the same ' +
|
|
74
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). ' +
|
|
75
79
|
'Use it to remember fixes that worked so future rounds apply them first.',
|
|
76
80
|
parameters: {
|
|
77
81
|
operation: {
|
|
78
82
|
type: 'string',
|
|
79
|
-
description: 'Operation: list (browse all), search (by query), get (by id), add (add a new experience). Default: list.',
|
|
80
|
-
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'],
|
|
81
85
|
},
|
|
82
86
|
query: {
|
|
83
87
|
type: 'string',
|
|
@@ -144,6 +148,9 @@ export function registerExperienceBankTool(ctx) {
|
|
|
144
148
|
`Tags: ${entry.tags.join(', ')}`,
|
|
145
149
|
].join('\n') }];
|
|
146
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
|
+
}
|
|
147
154
|
if (value.operation === 'get' && value.entry) {
|
|
148
155
|
const entry = value.entry;
|
|
149
156
|
return [{ type: 'text', text: [
|
|
@@ -201,6 +208,38 @@ export function registerExperienceBankTool(ctx) {
|
|
|
201
208
|
totalHits: next.totalHits,
|
|
202
209
|
};
|
|
203
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
|
+
}
|
|
204
243
|
const bank = readExperienceBank(projectRoot);
|
|
205
244
|
if (operation === 'get' && typeof args.id === 'string') {
|
|
206
245
|
const entry = bank.entries.find((e) => e.id === args.id);
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
|
+
import { iterateDir } from "../paths.js";
|
|
10
|
+
import { writeJsonAtomic } from "../atomic-fs.js";
|
|
9
11
|
const EXPERIENCE_FILE = 'experience.json';
|
|
10
12
|
/** Default empty experience bank. */
|
|
11
13
|
function emptyBank() {
|
|
@@ -37,13 +39,15 @@ export function readExperienceBank(projectRoot) {
|
|
|
37
39
|
* an entry that was never persisted.
|
|
38
40
|
*/
|
|
39
41
|
export function writeExperienceBank(projectRoot, bank) {
|
|
40
|
-
const dirPath =
|
|
42
|
+
const dirPath = iterateDir(projectRoot);
|
|
41
43
|
const filePath = path.join(dirPath, EXPERIENCE_FILE);
|
|
42
44
|
try {
|
|
43
45
|
if (!fs.existsSync(dirPath)) {
|
|
44
46
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
45
47
|
}
|
|
46
|
-
|
|
48
|
+
// Atomic (temp + rename): a crash mid-write can never leave a truncated
|
|
49
|
+
// experience bank behind.
|
|
50
|
+
writeJsonAtomic(filePath, bank);
|
|
47
51
|
}
|
|
48
52
|
catch (err) {
|
|
49
53
|
return { ok: false, error: `unable to write ${filePath}: ${String(err)}` };
|
|
@@ -136,3 +140,27 @@ export function upsertExperience(bank, entry) {
|
|
|
136
140
|
entryId: id,
|
|
137
141
|
};
|
|
138
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
|
+
}
|