iterate-plugin 2.5.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,10 +2,12 @@
2
2
 
3
3
  > **开发与评审在 [iterate-skill 主仓库](https://github.com/jingzhao-l/iterate-skill) 完成**:插件代码由主仓库统一维护,通过 `git subtree` 同步到本仓库;**版本发版与 npm 发布在本仓库(插件仓库)进行**,作为 dsh 生态的正式发布位。欢迎 **star / fork 主仓库** 并在 [主仓库 Issues](https://github.com/jingzhao-l/iterate-skill/issues) 反馈问题。
4
4
 
5
- `iterate-plugin` 是 [iterate](https://github.com/iterate-skill/iterate-skill) 技能的 [DeepSeek Harness (dsh)](https://github.com/deepseek-ai/deepseek-harness) 插件,提供**自治闭环代码迭代**和**dry-run 纯多轮审查**能力。
5
+ `iterate-plugin` 是 [iterate](https://github.com/jingzhao-l/iterate-skill) 技能的 [DeepSeek Harness (dsh)](https://github.com/deepseek-ai/deepseek-harness) 插件,提供**自治闭环代码迭代**和 **dry-run 纯多轮审查**能力。除 13 个纯函数工具外,还内置一套**免构建的 Web UI 层**(分诊面板、收敛看板、统计卡片、主题皮肤等),直接挂在 dsh 客户端的既有 UI 槽位上。
6
6
 
7
7
  ## 特性
8
8
 
9
+ ### 两种运行模式
10
+
9
11
  | 功能 | dry-run 模式 | normal 模式 |
10
12
  |------|-------------|------------|
11
13
  | 多轮收敛反复审查 | ✅ | ✅ |
@@ -15,12 +17,37 @@
15
17
  | 零文件修改(只读) | ✅ | ❌ |
16
18
  | 原子问题自动修复 | ❌ | ✅ |
17
19
  | 每轮修复后验证 | ❌ | ✅ |
20
+ | 修复失败回滚 | ❌ | ✅ |
18
21
  | 达标自停 | ✅ | ✅ |
19
22
  | 只修改 atomic 问题,保留 architectural 留待后续 | ❌ | ✅ |
23
+ | 断点保存 / 恢复(长迭代续跑) | ✅ | ✅ |
24
+
25
+ ### 工具层
26
+
27
+ - **13 个注册工具**:`iterate_config` / `iterate_validate` / `iterate_decision_log` / `iterate_context` / `iterate_review` / `iterate_triage` / `iterate_fix` / `iterate_diff` / `iterate_rollback` / `iterate_checkpoint` / `iterate_status` / `iterate_history` / `iterate_prune`
28
+ - **findings 分诊闭环**:审查 → UI 分诊(y/n/a)→ `iterate_triage` 写回 `known_intentional` → 下一轮自动过滤
29
+ - **结构化修复系统**:每次修复先备份、写注册表、记录 diff,验证失败可 `iterate_rollback` 还原
30
+ - **断点续跑**:长迭代在每轮开头保存 checkpoint,中断后可恢复进度
31
+ - **历史审计**:`iterate_history` 读取决策日志(按类型/时间/数量过滤)与修复注册表汇总,审查运行过程与修复明细
32
+ - **运行时清理**:`iterate_prune` 清理过期的决策日志条目、陈旧断点、孤儿修复备份与空轮次;默认 dry-run 只报告不删除,显式 `dryRun:false` 才真正清理,每次清理写入决策日志
33
+ - **配置读写**:`iterate_config` 支持带校验、备份、回滚的局部写入
34
+
35
+ ### UI 层(客户端免构建槽位)
36
+
37
+ | UI 组件 | 挂载槽位 | 功能 |
38
+ |---------|---------|------|
39
+ | 收敛看板 `ConvergenceDashboard` | `conversation.input.dock` | 输入框上方实时显示轮次进度条、严重度统计、维度徽章、趋势迷你图,normal 模式另显示修复计数徽章 |
40
+ | Findings 分诊面板 `TriagePanel` | `conversation.chat.turnTail` | 逐条 y/n/a 判定,支持筛选、批量(含一键全选所有 findings)、键盘快捷键、localStorage 持久化、复制 YAML/应用指令 |
41
+ | 收敛统计卡片 `StatsCard` | `conversation.chat.turnTail` | 无 findings 时显示收敛统计、历史轮次表、趋势图、完成摘要 |
42
+ | iterate 主题皮肤 | `theme.overrideTokens` | 暖琥珀配色的 13 个 dsw token 覆盖,明暗双模式,可在设置页开关 |
43
+ | 进度胶囊 `ProgressCapsule` | `shell.overlay` | 每轮完成/收敛时右下角弹出通知(含收敛确认) |
44
+ | iterate 设置区 `SettingsPanel` | `settings.section` | 主题开关、分诊持久化说明、配置管理指引、运行时状态概览(产物布局 + 查看/清理工具指引)、一键清空分诊数据 |
45
+
46
+ UI 层为**防御式设计**:`slots` / `theme` / `React` 任一不可用时自动降级,不会崩溃客户端。
20
47
 
21
48
  ## 安装
22
49
 
23
- ### 从 npm 安装(发布后)
50
+ ### 从 npm 安装
24
51
 
25
52
  ```bash
26
53
  dsh plugin --profile web add iterate-plugin
@@ -28,10 +55,20 @@ dsh plugin --profile web add iterate-plugin
28
55
  pnpm add iterate-plugin
29
56
  ```
30
57
 
58
+ ### 从 GitHub 安装(dsh 生态第三方安装方式)
59
+
60
+ dsh 官方支持从 GitHub 插件仓库直接安装:`dsh plugin --profile web add "github:owner/repo#ref"`(仓库根即插件,声明 `dsh.bundle` 后自动启用)。本插件在 [iterate-plugin 独立仓库](https://github.com/jingzhao-l/iterate-plugin) 维护仓库根即插件的发布位,由主仓库通过 `git subtree` 同步,内容与 npm 包一致:
61
+
62
+ ```bash
63
+ dsh plugin --profile web add "github:jingzhao-l/iterate-plugin#main"
64
+ ```
65
+
66
+ 安装完成后需重启 dsh 服务(建议 `dsh web --patch`)并刷新页面,宿主与客户端 UI 层才会加载。
67
+
31
68
  ### 本地开发 / 源码挂载
32
69
 
33
70
  ```bash
34
- dsh plugin --profile web add /Volumes/Eng-Dev/iterate-skill/harness/iterate-plugin
71
+ dsh plugin --profile web add /path/to/iterate-skill/harness/iterate-plugin
35
72
  # 或
36
73
  pnpm add /path/to/iterate-skill/harness/iterate-plugin
37
74
  ```
@@ -44,6 +81,8 @@ pnpm add /path/to/iterate-skill/harness/iterate-plugin
44
81
  name: 'iterate-plugin'
45
82
  ```
46
83
 
84
+ > 插件包自带 `dsh.bundle.patch`(即 `cordis.patch.yml`),npm 包内 `files` 已白名单化(`src` / `lib` / `cordis.patch.yml` / `README.md` / `LICENSE`)。
85
+
47
86
  ## 使用
48
87
 
49
88
  ### dry-run 模式(纯反复审查,不修改文件)
@@ -55,6 +94,7 @@ dry-run review this project, find all issues across all dimensions
55
94
  ```
56
95
 
57
96
  插件会自动触发 iterate 工作流:
97
+
58
98
  1. `plan` → 读取配置,生成评审计划
59
99
  2. `loop` → 每轮并行评审,只找新问题 → 确定性聚合去重 → 统计收敛 → 无新问题则停止
60
100
  3. `meta-review` → 审计报告一致性
@@ -69,8 +109,9 @@ iterate on this project, fix all atomic issues
69
109
  ```
70
110
 
71
111
  工作流:
112
+
72
113
  1. `plan` → 读取配置
73
- 2. `loop` → 并行评审 → 聚合去重 → 原子问题并行修复 → 执行验证命令 → 记录日志 → 无新问题则停止
114
+ 2. `loop` → 并行评审 → 聚合去重 → 原子问题并行修复 → 执行验证命令 → 验证失败则回滚 → 记录日志 → 无新问题则停止
74
115
  3. `report` → 输出修复统计
75
116
 
76
117
  ## 项目配置
@@ -92,6 +133,9 @@ max_rounds: 3
92
133
  # 评审范围
93
134
  review:
94
135
  scope: full # full = 全项目,changed-only = 只看变更文件
136
+ # 原子修复阈值(单次修复允许改动的最大行数,超过需 force)
137
+ atomic:
138
+ max_lines: 20
95
139
  # 已知故意不修复的问题(评审会过滤掉,不再重复报告)
96
140
  personalization:
97
141
  known_intentional:
@@ -106,24 +150,48 @@ validation:
106
150
  - npm run typecheck
107
151
  ```
108
152
 
109
- ## 注册工具
153
+ > 配置可通过 `iterate_config` 工具读取与**校验式局部写入**(自动备份,写入失败自动回滚)。
110
154
 
111
- 插件注册了 5 个工具:
155
+ ## 注册工具(13 个)
112
156
 
113
157
  | 工具 | 功能 |
114
158
  |------|------|
115
- | `iterate_config` | 读取并验证 `iterate.config.yaml` |
159
+ | `iterate_config` | 读取 / 写入 `iterate.config.yaml`。`operation=read` 返回完整配置或指定 section;`operation=write` 做 schema 校验、备份后局部合并写入,失败自动回滚 |
116
160
  | `iterate_validate` | 运行白名单验证命令,返回结果 |
117
- | `iterate_decision_log` | 追加决策日志(只追加,不改旧) |
161
+ | `iterate_decision_log` | 追加决策日志(只追加,不改旧),存储于 `.iterate/decision-log.jsonl` |
118
162
  | `iterate_context` | 读取 `SKILL.md` / `ITERATE.md` 上下文 |
119
- | `iterate_review` | 确定性评审引擎:`plan` 生成计划,`aggregate` 聚合结论,`meta-review` 审计报告 |
163
+ | `iterate_review` | 确定性评审引擎:`plan` 生成计划,`aggregate` 聚合去重 + 收敛统计,`meta-review` 审计报告一致性。纯计算,不触碰文件系统 |
164
+ | `iterate_triage` | 管理 `personalization.known_intentional`:`apply` 校验、去重(file\|dimension\|line)、备份后写回配置;`list` 读回当前条目。是浏览器分诊面板写回配置的唯一通道 |
165
+ | `iterate_fix` | 应用**一个原子修复**:校验相对路径、备份原文件、按 `atomic.max_lines` 强制原子性(可 `force` 跳过)、写入新内容、记录 FixRecord 与 `atomic_fix` 日志。normal 模式唯一合法的改文件入口 |
166
+ | `iterate_diff` | 查看修复累积变更:指定 `file` 返回相对首个备份的 unified diff;省略则返回每个已修复文件的汇总 |
167
+ | `iterate_rollback` | 回滚一个已应用的修复:从备份还原文件、从注册表移除该 FixRecord、追加 `revert` 日志。用于某轮验证失败后 |
168
+ | `iterate_checkpoint` | 迭代断点:`save` 保存当前进度到 `.iterate/checkpoint.json`,`load` 读回,`clear` 清除。长迭代可中断续跑 |
169
+ | `iterate_status` | 汇总当前迭代状态:模式、当前轮/总轮、已修复数、剩余 architectural、决策日志条数、是否存在 checkpoint |
170
+ | `iterate_history` | 读取迭代历史(只读):决策日志条目(可按 `type` / `since` / `limit` 过滤,默认取最新 50 条,上限 200 条)+ 修复注册表汇总(各轮 fixed/failed 计数)。用于审查运行过程、审计日志、盘点修复 |
171
+ | `iterate_prune` | 清理运行时产物:过期决策日志条目(按 `retainDays`,默认 30 天)、陈旧断点、孤儿修复备份、空轮次。默认 dry-run 只报告不删除;`dryRun:false` 才真正清理,每次清理写入决策日志 |
172
+
173
+ ## 运行时产物布局
174
+
175
+ 所有运行时状态都落在项目根目录的 `.iterate/` 下(可由 `.gitignore` 排除):
176
+
177
+ ```
178
+ .iterate/
179
+ decision-log.jsonl # 追加式决策日志(plan/review/fix/revert…)
180
+ checkpoint.json # 迭代断点(断点续跑)
181
+ fixes/
182
+ registry.json # 修复注册表(FixRecord 列表,按轮次组织)
183
+ <fix-id>_<ts>.bak # 每次修复前的原文件备份
184
+ ```
120
185
 
121
186
  ## 设计
122
187
 
123
188
  插件遵循 dsh "everything-is-a-plugin" 架构:
124
- - 只做一件事:注入系统 prompt 教模型写 iterate workflow + 注册 5 个纯函数工具
125
- - 所有 orchestration 通过 dsh 原生 `workflow` + `agent` + `parallel` 完成
126
- - 核心逻辑(去重/过滤/排序/收敛/meta-audit)全部纯函数,可单元测试,无 I/O
189
+
190
+ - **只做两件事**:注入系统 prompt 教模型写 iterate workflow + 注册 13 个纯函数工具
191
+ - **所有 orchestration 通过 dsh 原生 `workflow` + `agent` + `parallel` 完成**
192
+ - **核心逻辑全部纯函数**(去重/过滤/排序/收敛/meta-audit/diff 计算/历史过滤/清理报告),可单元测试,无 I/O
193
+ - **安全模型**:文件写入限定在解析后的项目根目录内(路径遍历防护);写文件前必备份,失败回滚;配置写入同样备份 + 回滚;`iterate_prune` 默认 dry-run、只清理 `.iterate/` 下产物、每次清理写日志;`iterate_fix` 对 content 设字符上限、`iterate_triage` 对 entries 设数量上限,防止异常超大负载
194
+ - **UI 免构建**:`lib/client.js` 用 `React.createElement` 树 + 注入 `<style>` 标签,全部颜色走 `--dsw-*` 令牌,缺服务自动降级
127
195
  - 遵循 iterate 原技能的设计原则:确定性收敛,可审计,最小权限
128
196
 
129
197
  ## 运行测试
@@ -136,9 +204,9 @@ npm test
136
204
  ```
137
205
 
138
206
  所有测试通过:
139
- - 63 个单元测试全绿
140
- - 覆盖去重、过滤、排序、多轮收敛、meta-review 审计、路径安全、超时钳制
141
- - 类型检查通过
207
+
208
+ - **212 个单元测试全绿**,类型检查通过
209
+ - 覆盖:去重、过滤、排序、多轮收敛、meta-review 审计、路径安全、超时钳制、配置读写与回滚、triage 合并、diff 计算、checkpoint 校验、修复注册表、历史读取与过滤、prune 清理报告与 dry-run 语义、UI 纯函数(select-all 键、运行时状态指引)等
142
210
 
143
211
  ## License
144
212
 
@@ -0,0 +1,171 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join, resolve, sep } from 'node:path';
3
+ import yaml from 'js-yaml';
4
+ /**
5
+ * Load and parse iterate.config.yaml from the project root.
6
+ * Returns null if the file is missing or invalid.
7
+ */
8
+ export function loadConfig(projectRoot) {
9
+ try {
10
+ const content = readFileSync(join(projectRoot, 'iterate.config.yaml'), 'utf-8');
11
+ const parsed = yaml.load(content);
12
+ if (!parsed || typeof parsed !== 'object')
13
+ return null;
14
+ return parsed;
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ /**
21
+ * Sensible defaults for every config field. These are the "Master" config:
22
+ * when a project has no iterate.config.yaml (or only partial overrides), every
23
+ * missing key is filled from here so the plugin is usable out of the box while
24
+ * never inventing trusted validation commands (they must be configured).
25
+ */
26
+ export function defaultConfig() {
27
+ return {
28
+ goal: 'Improve code quality and maintainability',
29
+ max_rounds: 7,
30
+ language: 'en',
31
+ dimensions: [
32
+ 'correctness',
33
+ 'security',
34
+ 'performance',
35
+ 'architecture',
36
+ 'style-tests',
37
+ 'tech-debt',
38
+ 'spec-compliance',
39
+ 'frontend-backend',
40
+ 'ui-ux',
41
+ ],
42
+ review: { scope: 'full' },
43
+ atomic: { max_lines: 20, max_adjacent_methods: 3 },
44
+ git: {
45
+ target_branch: 'main',
46
+ use_worktree: false,
47
+ push_per_round: false,
48
+ auto_merge: false,
49
+ },
50
+ validation: { command_whitelist: [], commands: {} },
51
+ reviewer: { output_schema_validation: true },
52
+ };
53
+ }
54
+ /**
55
+ * Recursively merge `override` on top of `base`.
56
+ * - Missing keys in `base` are added from `override`.
57
+ * - Present keys in `override` win.
58
+ * - Plain objects are merged recursively; arrays and scalars are replaced
59
+ * wholesale by the override (arrays are NOT concatenated).
60
+ * Returns a NEW object; neither input is mutated.
61
+ */
62
+ export function mergeConfig(base, override) {
63
+ if (!override || typeof override !== 'object')
64
+ return { ...base };
65
+ const out = { ...base };
66
+ for (const [key, value] of Object.entries(override)) {
67
+ if (value === undefined)
68
+ continue;
69
+ const baseValue = out[key];
70
+ if (baseValue &&
71
+ typeof baseValue === 'object' &&
72
+ !Array.isArray(baseValue) &&
73
+ value &&
74
+ typeof value === 'object' &&
75
+ !Array.isArray(value)) {
76
+ out[key] = mergeConfig(baseValue, value);
77
+ }
78
+ else {
79
+ out[key] = value;
80
+ }
81
+ }
82
+ return out;
83
+ }
84
+ /**
85
+ * Load the EFFECTIVE config for a project: project-root overrides merged on top
86
+ * of the built-in defaults ("Master + Overrides"). Never returns null — a
87
+ * project without a config file simply runs on the defaults (with an empty
88
+ * validation command set, so nothing untrusted can ever execute).
89
+ */
90
+ export function loadEffectiveConfig(projectRoot) {
91
+ const override = loadConfig(projectRoot);
92
+ if (!override) {
93
+ return { config: defaultConfig(), source: 'defaults', override: null };
94
+ }
95
+ const merged = mergeConfig(defaultConfig(), override);
96
+ return { config: merged, source: 'override', override };
97
+ }
98
+ /**
99
+ * Check whether a command is in the predefined commands list.
100
+ * A command is allowed if it is EXACTLY (after trim) listed in any
101
+ * module's command array in `validation.commands`.
102
+ * This replaces the old prefix-based whitelist at runtime — the
103
+ * `command_whitelist` is still used for config-time validation only.
104
+ */
105
+ export function isCommandAllowed(command, predefinedCommands) {
106
+ const trimmed = command.trim();
107
+ return predefinedCommands.includes(trimmed);
108
+ }
109
+ /**
110
+ * Flatten all commands from `validation.commands` into a single string array.
111
+ * Used for runtime exact-match checking.
112
+ */
113
+ export function flattenCommands(commands) {
114
+ if (!commands || typeof commands !== 'object')
115
+ return [];
116
+ const out = [];
117
+ for (const v of Object.values(commands)) {
118
+ if (Array.isArray(v))
119
+ out.push(...v);
120
+ }
121
+ return out;
122
+ }
123
+ /**
124
+ * Validate that the config has all required fields.
125
+ * Returns an array of missing field paths.
126
+ */
127
+ export function validateConfig(config) {
128
+ const errors = [];
129
+ if (!config || typeof config !== 'object') {
130
+ errors.push('root');
131
+ return errors;
132
+ }
133
+ const c = config;
134
+ if (!c.goal)
135
+ errors.push('goal');
136
+ if (!Array.isArray(c.dimensions))
137
+ errors.push('dimensions');
138
+ if (!c.validation || typeof c.validation !== 'object') {
139
+ errors.push('validation');
140
+ }
141
+ else {
142
+ const v = c.validation;
143
+ if (!Array.isArray(v.command_whitelist))
144
+ errors.push('validation.command_whitelist');
145
+ if (!v.commands || typeof v.commands !== 'object')
146
+ errors.push('validation.commands');
147
+ }
148
+ return errors;
149
+ }
150
+ /**
151
+ * Resolve a caller-supplied project root to a safe absolute path.
152
+ *
153
+ * Every tool accepts a model-controlled `path` argument. Before it is used in
154
+ * any file read/write or as a command `cwd`, it must be sanitized:
155
+ * - an empty/missing `path` falls back to the current working directory;
156
+ * - the path is resolved to an absolute path (collapsing `..` and symlinks);
157
+ * - the filesystem root (`/`) is refused — it would let a prompt point tools
158
+ * at arbitrary system directories (path-traversal escape).
159
+ *
160
+ * Returns `{ ok: true, root }` on success, or `{ ok: false, reason }` when the
161
+ * path is unsafe; callers must short-circuit on the failure and return a
162
+ * structured error instead of proceeding.
163
+ */
164
+ export function resolveProjectRoot(input) {
165
+ const raw = (input ?? '').trim();
166
+ const root = raw ? resolve(raw) : resolve(process.cwd());
167
+ if (!root || root === sep) {
168
+ return { ok: false, reason: 'Refusing filesystem root as project root.' };
169
+ }
170
+ return { ok: true, root };
171
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * src/config-write.ts — shared helpers for safely WRITING iterate.config.yaml.
3
+ *
4
+ * Used by the `iterate_config` write operation. Provides:
5
+ * - validateConfigUpdates : validate a caller-supplied partial update
6
+ * - applyConfigUpdates : merge a partial update into the current config
7
+ * - writeConfigFile : backup + write + rollback on failure
8
+ *
9
+ * The security posture mirrors the triage tool: never overwrite a malformed
10
+ * config, always back up before writing, roll back on failure.
11
+ */
12
+ import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import yaml from 'js-yaml';
15
+ /** Config file name (must match config-loader). */
16
+ export const CONFIG_FILE = 'iterate.config.yaml';
17
+ /** Backup suffix helper (filesystem-safe timestamp). */
18
+ export function configBackupSuffix(now = new Date()) {
19
+ return now.toISOString().replace(/[:.]/g, '-');
20
+ }
21
+ /**
22
+ * Validate a partial config update.
23
+ * Returns an array of error strings (empty when the update is valid).
24
+ */
25
+ export function validateConfigUpdates(updates) {
26
+ const errors = [];
27
+ if (!updates || typeof updates !== 'object' || Array.isArray(updates)) {
28
+ return ['updates must be a JSON object'];
29
+ }
30
+ if ('goal' in updates && typeof updates.goal !== 'string') {
31
+ errors.push('updates.goal must be a string');
32
+ }
33
+ if ('language' in updates && updates.language !== 'zh' && updates.language !== 'en') {
34
+ errors.push('updates.language must be "zh" or "en"');
35
+ }
36
+ if ('dimensions' in updates) {
37
+ if (!Array.isArray(updates.dimensions) || updates.dimensions.some((d) => typeof d !== 'string' || d.trim().length === 0)) {
38
+ errors.push('updates.dimensions must be an array of non-empty strings');
39
+ }
40
+ }
41
+ if ('max_rounds' in updates) {
42
+ if (typeof updates.max_rounds !== 'number' || !Number.isInteger(updates.max_rounds) || updates.max_rounds < 1) {
43
+ errors.push('updates.max_rounds must be a positive integer');
44
+ }
45
+ }
46
+ if ('review' in updates) {
47
+ const r = updates.review;
48
+ if (!r || typeof r !== 'object') {
49
+ errors.push('updates.review must be an object');
50
+ }
51
+ else if (r.scope !== undefined && r.scope !== 'full' && r.scope !== 'changed-only') {
52
+ errors.push('updates.review.scope must be "full" or "changed-only"');
53
+ }
54
+ }
55
+ if ('atomic' in updates) {
56
+ const a = updates.atomic;
57
+ if (!a || typeof a !== 'object') {
58
+ errors.push('updates.atomic must be an object');
59
+ }
60
+ else {
61
+ if (a.max_lines !== undefined && (typeof a.max_lines !== 'number' || !Number.isInteger(a.max_lines) || a.max_lines < 1)) {
62
+ errors.push('updates.atomic.max_lines must be a positive integer');
63
+ }
64
+ if (a.max_adjacent_methods !== undefined && (typeof a.max_adjacent_methods !== 'number' || a.max_adjacent_methods < 0)) {
65
+ errors.push('updates.atomic.max_adjacent_methods must be a non-negative number');
66
+ }
67
+ }
68
+ }
69
+ if ('git' in updates) {
70
+ const g = updates.git;
71
+ if (!g || typeof g !== 'object') {
72
+ errors.push('updates.git must be an object');
73
+ }
74
+ else {
75
+ if (g.target_branch !== undefined && typeof g.target_branch !== 'string') {
76
+ errors.push('updates.git.target_branch must be a string');
77
+ }
78
+ for (const boolKey of ['use_worktree', 'push_per_round', 'auto_merge']) {
79
+ if (g[boolKey] !== undefined && typeof g[boolKey] !== 'boolean') {
80
+ errors.push(`updates.git.${boolKey} must be a boolean`);
81
+ }
82
+ }
83
+ }
84
+ }
85
+ if ('validation' in updates) {
86
+ const v = updates.validation;
87
+ if (!v || typeof v !== 'object') {
88
+ errors.push('updates.validation must be an object');
89
+ }
90
+ else if ('commands' in v && v.commands !== undefined && typeof v.commands !== 'object') {
91
+ errors.push('updates.validation.commands must be an object of command arrays');
92
+ }
93
+ }
94
+ if ('personalization' in updates && (!updates.personalization || typeof updates.personalization !== 'object')) {
95
+ errors.push('updates.personalization must be an object');
96
+ }
97
+ if ('onboarding' in updates && (!updates.onboarding || typeof updates.onboarding !== 'object')) {
98
+ errors.push('updates.onboarding must be an object');
99
+ }
100
+ return errors;
101
+ }
102
+ /** Recursively merge `updates` over `base` (arrays replaced wholesale). */
103
+ export function applyConfigUpdates(base, updates) {
104
+ const out = { ...base };
105
+ for (const [key, value] of Object.entries(updates)) {
106
+ if (value === undefined)
107
+ continue;
108
+ const baseValue = out[key];
109
+ if (baseValue &&
110
+ typeof baseValue === 'object' &&
111
+ !Array.isArray(baseValue) &&
112
+ value &&
113
+ typeof value === 'object' &&
114
+ !Array.isArray(value)) {
115
+ out[key] = applyConfigUpdates(baseValue, value);
116
+ }
117
+ else {
118
+ out[key] = value;
119
+ }
120
+ }
121
+ return out;
122
+ }
123
+ /**
124
+ * Read the raw config object from disk (empty object when missing).
125
+ * Throws when the file exists but cannot be parsed as a YAML mapping
126
+ * (never overwrite a malformed config).
127
+ */
128
+ export function readRawConfig(configPath) {
129
+ if (!existsSync(configPath))
130
+ return {};
131
+ const content = readFileSync(configPath, 'utf-8');
132
+ let parsed;
133
+ try {
134
+ parsed = yaml.load(content);
135
+ }
136
+ catch {
137
+ throw new Error('existing iterate.config.yaml is not a valid YAML mapping');
138
+ }
139
+ if (!parsed || typeof parsed !== 'object') {
140
+ throw new Error('existing iterate.config.yaml is not a valid YAML mapping');
141
+ }
142
+ return parsed;
143
+ }
144
+ /**
145
+ * Write a config object to disk with backup + rollback.
146
+ * Returns `{ ok: true, backupPath }` or `{ ok: false, error }`.
147
+ */
148
+ export function writeConfigFile(projectRoot, config) {
149
+ const configPath = join(projectRoot, CONFIG_FILE);
150
+ const hadFile = existsSync(configPath);
151
+ const backupPath = hadFile ? `${configPath}.bak-${configBackupSuffix()}` : null;
152
+ if (backupPath) {
153
+ try {
154
+ copyFileSync(configPath, backupPath);
155
+ }
156
+ catch (err) {
157
+ return { ok: false, error: `failed to create backup: ${String(err)}` };
158
+ }
159
+ }
160
+ try {
161
+ writeFileSync(configPath, yaml.dump(config, { noRefs: true }), 'utf-8');
162
+ }
163
+ catch (err) {
164
+ try {
165
+ if (backupPath)
166
+ copyFileSync(backupPath, configPath);
167
+ }
168
+ catch {
169
+ // Rollback failure is reported, never swallowed silently.
170
+ }
171
+ return { ok: false, error: `failed to write config: ${String(err)}` };
172
+ }
173
+ return { ok: true, backupPath };
174
+ }
package/dist/index.js ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * iterate-plugin — dsh plugin for the iterate autonomous closed-loop workflow
3
+ *
4
+ * Architecture:
5
+ * - The plugin registers 6 tools (config, validate, decision-log, context, review, triage)
6
+ * - The plugin injects a system prompt section teaching the iterate workflow pattern
7
+ * - The model (prompted by the skill) writes a workflow script using dsh's `workflow` tool
8
+ * - The workflow script uses `agent()` / `parallel()` / `phase()` / `log()` to orchestrate
9
+ * - Subagents use the 6 tools to do real work (read config, run validation, log decisions, review, triage)
10
+ *
11
+ * Tool invocation model:
12
+ * - Workflow script CANNOT call tools directly (sandboxed vm, no Node API)
13
+ * - Workflow script spawns subagents via `agent(prompt, opts)`
14
+ * - Subagents are full agent sessions with access to all registered tools
15
+ * - The script is pure orchestration: fan-out, aggregate, loop, stop
16
+ *
17
+ * Key files:
18
+ * - src/index.ts — Plugin entry: register tools + inject skill prompt
19
+ * - src/tools/ — 6 tool implementations + meta-review/review engines
20
+ * - src/config-loader.ts — YAML config loading
21
+ * - src/types.ts — Shared types
22
+ */
23
+ import { registerConfigTool } from "./tools/config.js";
24
+ import { registerValidateTool } from "./tools/validate.js";
25
+ import { registerDecisionLogTool } from "./tools/decision-log.js";
26
+ import { registerContextTool } from "./tools/context.js";
27
+ import { registerReviewTool } from "./tools/review.js";
28
+ import { registerTriageTool } from "./tools/triage.js";
29
+ import { registerFixTool, registerDiffTool, registerRollbackTool } from "./tools/fix.js";
30
+ import { registerCheckpointTool, registerStatusTool } from "./tools/checkpoint.js";
31
+ import { registerHistoryTool } from "./tools/history.js";
32
+ import { registerPruneTool } from "./tools/prune.js";
33
+ import { ITERATE_SKILL_PROMPT } from "./skill-prompt.js";
34
+ export const name = 'iterate-plugin';
35
+ export const inject = ['tools', 'systemPrompt'];
36
+ export function apply(ctx) {
37
+ // 1. Register the 11 tools
38
+ registerConfigTool(ctx);
39
+ registerValidateTool(ctx);
40
+ registerDecisionLogTool(ctx);
41
+ registerContextTool(ctx);
42
+ registerReviewTool(ctx);
43
+ registerTriageTool(ctx);
44
+ registerFixTool(ctx);
45
+ registerDiffTool(ctx);
46
+ registerRollbackTool(ctx);
47
+ registerCheckpointTool(ctx);
48
+ registerStatusTool(ctx);
49
+ registerHistoryTool(ctx);
50
+ registerPruneTool(ctx);
51
+ // 2. Inject the iterate skill prompt as a system prompt section
52
+ // This teaches the model how to write iterate workflow scripts using the tools.
53
+ ctx.systemPrompt.section({
54
+ name: 'iterate-skill',
55
+ order: 100,
56
+ text: ITERATE_SKILL_PROMPT,
57
+ });
58
+ }