iterate-plugin 3.6.0 → 3.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 +37 -6
- package/README.zh-CN.md +29 -5
- package/dist/config-loader.js +7 -1
- package/dist/file-lock.js +40 -12
- package/dist/index.js +7 -0
- package/dist/live.js +2 -1
- package/dist/session-hooks.js +6 -1
- package/dist/tools/decision-log.js +4 -10
- package/dist/working-directory.js +114 -0
- package/lib/client.js +1097 -461
- package/lib/parse.js +253 -58
- package/package.json +16 -11
- package/src/client/i18n.ts +517 -0
- package/src/client/index.ts +531 -340
- package/src/client/session-cache.ts +54 -0
- package/src/config-loader.ts +7 -1
- package/src/file-lock.ts +36 -9
- package/src/index.ts +7 -0
- package/src/live.ts +2 -1
- package/src/session-hooks.ts +6 -1
- package/src/tools/decision-log.ts +4 -6
- package/src/working-directory.ts +126 -0
package/README.md
CHANGED
|
@@ -104,7 +104,7 @@ Both modes share:
|
|
|
104
104
|
- Validates after each round's fixes
|
|
105
105
|
- Rolls back failed fixes, and can save / resume progress
|
|
106
106
|
|
|
107
|
-
### UI layer (build-free client slots, v3.
|
|
107
|
+
### UI layer (build-free client slots, v3.7: 11 tabs, bilingual)
|
|
108
108
|
|
|
109
109
|
- **ConvergenceDashboard** (`conversation.input.dock`) — live round progress bar, severity stats, dimension badges, trend mini-chart above the input; normal mode also shows fix-count badges; plus a live workflow-phase chip (current phase + running/stopped) and a **task_mode indicator (code / iterate)**, now backed by the persisted transcript (`iterate_status` / `iterate_transcript` emit `taskMode`). While a run is active the dashboard switches to a running state (round + phase) instead of the static empty state, and the start buttons are hidden.
|
|
110
110
|
- **ObservatoryPanel** (`conversation.input.dock`) — an **eleven-tab** runtime observatory below the input: **live activity stream** (default tab, type filter) + **F1** review threads (expand / collapse all), **F2** convergence trend (incl. the per-round comparison table: findings raised / fixed / severity mix / validation outcomes), **F3** finding locations (severity / dimension / search filter), **F4** fixes + rollback, **F5** checkpoint resume, **F6** run console, **F7** decision timeline (type / round filter + search), **F8** Quality Gate, **F9** Experience Bank, **F10** Defense Events. Number-key shortcuts: `1`–`9` jump to F1–F9, `0` to F10, `-` back to the live stream. **F8/F9/F10 are live** — they scan the session stream for the latest `iterate_quality_gate` / `iterate_experience` / `iterate_defense_events` results and render real data (PASS/FAIL chip + per-dimension score bars, experience entries with adopt buttons + client-side search, defense type-count chips + filtered event stream); when the session stream has none they distinguish "no data on disk" from "not pulled yet" and offer a **拉取磁盘快照 (pull disk snapshot)** instruction that has the model call the read-side tools and echo the `.iterate/` artifacts back into the stream. One-click JSON export of all observatory data — manifest + live + `qualityGate` / `experienceBank` / `defenseEvents` / `report` (download, copy fallback). **v3.5.3: reset actions surfaced** — F5 copies an `iterate_checkpoint clear` instruction, F8 an `iterate_quality_gate clear` instruction, F10 an `iterate_defense_events clear` instruction.
|
|
@@ -112,7 +112,8 @@ Both modes share:
|
|
|
112
112
|
- **StatsCard** (`conversation.chat.turnTail`) — convergence stats, round history, trend chart and completion summary when no findings remain.
|
|
113
113
|
- **iterate theme skin** (`theme.overrideTokens`) — a warm-amber 13-token `--dsw-*` override, light/dark modes, togglable in settings.
|
|
114
114
|
- **ProgressCapsule** (`shell.overlay`) — popup notification on each round completion / convergence (incl. convergence confirm).
|
|
115
|
-
- **SettingsPanel** (`settings.section`) — theme toggle, triage-persistence notes, config-management guide (covers `validation.commands` / `language` / `personalization.known_intentional` with an explicit "empty validation.commands = unprotected" warning; a field picker generates the matching `iterate_config write` instruction), runtime status overview (artifact layout + view/cleanup tool guide + disk-snapshot pull), one-click triage data reset.
|
|
115
|
+
- **SettingsPanel** (`settings.section`) — theme toggle, triage-persistence notes, config-management guide (covers `validation.commands` / `language` / `personalization.known_intentional` with an explicit "empty validation.commands = unprotected" warning; a field picker generates the matching `iterate_config write` instruction), **v3.7: a 回读当前配置 (read back current config) button that closes the write → read-back loop** — it copies an `iterate_config read` instruction and, once the model echoes the result into the session, shows the current value of the selected field, runtime status overview (artifact layout + view/cleanup tool guide + disk-snapshot pull), one-click triage data reset.
|
|
116
|
+
- **Bilingual UI (v3.7)** — every piece of UI chrome (panels, empty states, tabs, badges, settings, stop-reason and completion labels) ships in 简体中文 and English. Pick the language under **Settings → 界面语言 / Interface language**; the choice persists in `localStorage` (`iterate-ui-lang`) and otherwise auto-detects from `navigator.language` (a stored choice always wins, and auto-detection never writes a preference). **Model-facing copy deliberately stays Chinese** — the `build*Instruction` payloads and the copy-paste guides are contract text mirroring `SKILL.md`, so the two expanded guides carry a 「中文(粘贴给模型的契约文案)」 chip instead of looking like a missed translation.
|
|
116
117
|
|
|
117
118
|
The UI layer is **defensive by design**: it degrades gracefully if any of `slots` / `theme` / `React` is unavailable — it never crashes the client.
|
|
118
119
|
|
|
@@ -125,7 +126,8 @@ Beyond the 17 registered tools (full reference further down), the plugin closes
|
|
|
125
126
|
- **Breakpoint resume** — checkpoints saved at the start of each round; interrupted long iterations can resume
|
|
126
127
|
- **History audit** — `iterate_history` reads the decision log (filtered by `type` / `since` / `round` / fixed `file` / `limit`, default latest 50, cap 200) plus a fix-registry summary (per-round fixed/failed counts, scoped by the same round/file filters) to audit run process and fix details
|
|
127
128
|
- **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
|
|
128
|
-
- **Config read / write** — `iterate_config` supports validated, backed-up, rollback-capable partial writes
|
|
129
|
+
- **Config read / write** — `iterate_config` supports validated, backed-up, rollback-capable partial writes; since v3.7 the settings page can also **read the effective config back** (instruction + inline current value)
|
|
130
|
+
- **Session-aware project root (v3.7)** — on dsh ≥ `0.2.1-alpha.2` the plugin follows the session's *mutable* working directory through the host's `WorkingDirectoryService` instead of the immutable `Session.header.cwd`, so a mid-session directory switch redirects everything under `.iterate/` (config, decision log, checkpoints, fix backups, approval policy) to the workspace actually in use. The service is feature-detected with a `header.cwd` fallback on every failure mode, so older hosts behave exactly as before.
|
|
129
131
|
- **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
|
|
130
132
|
- **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`)
|
|
131
133
|
- **v3.2 Defense Events** — `iterate_defense_events` queries defense events (precondition failures, rollbacks, invariant violations, assumption falsifications) and can `record` new ones; readable labels follow the project language (en / zh)
|
|
@@ -209,6 +211,8 @@ Press `1`–`9` to jump to F1–F9, `0` for F10, and `-` to return to the live s
|
|
|
209
211
|
|
|
210
212
|
The settings section copies a config-edit guide that lists the supported `iterate.config.yaml` fields — including `validation.commands` (with an explicit warning that an empty list means every round runs unprotected) — and offers a **field picker**: choose a field, and the button generates the matching `iterate_config write` instruction (schema-validated, auto-backup, auto-rollback) for you to paste back.
|
|
211
213
|
|
|
214
|
+
**Reading back what actually landed** — the matching **回读当前配置 / Read back current config** button copies an `iterate_config` `{ operation: "read" }` instruction; when the model echoes the result into the session, the card shows the current value of the selected field right under the picker (arrays inline, e.g. `["npm test","npm run lint"]`). Three states are told apart honestly: *nothing read back yet*, *the value*, and *read back but this field is absent* (the built-in defaults are in play). If the clipboard refuses, the read instruction is revealed in the panel for manual copy — the button is never a dead end.
|
|
215
|
+
|
|
212
216
|
---
|
|
213
217
|
|
|
214
218
|
## 💬 Usage
|
|
@@ -338,9 +342,10 @@ All runtime state lives under `.iterate/` at the project root (can be excluded v
|
|
|
338
342
|
`0.1.2-rc.1`, `0.1.3-alpha.1`, `0.1.5-alpha.1`, `0.1.5-alpha.2`,
|
|
339
343
|
`0.1.5-rc.1`, `0.1.5-rc.2`, `0.1.6-alpha.1`, `0.1.6-alpha.2`,
|
|
340
344
|
`0.1.7-alpha.1`, `0.1.7-alpha.2`, `0.1.7-rc.1`, `0.1.7-rc.2`, `0.2.0-rc.1`,
|
|
341
|
-
`0.2.0-rc.2`, `0.2.1-alpha.1` (
|
|
345
|
+
`0.2.0-rc.2`, `0.2.1-alpha.1`, `0.2.1-alpha.2` (19 releases, `0.1.1-rc.1` …
|
|
346
|
+
`0.2.1-alpha.2`).
|
|
342
347
|
The plugin is built against `@deepseek-ai/dsh-tools` /
|
|
343
|
-
`@deepseek-ai/dsh-util-values` `0.2.1-alpha.
|
|
348
|
+
`@deepseek-ai/dsh-util-values` `0.2.1-alpha.2` and uses only public contracts
|
|
344
349
|
(tool registration, client slots + theme, bundle patch).
|
|
345
350
|
- **Disposable-Profile evidence (real, `dsh` CLI `0.1.1-rc.1`)**: on a temp
|
|
346
351
|
`$DSH_HOME`, `dsh plugin --profile <p> add <this-repo>` installed the bundle in
|
|
@@ -380,7 +385,33 @@ following capabilities; elevated capability means a DSH Profile install stays
|
|
|
380
385
|
### Dependencies
|
|
381
386
|
|
|
382
387
|
Runtime dependencies: `@deepseek-ai/cordis`, `@deepseek-ai/dsh-tools`,
|
|
383
|
-
`@deepseek-ai/dsh-util-values`, `js-yaml` — all pinned exact versions.
|
|
388
|
+
`@deepseek-ai/dsh-util-values`, `js-yaml` — all pinned exact versions.
|
|
389
|
+
|
|
390
|
+
#### Upstream adoption (v3.7.0 → dsh `0.2.1-alpha.2`)
|
|
391
|
+
|
|
392
|
+
- **Adopted — the mutable session working directory.** `src/working-directory.ts`
|
|
393
|
+
resolves the project root through `ctx.get('workingDirectory').get(session)`
|
|
394
|
+
when the host offers it (feature-detected; `header.cwd` fallback on a missing
|
|
395
|
+
service, a throwing service, hostile getters or a non-string result). Needed
|
|
396
|
+
because as of `0.2.1-alpha.2` the immutable header keeps the *original*
|
|
397
|
+
project by design, so reading it after a directory switch would anchor the
|
|
398
|
+
review to the wrong checkout.
|
|
399
|
+
- **Evaluated, not adopted:**
|
|
400
|
+
- *Session state-record APIs* (experimental plugins writing/reading their own
|
|
401
|
+
session records) — reaching into shell-owned record internals is fragile
|
|
402
|
+
across alpha releases, and iterate's cross-session state already lives in
|
|
403
|
+
`.iterate/`.
|
|
404
|
+
- *Shared `AGENTS.md` agents directory (`DSH_AGENTS_HOME`)* — a host-level
|
|
405
|
+
instruction concern; the plugin injects its own workflow prompt.
|
|
406
|
+
- *Experimental Git Worktrees plugin* — an upstream agent-workflow feature;
|
|
407
|
+
the plugin's own `git.use_worktree` config predates it and is unrelated
|
|
408
|
+
(per-round git hygiene for the iterate loop).
|
|
409
|
+
- *Removal of the `both` tool-presentation mode* — the plugin never set a
|
|
410
|
+
presentation mode (it renders `presentCall` cards), so nothing to migrate.
|
|
411
|
+
- *Installing the dsh CLI globally from the plugin* — disk cost for a plugin
|
|
412
|
+
that never shells out at runtime.
|
|
413
|
+
- All five `@deepseek-ai/*` packages are pinned to the exact `0.2.1-alpha.2`
|
|
414
|
+
(`@deepseek-ai/cordis` stays `4.0.5-alpha.1`). No
|
|
384
415
|
install-time lifecycle scripts (`preinstall`/`install`/`postinstall`/`prepare`);
|
|
385
416
|
only `prepublishOnly` (build) runs on publish.
|
|
386
417
|
|
package/README.zh-CN.md
CHANGED
|
@@ -103,7 +103,7 @@ dsh plugin --profile web add iterate-plugin
|
|
|
103
103
|
- 每轮修复后验证
|
|
104
104
|
- 修复失败自动回滚
|
|
105
105
|
|
|
106
|
-
### UI 层(客户端免构建槽位,v3.
|
|
106
|
+
### UI 层(客户端免构建槽位,v3.7:11 个页签,双语)
|
|
107
107
|
|
|
108
108
|
- **收敛看板 `ConvergenceDashboard`**(`conversation.input.dock`)— 输入框上方实时显示轮次进度条、严重度统计、维度徽章、趋势迷你图;normal 模式另显示修复计数徽章;还有运行阶段芯片(当前工作流阶段 + 运行中/已结束)与 **task_mode 指示器(code / iterate)**。运行期间看板切换为"运行中"状态(显示轮次与阶段),并隐藏启动按钮。
|
|
109
109
|
- **运行时观测台 `ObservatoryPanel`**(`conversation.input.dock`)— 输入框下方**十一个页签**运行时观测台:**实时活动流**(默认页,支持按活动类型筛选)+ **F1** 审查线程(全部展开/全部收起)、**F2** 收敛趋势(含跨轮对比表:每轮 findings / fixed / 严重度分布 / 验证结果)、**F3** 发现定位(按严重度/维度/关键词筛选)、**F4** 修复与回滚、**F5** 断点恢复、**F6** 运行控制台、**F7** 决策时间线(按类型/轮次筛选与关键词搜索)、**F8** 质量门禁、**F9** 经验银行、**F10** 防御事件。数字键快捷键:`1`–`9` 跳到 F1–F9,`0` 到 F10,`-` 回实时活动流。**F8/F9/F10 为实时数据**——扫描会话流中最近一次 `iterate_quality_gate` / `iterate_experience` / `iterate_defense_events` 结果并渲染真实数据(PASS/FAIL 徽章 + 分维度评分条、经验条目(含采纳按钮 + 客户端搜索)、防御类型计数芯片 + 筛选后的事件流);会话流中没有数据时区分"磁盘无数据"与"尚未拉取",并提供**拉取磁盘快照**指令(让模型调用只读工具、把 `.iterate/` 产物回显进会话流)。一键导出全部观测数据为 JSON——manifest + live + `qualityGate` / `experienceBank` / `defenseEvents` / `report`(优先下载,失败回退复制)。**v3.5.3 新增重置操作**:F5 复制 `iterate_checkpoint clear`、F8 复制 `iterate_quality_gate clear`、F10 复制 `iterate_defense_events clear` 指令。
|
|
@@ -111,7 +111,8 @@ dsh plugin --profile web add iterate-plugin
|
|
|
111
111
|
- **收敛统计卡片 `StatsCard`**(`conversation.chat.turnTail`)— 无 findings 时显示收敛统计、历史轮次、趋势图、完成摘要。
|
|
112
112
|
- **iterate 主题皮肤**(`theme.overrideTokens`)— 暖琥珀配色的 13 个 `--dsw-*` token 覆盖,明暗双模式,可在设置页开关。
|
|
113
113
|
- **进度胶囊 `ProgressCapsule`**(`shell.overlay`)— 每轮完成 / 收敛时右下角弹出通知(含收敛确认)。
|
|
114
|
-
- **iterate 设置区 `SettingsPanel`**(`settings.section`)— 主题开关、分诊持久化说明、配置管理指引(覆盖 `validation.commands` / `language` / `personalization.known_intentional`,并显式警告"validation.commands 为空 = 本轮不受任何测试保护";字段选择器可生成对应的 `iterate_config write`
|
|
114
|
+
- **iterate 设置区 `SettingsPanel`**(`settings.section`)— 主题开关、分诊持久化说明、配置管理指引(覆盖 `validation.commands` / `language` / `personalization.known_intentional`,并显式警告"validation.commands 为空 = 本轮不受任何测试保护";字段选择器可生成对应的 `iterate_config write` 指令),**v3.7:新增「回读当前配置」补齐写入 → 回读闭环**——复制 `iterate_config read` 指令,模型把结果回显进会话后,卡片就地展示所选字段的当前值;运行时状态概览(产物布局 + 查看/清理工具指引 + 磁盘快照拉取)、一键清空分诊数据。
|
|
115
|
+
- **双语 UI(v3.7)**——所有界面文案(面板、空态、页签、徽标、设置、停止原因与完成汇总)均提供简体中文与 English。在**设置 → 界面语言 / Interface language**切换;选择持久化在 `localStorage`(`iterate-ui-lang`),否则按 `navigator.language` 自动探测(已保存的选择始终优先,且自动探测不会写入偏好)。**面向模型的文案有意保持中文**——`build*Instruction` 指令与可粘贴指引是与 `SKILL.md` 对齐的契约文本,因此两处展开的指引带「中文(粘贴给模型的契约文案)」标签,避免被误当成漏翻。
|
|
115
116
|
|
|
116
117
|
UI 层为**防御式设计**:`slots` / `theme` / `React` 任一不可用时自动降级,不会崩溃客户端。
|
|
117
118
|
|
|
@@ -124,7 +125,8 @@ UI 层为**防御式设计**:`slots` / `theme` / `React` 任一不可用时自
|
|
|
124
125
|
- **断点续跑** — 长迭代在每轮开头保存 checkpoint,中断后可恢复进度
|
|
125
126
|
- **历史审计** — `iterate_history` 读取决策日志(可按 `type` / `since` / `round` / 修复文件 `file` / `limit` 过滤,默认取最新 50 条,上限 200 条)与修复注册表汇总(各轮 fixed/failed 计数,按同一 round/file 过滤收窄),审查运行过程与修复明细
|
|
126
127
|
- **运行时清理** — `iterate_prune` 清理过期的决策日志条目、陈旧断点、孤儿修复备份、空轮次与原子写入崩溃残留的临时文件;默认 dry-run 只报告不删除,显式 `dryRun:false` 才真正清理,每次清理写入决策日志
|
|
127
|
-
- **配置读写** — `iterate_config`
|
|
128
|
+
- **配置读写** — `iterate_config` 支持带校验、备份、回滚的局部写入;v3.7 起设置页还能**回读生效配置**(指令 + 就地展示当前值)
|
|
129
|
+
- **跟随会话的工作目录(v3.7)** — 在 dsh ≥ `0.2.1-alpha.2` 上,插件通过宿主的 `WorkingDirectoryService` 解析项目根,而不是不可变的 `Session.header.cwd`:会话中途切换目录后,`.iterate/` 下的一切(配置、决策日志、断点、修复备份、审批策略)都会跟着当前工作区走。该服务按特性探测,任何失败路径(服务缺失/抛错/敌意 getter/非字符串结果)都回退到 `header.cwd`,旧宿主行为完全不变。
|
|
128
130
|
- **v3.2 经验银行** — `iterate_experience` 以检索 / 过滤 / 采纳查阅历史修复与模式,并可 `add` 持久化新的已验证修复——重复添加同一 pattern + dimension 时累加命中次数而非重复写入
|
|
129
131
|
- **v3.2 质量门禁** — `iterate_quality_gate` 读取质量门禁状态(各维度收敛率 + PASS/FAIL),并可基于本轮 findings / 验证结果 `compute` 重新计算并持久化一份新的质量凭证(收敛率来自 `findingsByRound` 的真实收敛序列)
|
|
130
132
|
- **v3.2 防御事件流** — `iterate_defense_events` 查询防御事件(前置条件失败、回滚、不变量违反、假设证伪),并可 `record` 记录新事件;可读标签跟随项目语言(en / zh)
|
|
@@ -208,6 +210,8 @@ pnpm add /path/to/iterate-skill/harness/iterate-plugin
|
|
|
208
210
|
|
|
209
211
|
设置区复制一份配置编辑指引,列出支持的 `iterate.config.yaml` 字段——包括 `validation.commands`(并显式警告:留空意味着每一轮迭代都不受任何测试保护)——并提供**字段选择器**:选中字段后,按钮会生成对应的 `iterate_config write` 指令(schema 校验、自动备份、失败自动回滚)供你粘贴回会话。
|
|
210
212
|
|
|
213
|
+
**回读实际写入的结果**——配套的**「回读当前配置」**按钮复制 `iterate_config` 的 `{ operation: "read" }` 指令;模型把结果回显进会话后,卡片会在选择器下方展示所选字段的当前值(数组内联展示,如 `["npm test","npm run lint"]`)。三种状态被诚实区分:*尚未回读*、*当前值*、*已回读但没有该字段*(说明走的是内置默认值)。若剪贴板被拒绝,回读指令会在面板中展开供手动复制——按钮不会成为死路。
|
|
214
|
+
|
|
211
215
|
---
|
|
212
216
|
|
|
213
217
|
## 💬 使用
|
|
@@ -330,7 +334,7 @@ validation:
|
|
|
330
334
|
### Node.js / DSH 兼容范围
|
|
331
335
|
|
|
332
336
|
- **Node.js**:`>=20`(`package.json` 的 `engines.node`)。
|
|
333
|
-
- **DSH**:`package.json` 的 `dsh.compatibility.dshReleases` 字段是发布兼容性的**唯一真源**(本 README 逐字镜像该字段;两者需同步更新)。以下官方版本声明为 `compatible`:`0.1.1-rc.1`、`0.1.2-alpha.4`、`0.1.2-alpha.5`、`0.1.2-rc.1`、`0.1.3-alpha.1`、`0.1.5-alpha.1`、`0.1.5-alpha.2`、`0.1.5-rc.1`、`0.1.5-rc.2`、`0.1.6-alpha.1`、`0.1.6-alpha.2`、`0.1.7-alpha.1`、`0.1.7-alpha.2`、`0.1.7-rc.1`、`0.1.7-rc.2`、`0.2.0-rc.1`、`0.2.0-rc.2`、`0.2.1-alpha.1`(共
|
|
337
|
+
- **DSH**:`package.json` 的 `dsh.compatibility.dshReleases` 字段是发布兼容性的**唯一真源**(本 README 逐字镜像该字段;两者需同步更新)。以下官方版本声明为 `compatible`:`0.1.1-rc.1`、`0.1.2-alpha.4`、`0.1.2-alpha.5`、`0.1.2-rc.1`、`0.1.3-alpha.1`、`0.1.5-alpha.1`、`0.1.5-alpha.2`、`0.1.5-rc.1`、`0.1.5-rc.2`、`0.1.6-alpha.1`、`0.1.6-alpha.2`、`0.1.7-alpha.1`、`0.1.7-alpha.2`、`0.1.7-rc.1`、`0.1.7-rc.2`、`0.2.0-rc.1`、`0.2.0-rc.2`、`0.2.1-alpha.1`、`0.2.1-alpha.2`(共 19 个版本,`0.1.1-rc.1` … `0.2.1-alpha.2`)。插件基于 `@deepseek-ai/dsh-tools` / `@deepseek-ai/dsh-util-values` `0.2.1-alpha.2` 构建,只使用公开契约(工具注册、客户端 slots + theme、bundle patch)。
|
|
334
338
|
- **一次性 Profile 证据(真实,`dsh` CLI `0.1.1-rc.1`)**:在临时 `$DSH_HOME` 下
|
|
335
339
|
`dsh plugin --profile <p> add <本仓库>` 约 449ms 完成安装;`dsh --profile <p>
|
|
336
340
|
--dump-config` 正确合成 `iterate-plugin` bundle patch;`dsh plugin --profile <p>
|
|
@@ -361,7 +365,27 @@ validation:
|
|
|
361
365
|
### 依赖
|
|
362
366
|
|
|
363
367
|
运行时依赖:`@deepseek-ai/cordis`、`@deepseek-ai/dsh-tools`、
|
|
364
|
-
`@deepseek-ai/dsh-util-values`、`js-yaml
|
|
368
|
+
`@deepseek-ai/dsh-util-values`、`js-yaml`——全部精确锁版。
|
|
369
|
+
|
|
370
|
+
#### 上游采纳记录(v3.7.0 → dsh `0.2.1-alpha.2`)
|
|
371
|
+
|
|
372
|
+
- **已采纳——可变的会话工作目录。** `src/working-directory.ts` 通过
|
|
373
|
+
`ctx.get('workingDirectory').get(session)` 解析项目根(特性探测;服务缺失、
|
|
374
|
+
抛错、敌意 getter 或非字符串结果一律回退 `header.cwd`)。必须采纳的原因:
|
|
375
|
+
自 `0.2.1-alpha.2` 起,不可变的 header 按设计保留*原始*项目,切换目录后再读它
|
|
376
|
+
会把评审锚定到错误的检出。
|
|
377
|
+
- **已评估、未采纳:**
|
|
378
|
+
- *Session 状态记录接口*(实验插件读写自己的会话记录)——依赖 shell 内部记录结构,
|
|
379
|
+
跨 alpha 版本极脆弱;iterate 的跨会话状态本就存放在 `.iterate/`。
|
|
380
|
+
- *共享 agents 目录的 `AGENTS.md`(`DSH_AGENTS_HOME`)*——属于宿主的全局指令机制,
|
|
381
|
+
插件注入的是自己的工作流提示词。
|
|
382
|
+
- *实验性 Git Worktrees 插件*——上游的 agent 工作流特性;插件自己的
|
|
383
|
+
`git.use_worktree` 配置早于它,且是另一回事(iterate 循环的逐轮 git 卫生)。
|
|
384
|
+
- *移除 `both` 混合展示模式*——插件从未设置展示模式(用的是 `presentCall` 卡片),
|
|
385
|
+
无需迁移。
|
|
386
|
+
- *从插件全局安装 dsh CLI*——运行时从不 shell 调用,纯占磁盘。
|
|
387
|
+
- 五个 `@deepseek-ai/*` 包全部精确锁到 `0.2.1-alpha.2`
|
|
388
|
+
(`@deepseek-ai/cordis` 保持 `4.0.5-alpha.1`)。无安装期生命周期脚本
|
|
365
389
|
(`preinstall`/`install`/`postinstall`/`prepare`);仅 `prepublishOnly`(构建)在发布时运行。
|
|
366
390
|
|
|
367
391
|
### 失败边界
|
package/dist/config-loader.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
4
4
|
import yaml from 'js-yaml';
|
|
5
|
+
import { sessionCwdOf } from "./working-directory.js";
|
|
5
6
|
/**
|
|
6
7
|
* Load and parse iterate.config.yaml from the project root.
|
|
7
8
|
* Returns null if the file is missing or invalid.
|
|
@@ -701,9 +702,14 @@ export function resolveProjectRoot(input, sessionCwd) {
|
|
|
701
702
|
/**
|
|
702
703
|
* Thin adapter for tool `execute(args, exec)` bodies: pull the session cwd
|
|
703
704
|
* from the DSH run context and hand it to {@link resolveProjectRoot}.
|
|
705
|
+
*
|
|
706
|
+
* The cwd source is the EFFECTIVE session directory (dsh 0.2.1-alpha.2's
|
|
707
|
+
* `workingDirectory` service when the host provides one, else the immutable
|
|
708
|
+
* `header.cwd` — see `./working-directory.ts`), so a session whose directory
|
|
709
|
+
* was switched mid-run still resolves the project it is actually editing.
|
|
704
710
|
*/
|
|
705
711
|
export function resolveProjectRootForExec(exec, input) {
|
|
706
|
-
return resolveProjectRoot(input, exec
|
|
712
|
+
return resolveProjectRoot(input, sessionCwdOf(exec));
|
|
707
713
|
}
|
|
708
714
|
/**
|
|
709
715
|
* Resolve the default working directory for tools invoked without an explicit
|
package/dist/file-lock.js
CHANGED
|
@@ -135,20 +135,48 @@ export function acquireProjectLock(projectRoot, name, options = {}) {
|
|
|
135
135
|
catch {
|
|
136
136
|
// best-effort
|
|
137
137
|
}
|
|
138
|
-
|
|
139
|
-
return () => {
|
|
140
|
-
if (released)
|
|
141
|
-
return;
|
|
142
|
-
released = true;
|
|
143
|
-
try {
|
|
144
|
-
unlinkSync(lockPath);
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
// already gone
|
|
148
|
-
}
|
|
149
|
-
};
|
|
138
|
+
return makeLockRelease(lockPath);
|
|
150
139
|
}
|
|
151
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Build the release function for a lock file this process just acquired.
|
|
143
|
+
*
|
|
144
|
+
* Advisory locks here are STOLEN when the recorded owner dies or the mtime
|
|
145
|
+
* goes ancient: the steal unlinks our file and creates a new one stamped with
|
|
146
|
+
* the STEALER's pid. An unguarded release would then unlink the new holder's
|
|
147
|
+
* lock — turning our release into a double-release that admits a third process
|
|
148
|
+
* into a "held" critical section. So the release re-reads the stamp and only
|
|
149
|
+
* unlinks a file that is still ours (same pid) or unstamped (the shape left
|
|
150
|
+
* when our own advisory pid write failed, or when the content is not a pid).
|
|
151
|
+
*
|
|
152
|
+
* Always callable, idempotent, and never throws.
|
|
153
|
+
*/
|
|
154
|
+
export function makeLockRelease(lockPath) {
|
|
155
|
+
let released = false;
|
|
156
|
+
return () => {
|
|
157
|
+
if (released)
|
|
158
|
+
return;
|
|
159
|
+
released = true;
|
|
160
|
+
let raw = '';
|
|
161
|
+
try {
|
|
162
|
+
raw = readFileSync(lockPath, 'utf-8').trim();
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Already gone (or unreadable) — nothing of ours left to remove.
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// A stamp naming another pid means the lock was stolen and reassigned:
|
|
169
|
+
// it is no longer ours to delete.
|
|
170
|
+
if (raw !== '' && Number(raw) !== process.pid)
|
|
171
|
+
return;
|
|
172
|
+
try {
|
|
173
|
+
unlinkSync(lockPath);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// already gone
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
152
180
|
/**
|
|
153
181
|
* Run `fn` while holding the advisory lock named `name`.
|
|
154
182
|
* The release is guaranteed via `finally`; `fn`'s value or error propagates
|
package/dist/index.js
CHANGED
|
@@ -43,6 +43,7 @@ import { registerQualityGateTool } from "./tools/quality-gate.js";
|
|
|
43
43
|
import { registerDefenseEventsTool } from "./tools/defense-events.js";
|
|
44
44
|
import { registerSessionHooks } from "./session-hooks.js";
|
|
45
45
|
import { registerLiveCapture } from "./live.js";
|
|
46
|
+
import { installWorkingDirectoryContext } from "./working-directory.js";
|
|
46
47
|
import { ITERATE_SKILL_PROMPT } from "./skill-prompt.js";
|
|
47
48
|
export const name = 'iterate-plugin';
|
|
48
49
|
export const inject = ['tools', 'systemPrompt'];
|
|
@@ -54,6 +55,12 @@ export function apply(ctx) {
|
|
|
54
55
|
// running with NO `tools/pre-execute` approval gate — a fail-open hole
|
|
55
56
|
// created by load order. Registering the gate first means a partial load
|
|
56
57
|
// can only ever err on the side of MORE gating, never less.
|
|
58
|
+
//
|
|
59
|
+
// The working-directory context is installed first for the same reason:
|
|
60
|
+
// every cwd read below (gate policy, live capture, tool project roots)
|
|
61
|
+
// must see dsh 0.2.1-alpha.2+ directory switches, and installing it last
|
|
62
|
+
// would leave a partially-loaded plugin resolving the wrong workspace.
|
|
63
|
+
installWorkingDirectoryContext(ctx);
|
|
57
64
|
registerSessionHooks(ctx);
|
|
58
65
|
registerLiveCapture(ctx);
|
|
59
66
|
// 1. Register the 17 tools (14 original + 3 v3.0). Each registration is
|
package/dist/live.js
CHANGED
|
@@ -31,6 +31,7 @@ import { existsSync } from 'node:fs';
|
|
|
31
31
|
import { writeTextAtomicAsync } from "./atomic-fs.js";
|
|
32
32
|
import { join } from 'node:path';
|
|
33
33
|
import { loadEffectiveConfig, resolveProjectRoot } from "./config-loader.js";
|
|
34
|
+
import { sessionCwdOf } from "./working-directory.js";
|
|
34
35
|
/** Keep at most this many live activity entries. */
|
|
35
36
|
export const LIVE_MAX_ENTRIES = 300;
|
|
36
37
|
/** Rewrite the live file when its byte size exceeds this threshold. */
|
|
@@ -107,7 +108,7 @@ export function appendLive(projectRoot, entry) {
|
|
|
107
108
|
}
|
|
108
109
|
/** Resolve the project root a tool execution belongs to, if any. */
|
|
109
110
|
function projectRootOf(exec) {
|
|
110
|
-
const cwd = exec
|
|
111
|
+
const cwd = sessionCwdOf(exec);
|
|
111
112
|
if (!cwd)
|
|
112
113
|
return null;
|
|
113
114
|
const resolved = resolveProjectRoot(undefined, cwd);
|
package/dist/session-hooks.js
CHANGED
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
*/
|
|
55
55
|
import { loadEffectiveConfig, resolveProjectRoot } from "./config-loader.js";
|
|
56
56
|
import { decideApproval, isDestructiveIterateTool } from "./approval-gate.js";
|
|
57
|
+
import { sessionCwdOf } from "./working-directory.js";
|
|
57
58
|
/** Structured error identity attached to policy denials (dsh 0.1.6-alpha.1:
|
|
58
59
|
* `deny.info` lets durable projections / retry / replay distinguish an
|
|
59
60
|
* approval-policy denial from a tool-thrown failure). */
|
|
@@ -111,7 +112,11 @@ export function gateDecision(exec) {
|
|
|
111
112
|
if (typeof p === 'string')
|
|
112
113
|
argPath = p;
|
|
113
114
|
}
|
|
114
|
-
|
|
115
|
+
// Effective session directory (see ./working-directory.ts): a session whose
|
|
116
|
+
// directory was switched via dsh 0.2.1-alpha.2's `working_directory` tool
|
|
117
|
+
// must have its POLICY read from the workspace it is now in — `header.cwd`
|
|
118
|
+
// alone would read the original project's approval policy.
|
|
119
|
+
sessionCwd = sessionCwdOf(exec);
|
|
115
120
|
}
|
|
116
121
|
catch {
|
|
117
122
|
// hostile/proxied exec — fall through with both undefined (defaults to ask)
|
|
@@ -2,6 +2,7 @@ import { appendFileSync, readFileSync, mkdirSync, existsSync, openSync, writeSyn
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
4
4
|
import { resolveProjectRootForExec } from "../config-loader.js";
|
|
5
|
+
import { makeLockRelease } from "../file-lock.js";
|
|
5
6
|
const LOG_DIR = '.iterate';
|
|
6
7
|
const LOG_FILE = 'decision-log.jsonl';
|
|
7
8
|
// ─── Cross-process log mutex ────────────────────────────────────────────────
|
|
@@ -111,6 +112,8 @@ export function acquireLogLockChecked(projectRoot, options = {}) {
|
|
|
111
112
|
continue;
|
|
112
113
|
}
|
|
113
114
|
// Owned the lock: stamp the holder pid, then release once on return.
|
|
115
|
+
// The release is ownership-checked (see `makeLockRelease`): a lock stolen
|
|
116
|
+
// from this process after its owner went stale must not be unlinked by us.
|
|
114
117
|
try {
|
|
115
118
|
writeSync(fd, String(process.pid));
|
|
116
119
|
}
|
|
@@ -119,18 +122,9 @@ export function acquireLogLockChecked(projectRoot, options = {}) {
|
|
|
119
122
|
closeSync(fd);
|
|
120
123
|
}
|
|
121
124
|
catch { /* best-effort */ }
|
|
122
|
-
let released = false;
|
|
123
125
|
return {
|
|
124
126
|
acquired: true,
|
|
125
|
-
release: ()
|
|
126
|
-
if (released)
|
|
127
|
-
return;
|
|
128
|
-
released = true;
|
|
129
|
-
try {
|
|
130
|
-
unlinkSync(lockPath);
|
|
131
|
-
}
|
|
132
|
-
catch { /* already gone */ }
|
|
133
|
-
},
|
|
127
|
+
release: makeLockRelease(lockPath),
|
|
134
128
|
};
|
|
135
129
|
}
|
|
136
130
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/working-directory.ts — follow dsh's mutable session working directory.
|
|
3
|
+
*
|
|
4
|
+
* Upstream history:
|
|
5
|
+
* - through dsh 0.2.1-alpha.1 the session's working directory WAS the
|
|
6
|
+
* immutable `Session.header.cwd`, so reading `exec.agent.session.header.cwd`
|
|
7
|
+
* was authoritative;
|
|
8
|
+
* - dsh 0.2.1-alpha.2 added the `working_directory` tool and the
|
|
9
|
+
* `WorkingDirectoryService` (`@deepseek-ai/dsh-working-directory`), whose
|
|
10
|
+
* contract explicitly states "the immutable Session header retains the
|
|
11
|
+
* ORIGINAL project" while `working-directory/change` events record the
|
|
12
|
+
* current one. After a model/user switches directories mid-session,
|
|
13
|
+
* `header.cwd` is therefore stale BY DESIGN.
|
|
14
|
+
*
|
|
15
|
+
* The iterate plugin resolves the project root (where `.iterate/` lives, where
|
|
16
|
+
* fixes are written, and — critically — which workspace the F8 approval policy
|
|
17
|
+
* is read from) from the session cwd. Sticking to `header.cwd` on a host that
|
|
18
|
+
* supports directory switches would silently operate on the wrong project:
|
|
19
|
+
* reviews would anchor to the original checkout while the model edits files
|
|
20
|
+
* elsewhere, and a `deny` policy in the active workspace would be read from
|
|
21
|
+
* the original one.
|
|
22
|
+
*
|
|
23
|
+
* So the effective directory is resolved through the host's service when it
|
|
24
|
+
* exists, and degrades to `header.cwd` otherwise:
|
|
25
|
+
* - feature-detected (`ctx.get('workingDirectory')`) — hosts older than
|
|
26
|
+
* 0.2.1-alpha.2 simply have no such service and keep the old behavior;
|
|
27
|
+
* - `service.get(session)` is the service's synchronous, I/O-free read of
|
|
28
|
+
* the committed directory, so this stays safe on the approval path;
|
|
29
|
+
* - every failure mode (no service, service throws, hostile getters, a
|
|
30
|
+
* non-string result) falls back to `header.cwd`, never to `undefined`
|
|
31
|
+
* where a header cwd exists.
|
|
32
|
+
*
|
|
33
|
+
* The service object is fetched lazily on every call, not once at plugin
|
|
34
|
+
* apply: dsh may register the service after this plugin loads, and a disposed
|
|
35
|
+
* context must degrade rather than cache a dead handle.
|
|
36
|
+
*/
|
|
37
|
+
/** The plugin context, installed once by `apply()`; null on hosts without it. */
|
|
38
|
+
let installedContext = null;
|
|
39
|
+
/** True when `value` looks like a cordis context carrying `ctx.get`. */
|
|
40
|
+
function isContextLike(value) {
|
|
41
|
+
return (typeof value === 'object' &&
|
|
42
|
+
value !== null &&
|
|
43
|
+
typeof value.get === 'function');
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Install the plugin context used to look up the `workingDirectory` service.
|
|
47
|
+
* Passing anything without a callable `get` clears the installation (tests and
|
|
48
|
+
* hosts that load the plugin into a degraded context).
|
|
49
|
+
*/
|
|
50
|
+
export function installWorkingDirectoryContext(ctx) {
|
|
51
|
+
installedContext = isContextLike(ctx) ? ctx : null;
|
|
52
|
+
}
|
|
53
|
+
/** Forget the installed context (test isolation / unload). */
|
|
54
|
+
export function resetWorkingDirectoryContext() {
|
|
55
|
+
installedContext = null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Read the host's committed working directory for `session`, if the host
|
|
59
|
+
* provides the service and it answers with a usable absolute path.
|
|
60
|
+
*
|
|
61
|
+
* Returns `undefined` for every degradation path so callers keep their
|
|
62
|
+
* previous `header.cwd` behavior.
|
|
63
|
+
*/
|
|
64
|
+
function serviceWorkingDirectory(session) {
|
|
65
|
+
if (session === undefined || session === null)
|
|
66
|
+
return undefined;
|
|
67
|
+
const ctx = installedContext;
|
|
68
|
+
if (!ctx)
|
|
69
|
+
return undefined;
|
|
70
|
+
try {
|
|
71
|
+
const service = ctx.get?.('workingDirectory');
|
|
72
|
+
if (!service || typeof service.get !== 'function')
|
|
73
|
+
return undefined;
|
|
74
|
+
const dir = service.get(session);
|
|
75
|
+
// A relative or empty directory would move the project root somewhere
|
|
76
|
+
// unspecified — refuse it and keep the header value instead.
|
|
77
|
+
if (typeof dir !== 'string' || dir.length === 0)
|
|
78
|
+
return undefined;
|
|
79
|
+
return dir;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Missing/disposed service, unpublished session, or a throwing getter —
|
|
83
|
+
// degrade to the historical source rather than failing the tool call.
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The effective working directory of a session: the host's committed
|
|
89
|
+
* directory when available, else the immutable session header cwd.
|
|
90
|
+
*
|
|
91
|
+
* Returns `undefined` only when neither source yields a usable path.
|
|
92
|
+
*/
|
|
93
|
+
export function effectiveSessionCwd(session, headerCwd) {
|
|
94
|
+
const fromService = serviceWorkingDirectory(session);
|
|
95
|
+
if (fromService !== undefined)
|
|
96
|
+
return fromService;
|
|
97
|
+
return typeof headerCwd === 'string' && headerCwd.length > 0 ? headerCwd : undefined;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Extract the effective cwd from a tool execution (`exec.agent.session`),
|
|
101
|
+
* reading every property defensively: a hostile/proxied exec must degrade to
|
|
102
|
+
* the fallback path, never throw out of an approval or capture hook.
|
|
103
|
+
*/
|
|
104
|
+
export function sessionCwdOf(exec) {
|
|
105
|
+
try {
|
|
106
|
+
const agent = exec?.agent;
|
|
107
|
+
const session = agent?.session;
|
|
108
|
+
const headerCwd = typeof agent?.session?.header?.cwd === 'string' ? agent.session.header.cwd : undefined;
|
|
109
|
+
return effectiveSessionCwd(session, headerCwd);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|