iterate-plugin 2.11.0 → 2.12.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
@@ -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
 
@@ -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
 
@@ -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
+ }
@@ -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
  /**
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({
package/dist/live.js ADDED
@@ -0,0 +1,155 @@
1
+ /**
2
+ * src/live.ts — live reviewer-activity feed for the iterate observatory (F1 live).
3
+ *
4
+ * Watches `tools/result` and, for tool calls we can attribute to a project root
5
+ * (the caller agent's session cwd), appends one line to an append-only NDJSON
6
+ * file `.iterate/transcript-live.ndjson`. The `iterate_transcript` tool then
7
+ * mixes the most recent entries into its `read` / `capture` results so the
8
+ * client observatory shows what reviewers are doing in near-real-time (which
9
+ * files they read, which fixes/rollbacks/diffs land, where the run is).
10
+ *
11
+ * Why project-scoped (not per-thread):
12
+ * Tool executions carry the calling agent's session cwd but NOT the workflow
13
+ * sub-agent's `dimension` / `round` label, so we cannot reliably attribute a
14
+ * read to a specific reviewer thread without inventing data. We therefore
15
+ * record honest project-level activity and never fabricate an attribution.
16
+ * Per-thread narration stays the job of the final `iterate_transcript capture`.
17
+ *
18
+ * Safety:
19
+ * - Read-only observer: never mutates source files; writes only the NDJSON
20
+ * live file under `.iterate/`.
21
+ * - The live file is byte-capped (rewrite to last N lines when it grows too
22
+ * large) so it can never grow unbounded.
23
+ * - Any capture failure is swallowed (fire-and-forget) so it can never block
24
+ * or crash a tool call.
25
+ */
26
+ import { mkdir, readFile, writeFile, stat, appendFile, rename } from 'node:fs/promises';
27
+ import { existsSync } from 'node:fs';
28
+ import { join } from 'node:path';
29
+ import { resolveProjectRoot } from "./config-loader.js";
30
+ /** Keep at most this many live activity entries. */
31
+ export const LIVE_MAX_ENTRIES = 300;
32
+ /** Rewrite the live file when its byte size exceeds this threshold. */
33
+ export const LIVE_MAX_BYTES = 64 * 1024;
34
+ /** File path of the live NDJSON feed for a project root. */
35
+ export function liveFilePath(projectRoot) {
36
+ return join(projectRoot, '.iterate', 'transcript-live.ndjson');
37
+ }
38
+ /** Resolve the project root a tool execution belongs to, if any. */
39
+ function projectRootOf(exec) {
40
+ const cwd = exec.agent?.session?.header?.cwd;
41
+ if (!cwd)
42
+ return null;
43
+ const resolved = resolveProjectRoot(undefined, cwd);
44
+ return resolved.ok ? resolved.root : null;
45
+ }
46
+ /** Classify a settled tool call into a live activity entry, or null to skip. */
47
+ export function classifyTool(name, args, projectRoot) {
48
+ // `read_file` is the dsh-native file reader reviewers use to inspect code.
49
+ if (name === 'read_file') {
50
+ const file = args && typeof args === 'object' && typeof args.path === 'string'
51
+ ? args.path
52
+ : '';
53
+ return file ? { ts: new Date().toISOString(), type: 'read', tool: name, target: file } : null;
54
+ }
55
+ // The iterate plugin's own tools — surface what the workflow is doing live.
56
+ const records = {
57
+ iterate_fix: 'fix',
58
+ iterate_rollback: 'rollback',
59
+ iterate_diff: 'diff',
60
+ iterate_review: 'review',
61
+ iterate_triage: 'triage',
62
+ iterate_checkpoint: 'checkpoint',
63
+ iterate_validate: 'validate',
64
+ iterate_decision_log: 'log',
65
+ iterate_history: 'info',
66
+ iterate_prune: 'prune',
67
+ iterate_transcript: 'log',
68
+ iterate_status: 'info',
69
+ iterate_config: 'info',
70
+ iterate_context: 'info',
71
+ };
72
+ const type = records[name];
73
+ if (!type)
74
+ return null;
75
+ let target = '';
76
+ if (args && typeof args === 'object') {
77
+ const a = args;
78
+ if (typeof a.file === 'string' && a.file)
79
+ target = a.file;
80
+ else if (typeof a.path === 'string' && a.path)
81
+ target = a.path;
82
+ else if (typeof a.operation === 'string' && a.operation)
83
+ target = a.operation;
84
+ else if (name === 'iterate_rollback' && typeof a.id === 'string' && a.id) {
85
+ target = `fix ${a.id}`;
86
+ }
87
+ }
88
+ if (!target)
89
+ target = name;
90
+ return { ts: new Date().toISOString(), type, tool: name, target };
91
+ }
92
+ /** Append one activity record to the project's live feed (byte-capped). */
93
+ export async function appendLive(projectRoot, entry) {
94
+ const file = liveFilePath(projectRoot);
95
+ const line = JSON.stringify(entry) + '\n';
96
+ await mkdir(join(projectRoot, '.iterate'), { recursive: true });
97
+ // Amortized O(1): only read+rewrite when the file has grown past the cap.
98
+ try {
99
+ const st = await stat(file).catch(() => null);
100
+ if (st && st.size > LIVE_MAX_BYTES) {
101
+ const raw = await readFile(file, 'utf-8');
102
+ const lines = raw.split('\n').filter(Boolean);
103
+ const tail = lines.slice(-LIVE_MAX_ENTRIES);
104
+ const tmp = `${file}.trim.tmp`;
105
+ await writeFile(tmp, tail.join('\n') + '\n', 'utf-8');
106
+ await rename(tmp, file);
107
+ }
108
+ await appendFile(file, line, 'utf-8');
109
+ }
110
+ catch {
111
+ // Fire-and-forget: never let live capture break a tool call.
112
+ }
113
+ }
114
+ /** Read the live feed (newest first), capped at the last LIVE_MAX_ENTRIES. */
115
+ export async function readLive(projectRoot) {
116
+ const file = liveFilePath(projectRoot);
117
+ if (!existsSync(file))
118
+ return [];
119
+ try {
120
+ const raw = await readFile(file, 'utf-8');
121
+ const entries = [];
122
+ for (const line of raw.split('\n')) {
123
+ if (!line.trim())
124
+ continue;
125
+ try {
126
+ const parsed = JSON.parse(line);
127
+ if (parsed && typeof parsed.ts === 'string' && typeof parsed.type === 'string') {
128
+ entries.push(parsed);
129
+ }
130
+ }
131
+ catch {
132
+ // skip malformed lines
133
+ }
134
+ }
135
+ return entries.slice(-LIVE_MAX_ENTRIES).reverse();
136
+ }
137
+ catch {
138
+ return [];
139
+ }
140
+ }
141
+ /**
142
+ * Register a `tools/result` observer that captures reviewer activity into the
143
+ * project's live feed. Fire-and-forget; failures are swallowed.
144
+ */
145
+ export function registerLiveCapture(ctx) {
146
+ ctx.on('tools/result', (exec) => {
147
+ const root = projectRootOf(exec);
148
+ if (!root)
149
+ return;
150
+ const entry = classifyTool(exec.name, exec.arguments, root);
151
+ if (!entry)
152
+ return;
153
+ void appendLive(root, entry);
154
+ });
155
+ }
package/dist/paths.js CHANGED
@@ -30,3 +30,7 @@ export function fixBackupPath(projectRoot, id, timestamp) {
30
30
  export function checkpointPath(projectRoot) {
31
31
  return join(iterateDir(projectRoot), 'checkpoint.json');
32
32
  }
33
+ /** Runtime-observatory transcript file (JSON). */
34
+ export function transcriptPath(projectRoot) {
35
+ return join(iterateDir(projectRoot), 'transcript.json');
36
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * src/session-hooks.ts — dsh pipeline hooks for the iterate observatory (F8).
3
+ *
4
+ * Wires the {@link decideApproval} policy gate to dsh's `tools/pre-execute`
5
+ * waterfall. This is the AUTHORITATIVE approval seam for destructive iterate
6
+ * tools (`iterate_fix` / `iterate_rollback` / `iterate_prune` with dryRun:false):
7
+ *
8
+ * - `allow` policy → the call runs.
9
+ * - `deny` policy → the call is refused (fail-closed), surfaced as an
10
+ * error to the model.
11
+ * - `ask` policy → return `{ kind: 'ask', reason }`; dsh's own
12
+ * scheduler routes it through the `approval` service
13
+ * (see `@deepseek-ai/dsh-user-approval`), which
14
+ * prompts the human and audits an approve/deny pair
15
+ * on the session.
16
+ *
17
+ * We deliberately do NOT also add `approved` flags inside the tool bodies:
18
+ * the pre-execute waterfall consumes the human decision before the tool runs,
19
+ * so a second tool-internal gate would double-ask. This one gate is enough and
20
+ * stays dsh-native.
21
+ *
22
+ * Safety properties:
23
+ * - Read-only tools and non-iterate tools are always allowed (the gate only
24
+ * inspects the three destructive iterate toolnames).
25
+ * - If the project root / observatory config cannot be resolved, the policy
26
+ * degrades to `ask` (fail-safe: destructive writes always require consent).
27
+ */
28
+ import { loadEffectiveConfig, resolveProjectRoot } from "./config-loader.js";
29
+ import { decideApproval, isDestructiveIterateTool } from "./approval-gate.js";
30
+ /**
31
+ * Build the per-call approval decision for a tool execution.
32
+ * Returns a dsh `PreToolDecision` so the caller can short-circuit the caller.
33
+ */
34
+ export function gateDecision(exec) {
35
+ // Importing the decision, and only inspecting our own tools, keeps unrelated
36
+ // tooling untouched. Anything we cannot classify is allowed by default.
37
+ if (!isDestructiveIterateTool(exec.name))
38
+ return { kind: 'allow' };
39
+ // Resolve the project root (use the call's own `path` arg, else the agent's
40
+ // session cwd) to read the effective observatory policy.
41
+ const argPath = typeof exec.arguments === 'object' && exec.arguments && !Array.isArray(exec.arguments)
42
+ && typeof exec.arguments.path === 'string'
43
+ ? exec.arguments.path
44
+ : undefined;
45
+ const sessionCwd = exec.agent?.session?.header?.cwd;
46
+ const resolved = resolveProjectRoot(argPath, sessionCwd);
47
+ let policy = 'ask';
48
+ if (resolved.ok) {
49
+ const { config } = loadEffectiveConfig(resolved.root);
50
+ const p = config.observatory?.approval;
51
+ if (p === 'deny')
52
+ policy = 'deny';
53
+ else if (p === 'allow')
54
+ policy = 'allow';
55
+ // anything else (including a corrupt/missing `ask`) → 'ask'
56
+ }
57
+ const decision = decideApproval(exec, policy);
58
+ if (decision.kind === 'deny')
59
+ return { kind: 'deny', reason: decision.reason };
60
+ if (decision.kind === 'ask')
61
+ return { kind: 'ask', reason: decision.reason };
62
+ return { kind: 'allow' };
63
+ }
64
+ /**
65
+ * Register the `tools/pre-execute` waterfall listener that applies the
66
+ * observatory approval gate to every destructive iterate tool call.
67
+ */
68
+ export function registerSessionHooks(ctx) {
69
+ ctx.on('tools/pre-execute', (exec, next) => {
70
+ // Never let a throwing gate break the pipeline — degrade to allow.
71
+ let decision;
72
+ try {
73
+ decision = gateDecision(exec);
74
+ }
75
+ catch {
76
+ return next();
77
+ }
78
+ if (decision.kind === 'ask') {
79
+ // Delegate the actual human-consent prompt + audit to dsh's approval
80
+ // service via the scheduler's `ask` path. `next()` here would short-circuit
81
+ // to allow, which would bypass consent — so return our ask decision.
82
+ return Promise.resolve(decision);
83
+ }
84
+ if (decision.kind === 'deny') {
85
+ return Promise.resolve(decision);
86
+ }
87
+ return next();
88
+ });
89
+ }