iterate-plugin 2.10.0 → 2.12.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.
Files changed (55) hide show
  1. package/README.md +42 -2
  2. package/README.zh-CN.md +40 -2
  3. package/dist/approval-gate.js +92 -0
  4. package/dist/config-loader.js +18 -3
  5. package/dist/config-write.js +7 -4
  6. package/dist/evidence.js +67 -1
  7. package/dist/git-scope.js +35 -6
  8. package/dist/index.js +15 -5
  9. package/dist/live.js +155 -0
  10. package/dist/meta-review.js +19 -5
  11. package/dist/method-scope.js +5 -1
  12. package/dist/paths.js +4 -0
  13. package/dist/review-scope.js +12 -8
  14. package/dist/review.js +76 -24
  15. package/dist/session-hooks.js +89 -0
  16. package/dist/skill-prompt.js +101 -19
  17. package/dist/tools/checkpoint.js +10 -3
  18. package/dist/tools/context.js +16 -4
  19. package/dist/tools/decision-log.js +29 -9
  20. package/dist/tools/fix.js +120 -3
  21. package/dist/tools/prune.js +16 -9
  22. package/dist/tools/review.js +4 -1
  23. package/dist/tools/transcript.js +324 -0
  24. package/dist/tools/triage.js +9 -6
  25. package/dist/tools/validate.js +5 -2
  26. package/dist/transcript.js +421 -0
  27. package/lib/client.js +966 -80
  28. package/lib/parse.js +302 -17
  29. package/package.json +1 -1
  30. package/src/approval-gate.ts +119 -0
  31. package/src/client/index.ts +807 -62
  32. package/src/config-loader.ts +16 -2
  33. package/src/config-write.ts +6 -4
  34. package/src/evidence.ts +69 -1
  35. package/src/git-scope.ts +34 -6
  36. package/src/index.ts +17 -6
  37. package/src/live.ts +185 -0
  38. package/src/meta-review.ts +24 -10
  39. package/src/method-scope.ts +5 -1
  40. package/src/paths.ts +5 -0
  41. package/src/review-scope.ts +11 -7
  42. package/src/review.ts +82 -25
  43. package/src/session-hooks.ts +90 -0
  44. package/src/skill-prompt.ts +101 -19
  45. package/src/tools/checkpoint.ts +10 -3
  46. package/src/tools/context.ts +14 -3
  47. package/src/tools/decision-log.ts +27 -10
  48. package/src/tools/fix.ts +114 -3
  49. package/src/tools/prune.ts +14 -11
  50. package/src/tools/review.ts +5 -2
  51. package/src/tools/transcript.ts +334 -0
  52. package/src/tools/triage.ts +9 -6
  53. package/src/tools/validate.ts +5 -2
  54. package/src/transcript.ts +475 -0
  55. package/src/types.ts +129 -0
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # iterate-plugin for DeepSeek Harness (dsh)
2
2
 
3
+ > dsh 桌面端的 iterate 收敛面板、UI 分诊与审查闭环插件。把 iterate 生态的同一套 review/fix loop 直接搬进 dsh 界面。
4
+ > The iterate ecosystem's convergence dashboard, triage UI, and review/fix loop, natively embedded inside the DeepSeek Harness (dsh) desktop client.
5
+
3
6
  <p align="center">
4
7
  <a href="README.md"><strong>English</strong></a> ·
5
8
  <a href="README.zh-CN.md"><strong>简体中文</strong></a>
@@ -11,9 +14,44 @@
11
14
  <a href="https://github.com/jingzhao-l/iterate-plugin"><img src="https://img.shields.io/github/stars/jingzhao-l/iterate-plugin?style=social&label=Star" alt="Stars"></a>
12
15
  <a href="https://github.com/jingzhao-l/iterate-skill"><img src="https://img.shields.io/github/stars/jingzhao-l/iterate-skill?style=social&label=Main%20Repo%20Star" alt="Main Repo Stars"></a>
13
16
  <a href="https://www.npmjs.com/package/iterate-plugin"><img src="https://img.shields.io/npm/dt/iterate-plugin?label=Downloads&logo=npm&logoColor=white" alt="npm downloads"></a>
17
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-yellow" alt="License"></a>
18
+ <a href="https://github.com/jingzhao-l/iterate-plugin/releases"><img src="https://img.shields.io/github/v/release/jingzhao-l/iterate-plugin" alt="GitHub release"></a>
14
19
  </p>
15
20
 
16
- > ⭐ If this helps your dsh workflow, give the main repo a star — it means a lot!
21
+ > ⭐ If this helps your dsh workflow, give the main repo a star — it means a lot to open-source maintenance!
22
+
23
+ ---
24
+
25
+ ## The iterate Ecosystem / iterate 生态一览
26
+
27
+ **iterate** is not one single binary — it is a **skill ecosystem** that layers a strict multi-round code gate on top of your existing AI assistants, IDEs, and scripts. It never replaces your tools; it adds an audit-and-close-the-loop layer on them. The whole ecosystem ships as **three interchangeable components sharing one `iterate.config.yaml` + one review-dimension system**:
28
+
29
+ | Component | Form & Source | Target Scenario |
30
+ |---|---|---|
31
+ | **[Core Skill + CLI](https://github.com/jingzhao-l/iterate-skill)** | Portable AI skill `/iterate` + `iterate` CLI (source: iterate-skill monorepo root) | Conversation-driven multi-round iteration inside Trae / Claude Code / Cursor / Copilot / Codex and 25+ other assistants |
32
+ | **[iterate-harness](https://github.com/jingzhao-l/iterate-harness)** | Standalone headless engine, command `ih` (npm: `iterate-harness`) | Run the EXACT same loop in terminal / CI / git hooks, without any conversational assistant required |
33
+ | **iterate-plugin (this repo)** | dsh desktop-client plugin (npm: `iterate-plugin`) | Plug the harness runtime **into the dsh UI**: convergence dashboard, triage panel, round progress — all surfaced as native dsh widgets |
34
+
35
+ How they fit together: **Core Skill** is the canonical, assistant-agnostic review/fix engine (the "brains"). **iterate-harness** is the same engine wrapped as a headless CLI + WebUI for unattended runs. **iterate-plugin** (this repository) wraps that harness runtime as a dsh plugin, rendering the triage UI and convergence dashboard directly inside the dsh desktop client. Configuration (`iterate.config.yaml`) and the 9-dimension review system are **identical across all three** — learn one, use them all.
36
+
37
+ Quick install / entry points for the rest of the ecosystem:
38
+
39
+ ```bash
40
+ # Core Skill + CLI (install into 25+ AI assistants)
41
+ npx iterate-skill-installer
42
+
43
+ # iterate-harness: headless engine (npm wrapper, simplest)
44
+ npm install -g iterate-harness
45
+ curl -fsSL https://raw.githubusercontent.com/jingzhao-l/iterate-harness/main/scripts/install.sh | bash
46
+ ih iterate init && ih iterate review
47
+
48
+ # iterate-plugin: dsh desktop plugin (this repo — commands repeated under Installation below)
49
+ dsh plugin --profile web add iterate-plugin
50
+ ```
51
+
52
+ > This document focuses on **iterate-plugin (this repo)**. For Core Skill docs see the [iterate-skill monorepo](https://github.com/jingzhao-l/iterate-skill); for headless engine docs see [iterate-harness](https://github.com/jingzhao-l/iterate-harness).
53
+
54
+ ---
17
55
 
18
56
  ## About This Plugin
19
57
 
@@ -23,7 +61,7 @@
23
61
 
24
62
  `iterate-plugin` is the [iterate](https://github.com/jingzhao-l/iterate-skill) integration for the [DeepSeek Harness (dsh)](https://github.com/deepseek-ai/deepseek-harness) desktop client. It brings iterate's review loop (review → triage → fix → validate → converge) directly into the dsh UI, offering **autonomous closed-loop code iteration** (normal mode) and **dry-run read-only multi-round review**.
25
63
 
26
- Besides 13 pure-function tools, it ships a **build-free Web UI layer** (triage panel, convergence dashboard, stats card, theme skin, etc.) that plugs straight into dsh's existing UI slots. Configuration (`iterate.config.yaml` and the review dimensions) is identical across the other two components of the iterate ecosystem (skill / headless engine) — zero migration cost.
64
+ Besides 13 pure-function tools, it ships a **build-free Web UI layer** (triage panel, convergence dashboard, stats card, theme skin, etc.) that plugs straight into dsh's existing UI slots. Configuration (`iterate.config.yaml` and the review dimensions) is identical across the other two components of the iterate ecosystem ([skill](https://github.com/jingzhao-l/iterate-skill) / [headless engine](https://github.com/jingzhao-l/iterate-harness)) — zero migration cost.
27
65
 
28
66
  ## Features
29
67
 
@@ -199,6 +237,8 @@ All runtime state lives under `.iterate/` at the project root (can be excluded v
199
237
  .iterate/
200
238
  decision-log.jsonl # append-only decision log (plan/review/fix/revert…)
201
239
  checkpoint.json # iteration breakpoint (resume)
240
+ transcript.json # runtime-observatory manifest (per-reviewer threads, trend, fixes, timeline, nudge)
241
+ transcript-live.ndjson # append-only near-real-time reviewer-activity feed (read/fix/rollback/validate…), byte-capped
202
242
  fixes/
203
243
  registry.json # fix registry (list of FixRecords, grouped by round)
204
244
  <fix-id>_<ts>.bak # original file backup before each fix
package/README.zh-CN.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # iterate-plugin for DeepSeek Harness (dsh)
2
2
 
3
+ > dsh 桌面端的 iterate 收敛面板、UI 分诊与审查闭环插件。把 iterate 生态的同一套 review/fix loop 直接搬进 dsh 界面。
4
+ > The iterate ecosystem's convergence dashboard, triage UI, and review/fix loop, natively embedded inside the DeepSeek Harness (dsh) desktop client.
5
+
3
6
  <p align="center">
4
7
  <a href="README.md"><strong>English</strong></a> ·
5
8
  <a href="README.zh-CN.md"><strong>简体中文</strong></a>
@@ -11,9 +14,44 @@
11
14
  <a href="https://github.com/jingzhao-l/iterate-plugin"><img src="https://img.shields.io/github/stars/jingzhao-l/iterate-plugin?style=social&label=Star" alt="Stars"></a>
12
15
  <a href="https://github.com/jingzhao-l/iterate-skill"><img src="https://img.shields.io/github/stars/jingzhao-l/iterate-skill?style=social&label=主仓库%20Star" alt="主仓库 Stars"></a>
13
16
  <a href="https://www.npmjs.com/package/iterate-plugin"><img src="https://img.shields.io/npm/dt/iterate-plugin?label=Downloads&logo=npm&logoColor=white" alt="npm downloads"></a>
17
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-yellow" alt="License"></a>
18
+ <a href="https://github.com/jingzhao-l/iterate-plugin/releases"><img src="https://img.shields.io/github/v/release/jingzhao-l/iterate-plugin" alt="GitHub release"></a>
14
19
  </p>
15
20
 
16
- > ⭐ 如果这个插件对你的 dsh 工作流有帮助,欢迎为主仓库点亮 Star,这是对开源维护最大的支持!
21
+ > ⭐ 如果这个插件对你的 dsh 工作流有帮助,欢迎为主仓库点亮 Star,这是对开源维护最大的支持!你的 Star 能让 iterate 被更多开发者看见。
22
+
23
+ ---
24
+
25
+ ## iterate 生态一览 / The iterate Ecosystem
26
+
27
+ **iterate 不是一个独立的工具,而是一套附着在现有 AI 助手之上的技能生态。** 它不会替换你的 IDE 或 AI 工具,而是在你已有的工作流里,叠加一层"严格的代码收尾与把关"。整个生态由**三个可互换组件构成,共用同一套 `iterate.config.yaml` 与同一套 9 维度审查体系**:
28
+
29
+ | 组件 | 形态与位置 | 面向场景 |
30
+ |---|---|---|
31
+ | **[Core Skill + CLI / 核心技能与命令行](https://github.com/jingzhao-l/iterate-skill)** | 可移植 AI 技能 `/iterate` + `iterate` CLI(源码:iterate-skill 主仓库根目录) | 在 Trae / Claude Code / Cursor / Copilot / Codex 等 25+ 助手中以对话方式进行多轮迭代 |
32
+ | **[iterate-harness / 无头引擎](https://github.com/jingzhao-l/iterate-harness)** | 独立无头引擎,命令 `ih`(npm: `iterate-harness`) | 在终端 / CI / Git 钩子里**脱离对话式助手**,运行同一套闭环 |
33
+ | **iterate-plugin / dsh 桌面插件(本仓库)** | dsh 桌面客户端插件(npm: `iterate-plugin`) | 把 harness 运行时**直接接入 dsh 界面**:收敛看板、分诊面板、轮次进度,均以原生 dsh 控件形式展示 |
34
+
35
+ 三者关系:**Core Skill** 是最通用的跨助手审查/修复引擎("大脑");**iterate-harness** 是同一引擎封装为无头 CLI + WebUI,适合跑在无需交互的场景;**iterate-plugin**(本仓库)把 harness 运行时进一步封装为 dsh 插件,直接在 dsh 桌面客户端内渲染分诊 UI 与收敛看板。配置(`iterate.config.yaml`)与 9 维度审查体系**三者完全一致**——理解其一,即可举一反三。
36
+
37
+ 生态其余组件的快速安装/入口:
38
+
39
+ ```bash
40
+ # Core Skill + CLI(一键安装到 25+ AI 编程助手)
41
+ npx iterate-skill-installer
42
+
43
+ # iterate-harness:无头引擎(npm 包装器,最简)
44
+ npm install -g iterate-harness
45
+ curl -fsSL https://raw.githubusercontent.com/jingzhao-l/iterate-harness/main/scripts/install.sh | bash
46
+ ih iterate init && ih iterate review
47
+
48
+ # iterate-plugin:dsh 桌面插件(本仓库,安装命令在下文「安装」一节重复列出)
49
+ dsh plugin --profile web add iterate-plugin
50
+ ```
51
+
52
+ > 本文档从下一节开始聚焦 **iterate-plugin(本仓库)**。核心技能文档见 [iterate-skill 主仓库](https://github.com/jingzhao-l/iterate-skill),无头引擎文档见 [iterate-harness 独立仓库](https://github.com/jingzhao-l/iterate-harness)。
53
+
54
+ ---
17
55
 
18
56
  ## 这是什么 / About This Plugin
19
57
 
@@ -23,7 +61,7 @@
23
61
 
24
62
  `iterate-plugin` 是 [iterate](https://github.com/jingzhao-l/iterate-skill) 项目在 [DeepSeek Harness (dsh)](https://github.com/deepseek-ai/deepseek-harness) 桌面客户端中的落地插件。它把 iterate 的开环审查闭环(review → triage → fix → validate → 收敛)直接带进 dsh 的界面:提供**自治闭环代码迭代**(normal 模式)与 **dry-run 纯多轮审查**(只读)两种能力。
25
63
 
26
- 除 13 个纯函数工具外,还内置一套**免构建的 Web UI 层**(分诊面板、收敛看板、统计卡片、主题皮肤等),直接挂在 dsh 客户端的既有 UI 槽位上。配置方式(`iterate.config.yaml` 与审查维度)与迭代生态的另外两个组件(技能 / 无头引擎)完全一致,迁移零成本。
64
+ 除 13 个纯函数工具外,还内置一套**免构建的 Web UI 层**(分诊面板、收敛看板、统计卡片、主题皮肤等),直接挂在 dsh 客户端的既有 UI 槽位上。配置方式(`iterate.config.yaml` 与审查维度)与迭代生态的另外两个组件([技能](https://github.com/jingzhao-l/iterate-skill) / [无头引擎](https://github.com/jingzhao-l/iterate-harness))完全一致,迁移零成本。
27
65
 
28
66
  ## 特性
29
67
 
@@ -0,0 +1,92 @@
1
+ /**
2
+ * src/approval-gate.ts — pure policy gate for destructive iterate tool calls.
3
+ *
4
+ * Feeds dsh's `tools/pre-execute` waterfall (registered in `session-hooks.ts`).
5
+ * The gate classifies a tool execution and returns a typed decision without
6
+ * any I/O, so it is fully unit-testable:
7
+ *
8
+ * - `{ kind: 'allow' }` → run the call.
9
+ * - `{ kind: 'ask', reason }` → prompt the human via the dsh approval service.
10
+ * - `{ kind: 'deny', reason }` → refuse; the caller surfaces the reason.
11
+ *
12
+ * Policy (config `observatory.approval`, default 'ask'):
13
+ * - `allow` → destructive iterate calls always run (debug/trusted).
14
+ * - `deny` → destructive iterate calls are always refused (fail-closed).
15
+ * - `ask` → destructive iterate calls prompt the human first.
16
+ *
17
+ * Destructive calls are exactly the ones that mutate the workspace:
18
+ * `iterate_fix` (writes files), `iterate_rollback` (restores from backups),
19
+ * and `iterate_prune` with `dryRun !== true` (deletes `.iterate/` artifacts).
20
+ * Read-only calls are always allowed. Non-iterate calls are untouched — the
21
+ * gate only ever inspects iterate tools so it cannot alter unrelated behavior.
22
+ */
23
+ /** Destructive iterate tools subject to the gate. */
24
+ const DESTRUCTIVE_TOOLS = new Set(['iterate_fix', 'iterate_rollback', 'iterate_prune']);
25
+ /** Human-readable reason rendered in the approval prompt. */
26
+ function describe(toolName, arguments0) {
27
+ const file = arguments0 && typeof arguments0.file === 'string'
28
+ ? `\`${arguments0.file}\``
29
+ : 'the workspace';
30
+ switch (toolName) {
31
+ case 'iterate_fix':
32
+ return `Apply an atomic fix to ${file}`;
33
+ case 'iterate_rollback': {
34
+ const id = arguments0 && typeof arguments0.id === 'string' ? ` \`${arguments0.id}\`` : '';
35
+ return `Revert fix${id} (restore ${file} from backup)`;
36
+ }
37
+ case 'iterate_prune':
38
+ return 'Delete stale `.iterate/` runtime artifacts';
39
+ default:
40
+ return `Run ${toolName}`;
41
+ }
42
+ }
43
+ /**
44
+ * Decide whether a tool execution may proceed under the given policy.
45
+ * Returns `allow` for read-only prune (`dryRun: true`), for `allow`-policy
46
+ * deployments, and for any non-iterate tool.
47
+ */
48
+ export function decideApproval(execution, policy) {
49
+ const name = typeof execution?.name === 'string' ? execution.name : '';
50
+ if (!name)
51
+ return { kind: 'allow' };
52
+ if (!DESTRUCTIVE_TOOLS.has(name))
53
+ return { kind: 'allow' };
54
+ const rawArgs = execution.arguments;
55
+ const args = rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
56
+ ? rawArgs
57
+ : {};
58
+ // `iterate_prune` is read-only in its default dry-run mode — no gate needed.
59
+ if (name === 'iterate_prune' && args.dryRun === false) {
60
+ // falls through to the destructive path below
61
+ }
62
+ else if (name === 'iterate_prune') {
63
+ return { kind: 'allow' };
64
+ }
65
+ if (policy === 'allow')
66
+ return { kind: 'allow' };
67
+ const reason = describe(name, args);
68
+ if (policy === 'deny')
69
+ return { kind: 'deny', reason };
70
+ return { kind: 'ask', reason };
71
+ }
72
+ /** True when any destructive iterate tool is listed in a name set. */
73
+ export function isDestructiveIterateTool(name) {
74
+ return typeof name === 'string' && DESTRUCTIVE_TOOLS.has(name);
75
+ }
76
+ /**
77
+ * Evaluate an iterate tool's own boundary gate for a destructive call.
78
+ * `approvedArg` is the caller-supplied `approved: true` flag (human consent
79
+ * already obtained). Returns a run / refuse result without any I/O.
80
+ */
81
+ export function toolGate(policy, execution, approvedArg) {
82
+ const decision = decideApproval(execution, policy);
83
+ if (decision.kind === 'allow')
84
+ return { ok: true };
85
+ if (decision.kind === 'deny') {
86
+ return { ok: false, error: `Blocked by observatory approval policy: ${decision.reason}` };
87
+ }
88
+ // ask
89
+ if (approvedArg === true)
90
+ return { ok: true };
91
+ return { ok: false, requiresApproval: true, reason: decision.reason };
92
+ }
@@ -1,4 +1,4 @@
1
- import { readFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { join, resolve, sep } from 'node:path';
4
4
  import yaml from 'js-yaml';
@@ -55,6 +55,10 @@ export function defaultConfig() {
55
55
  coverage_validation: true,
56
56
  scope_chunk_size: 25,
57
57
  },
58
+ observatory: {
59
+ capture: true,
60
+ approval: 'ask',
61
+ },
58
62
  };
59
63
  }
60
64
  /**
@@ -72,6 +76,11 @@ export function mergeConfig(base, override) {
72
76
  for (const [key, value] of Object.entries(override)) {
73
77
  if (value === undefined)
74
78
  continue;
79
+ // Prototype-pollution guard: a YAML `__proto__`/`constructor`/`prototype`
80
+ // key must never be plain-assigned — js-yaml stores __proto__ as an own
81
+ // data property, and `out[key] = value` would invoke the __proto__ setter.
82
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype')
83
+ continue;
75
84
  const baseValue = out[key];
76
85
  if (baseValue &&
77
86
  typeof baseValue === 'object' &&
@@ -223,8 +232,14 @@ function effectiveCwd(sessionCwd) {
223
232
  if (encoded && encoded.startsWith('--') && encoded.endsWith('--')) {
224
233
  try {
225
234
  const decoded = decodeURIComponent(encoded.slice(2, -2).replace(/~/g, '%'));
226
- if (decoded && decoded.startsWith(sep))
227
- return decoded;
235
+ // The workspace encoding drops the leading root separator (`/Volumes/…`
236
+ // → `Volumes-…`), so re-attach it when absent. `~<hex>` → `%<hex>` is
237
+ // the documented percent spelling; '-' doubles as the '/' separator, so
238
+ // literal dashes in a path cannot round-trip — verify the result exists
239
+ // and fall through otherwise.
240
+ const candidate = decoded && !decoded.startsWith(sep) ? sep + decoded : decoded;
241
+ if (candidate && candidate.startsWith(sep) && existsSync(candidate))
242
+ return candidate;
228
243
  }
229
244
  catch {
230
245
  // malformed encoding — fall through to cwd
@@ -9,7 +9,7 @@
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, writeFileSync } from 'node:fs';
12
+ import { copyFileSync, existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
13
13
  import { join } from 'node:path';
14
14
  import yaml from 'js-yaml';
15
15
  /** Config file name (must match config-loader). */
@@ -161,14 +161,17 @@ export function writeConfigFile(projectRoot, config) {
161
161
  writeFileSync(configPath, yaml.dump(config, { noRefs: true }), 'utf-8');
162
162
  }
163
163
  catch (err) {
164
+ let rollbackError = '';
164
165
  try {
165
166
  if (backupPath)
166
167
  copyFileSync(backupPath, configPath);
168
+ else if (existsSync(configPath))
169
+ rmSync(configPath, { force: true });
167
170
  }
168
- catch {
169
- // Rollback failure is reported, never swallowed silently.
171
+ catch (rbErr) {
172
+ rollbackError = `; rollback also failed: ${String(rbErr)}`;
170
173
  }
171
- return { ok: false, error: `failed to write config: ${String(err)}` };
174
+ return { ok: false, error: `failed to write config: ${String(err)}${rollbackError}` };
172
175
  }
173
176
  return { ok: true, backupPath };
174
177
  }
package/dist/evidence.js CHANGED
@@ -22,10 +22,17 @@
22
22
  * The pure math (`countLines`, `verifyLineBounds`) is separated from the
23
23
  * filesystem half (`verifyFinding`) to stay unit-testable without touching disk.
24
24
  */
25
- import { existsSync, readFileSync } from 'node:fs';
25
+ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
26
26
  import { resolve, sep } from 'node:path';
27
27
  /** Sentinel for whole-file findings (line 0 or omitted means the whole file). */
28
28
  export const WHOLE_FILE_LINE = 0;
29
+ /**
30
+ * Hard cap on a single evidence file read. `verifyFinding` only needs the
31
+ * line count + a NUL check; reading an unbounded file (or a device file
32
+ * reached through a symlink) is a memory/hang hazard, so anything larger is
33
+ * treated as not line-addressable.
34
+ */
35
+ const MAX_EVIDENCE_BYTES = 10 * 1024 * 1024;
29
36
  /** Number of physical lines in `text`. A trailing newline does not add a line. */
30
37
  export function countLines(text) {
31
38
  if (text === '')
@@ -51,6 +58,22 @@ export function resolveWithin(root, rel) {
51
58
  return null;
52
59
  return resolved;
53
60
  }
61
+ /** True when `candidate` is `root` itself or lexically inside `root`. */
62
+ function isWithin(root, candidate) {
63
+ if (candidate === root)
64
+ return true;
65
+ const prefix = root.endsWith(sep) ? root : root + sep;
66
+ return candidate.startsWith(prefix);
67
+ }
68
+ /** best-effort realpath; falls back to the lexical path on any failure. */
69
+ function safeRealpath(p) {
70
+ try {
71
+ return realpathSync(p);
72
+ }
73
+ catch {
74
+ return p;
75
+ }
76
+ }
54
77
  /**
55
78
  * Pure check that `line` (if anchored) exists in `text`.
56
79
  * Whole-file findings (undefined/0) are always bounds-valid.
@@ -79,6 +102,49 @@ export function verifyFinding(root, input, opts = {}) {
79
102
  error: 'file_not_found',
80
103
  };
81
104
  }
105
+ // Symlink containment: resolveWithin is lexical only, but existsSync /
106
+ // readFileSync follow symlinks. Verify the REAL path stays inside the REAL
107
+ // project root so a finding path can never read (or line-count) a file
108
+ // outside the project via a symlinked directory or file.
109
+ const rootReal = safeRealpath(root);
110
+ const real = safeRealpath(resolved);
111
+ if (!isWithin(rootReal, real)) {
112
+ return {
113
+ file: relFile,
114
+ line,
115
+ lineTotal: null,
116
+ resolvedPath: resolved,
117
+ verified: false,
118
+ error: 'file_not_found',
119
+ };
120
+ }
121
+ // Regular-file + size guard: a directory, device file (/dev/zero), FIFO or
122
+ // multi-GB file is not a line-addressable text target. statSync follows
123
+ // symlinks, so a link to a device still lands here and is rejected.
124
+ let st;
125
+ try {
126
+ st = statSync(resolved);
127
+ }
128
+ catch {
129
+ return {
130
+ file: relFile,
131
+ line,
132
+ lineTotal: null,
133
+ resolvedPath: resolved,
134
+ verified: false,
135
+ error: 'file_not_found',
136
+ };
137
+ }
138
+ if (!st.isFile() || st.size > MAX_EVIDENCE_BYTES) {
139
+ return {
140
+ file: relFile,
141
+ line,
142
+ lineTotal: null,
143
+ resolvedPath: resolved,
144
+ verified: false,
145
+ error: 'line_out_of_range',
146
+ };
147
+ }
82
148
  let raw;
83
149
  try {
84
150
  raw = readFileSync(resolved);
package/dist/git-scope.js CHANGED
@@ -24,14 +24,31 @@ import { execFile } from 'node:child_process';
24
24
  import { existsSync, statSync } from 'node:fs';
25
25
  import { join } from 'node:path';
26
26
  /**
27
- * Parse `git diff --name-only` stdout into a list of relative paths.
28
- * Pure: strips blank lines, trims whitespace, drops quotes (git can quote
29
- * paths with special characters).
27
+ * Parse `git diff --name-only -z` stdout into a list of relative paths.
28
+ * Pure. NUL-delimited mode is machine-safe (handles any filename); when no
29
+ * NUL is present (callers that did not pass -z) fall back to newline-split
30
+ * with C-style quote/escape unescaping for core.quotePath output.
30
31
  */
31
32
  export function parseChangedFiles(stdout) {
33
+ if (stdout.includes('\0')) {
34
+ return stdout.split('\0').map((s) => s.trim()).filter((s) => s.length > 0);
35
+ }
32
36
  return stdout
33
37
  .split('\n')
34
- .map((line) => line.trim().replace(/^"|"$/g, ''))
38
+ .map((line) => {
39
+ const trimmed = line.trim();
40
+ // git core.quotePath wraps paths with special characters in "..."; the
41
+ // content uses C-style escapes (\" \\ \t \n and \ooo octal for non-ASCII).
42
+ const quoted = trimmed.match(/^"(.*)"$/);
43
+ if (!quoted)
44
+ return trimmed;
45
+ return quoted[1]
46
+ .replace(/\\"/g, '"')
47
+ .replace(/\\\\/g, '\\')
48
+ .replace(/\\t/g, '\t')
49
+ .replace(/\\n/g, '\n')
50
+ .replace(/\\([0-7]{3})/g, (_m, oct) => String.fromCharCode(parseInt(oct, 8)));
51
+ })
35
52
  .filter((line) => line.length > 0);
36
53
  }
37
54
  /**
@@ -90,9 +107,21 @@ export function runGit(args, cwd) {
90
107
  * plan because git is unavailable.
91
108
  */
92
109
  export async function resolveChangedFiles(root, targetBranch) {
93
- const { ok, stdout, stderr } = await runGit(['diff', '--name-only', targetBranch, '--'], root);
110
+ // Option-injection guard: a branch name starting with '-' would be parsed by
111
+ // git as an option (e.g. --output=...), not a ref. Reject it outright.
112
+ if (typeof targetBranch !== 'string' || targetBranch.trim() === '' || targetBranch.startsWith('-')) {
113
+ return {
114
+ scope: 'full',
115
+ changedFiles: [],
116
+ fallbackToFull: true,
117
+ error: `invalid target branch "${String(targetBranch)}"`,
118
+ };
119
+ }
120
+ // -z: NUL-delimited names — machine-safe for any filename (spaces, quotes,
121
+ // non-ASCII), and never confused with option-like content.
122
+ const { ok, stdout, stderr } = await runGit(['diff', '--name-only', '-z', targetBranch, '--'], root);
94
123
  if (!ok) {
95
- const reason = stderr.trim() || `git diff --name-only ${targetBranch} failed`;
124
+ const reason = stderr.trim() || `git diff --name-only -z ${targetBranch} failed`;
96
125
  return { scope: 'full', changedFiles: [], fallbackToFull: true, error: reason };
97
126
  }
98
127
  const existing = filterExistingFiles(root, parseChangedFiles(stdout));
package/dist/index.js CHANGED
@@ -2,13 +2,15 @@
2
2
  * iterate-plugin — dsh plugin for the iterate autonomous closed-loop workflow
3
3
  *
4
4
  * Architecture:
5
- * - The plugin registers 13 tools (config, validate, decision-log, context, review,
6
- * triage, fix, diff, rollback, checkpoint, status, history, prune)
5
+ * - The plugin registers 14 tools (config, validate, decision-log, context, review,
6
+ * triage, fix, diff, rollback, checkpoint, status, history, prune, transcript)
7
7
  * - The plugin injects a system prompt section teaching the iterate workflow pattern
8
8
  * - The model (prompted by the skill) writes a workflow script using dsh's `workflow` tool
9
9
  * - The workflow script uses `agent()` / `parallel()` / `phase()` / `log()` to orchestrate
10
- * - Subagents use the 13 tools to do real work (read config, run validation, log decisions,
11
- * review, triage, apply/rollback/fixing, checkpoint, status, history, prune)
10
+ * - Subagents use the 14 tools to do real work (read config, run validation, log decisions,
11
+ * review, triage, apply/rollback/fixing, checkpoint, status, history, prune, transcript)
12
+ * - A `tools/pre-execute` hook gates destructive iterate calls behind human approval
13
+ * (F8 observatory approval policy: ask / deny / allow).
12
14
  *
13
15
  * Tool invocation model:
14
16
  * - Workflow script CANNOT call tools directly (sandboxed vm, no Node API)
@@ -32,11 +34,14 @@ import { registerFixTool, registerDiffTool, registerRollbackTool } from "./tools
32
34
  import { registerCheckpointTool, registerStatusTool } from "./tools/checkpoint.js";
33
35
  import { registerHistoryTool } from "./tools/history.js";
34
36
  import { registerPruneTool } from "./tools/prune.js";
37
+ import { registerTranscriptTool } from "./tools/transcript.js";
38
+ import { registerSessionHooks } from "./session-hooks.js";
39
+ import { registerLiveCapture } from "./live.js";
35
40
  import { ITERATE_SKILL_PROMPT } from "./skill-prompt.js";
36
41
  export const name = 'iterate-plugin';
37
42
  export const inject = ['tools', 'systemPrompt'];
38
43
  export function apply(ctx) {
39
- // 1. Register the 13 tools
44
+ // 1. Register the 14 tools
40
45
  registerConfigTool(ctx);
41
46
  registerValidateTool(ctx);
42
47
  registerDecisionLogTool(ctx);
@@ -50,6 +55,11 @@ export function apply(ctx) {
50
55
  registerStatusTool(ctx);
51
56
  registerHistoryTool(ctx);
52
57
  registerPruneTool(ctx);
58
+ registerTranscriptTool(ctx);
59
+ // 2. Wire the observatory approval gate onto dsh's tools/pre-execute waterfall,
60
+ // and the live reviewer-activity feed onto tools/result.
61
+ registerSessionHooks(ctx);
62
+ registerLiveCapture(ctx);
53
63
  // 2. Inject the iterate skill prompt as a system prompt section
54
64
  // This teaches the model how to write iterate workflow scripts using the tools.
55
65
  ctx.systemPrompt.section({