iterate-plugin 3.6.0 → 3.8.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 +70 -7
- package/README.zh-CN.md +50 -6
- package/dist/config-loader.js +7 -1
- package/dist/file-lock.js +110 -21
- package/dist/git-scope.js +22 -2
- package/dist/index.js +7 -0
- package/dist/live.js +2 -1
- package/dist/session-hooks.js +6 -1
- package/dist/skill-prompt.js +2 -2
- package/dist/tools/checkpoint.js +24 -17
- package/dist/tools/config.js +42 -33
- package/dist/tools/decision-log.js +13 -99
- package/dist/tools/transcript.js +74 -44
- package/dist/tools/triage.js +52 -45
- package/dist/transcript.js +2 -2
- package/dist/working-directory.js +114 -0
- package/lib/client.js +1128 -468
- package/lib/parse.js +280 -62
- package/package.json +34 -11
- package/src/client/i18n.ts +517 -0
- package/src/client/index.ts +553 -344
- package/src/client/session-cache.ts +54 -0
- package/src/config-loader.ts +7 -1
- package/src/file-lock.ts +115 -18
- package/src/git-scope.ts +21 -2
- package/src/index.ts +7 -0
- package/src/live.ts +2 -1
- package/src/session-hooks.ts +6 -1
- package/src/skill-prompt.ts +2 -2
- package/src/tools/checkpoint.ts +23 -16
- package/src/tools/config.ts +40 -31
- package/src/tools/decision-log.ts +12 -82
- package/src/tools/transcript.ts +72 -43
- package/src/tools/triage.ts +47 -40
- package/src/transcript.ts +2 -2
- package/src/types.ts +1 -1
- 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,10 +342,15 @@ 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`, `0.2.1-alpha.3`,
|
|
346
|
+
`0.2.1-alpha.4` (21 releases, `0.1.1-rc.1` …
|
|
347
|
+
`0.2.1-alpha.4`).
|
|
342
348
|
The plugin is built against `@deepseek-ai/dsh-tools` /
|
|
343
|
-
`@deepseek-ai/dsh-util-values` `0.2.1-alpha.
|
|
349
|
+
`@deepseek-ai/dsh-util-values` `0.2.1-alpha.4` and uses only public contracts
|
|
344
350
|
(tool registration, client slots + theme, bundle patch).
|
|
351
|
+
It also declares `engines.dsh` (`>=0.1.1-rc.1`) so the dsh ≥ 0.2.1-alpha.4
|
|
352
|
+
runtime evaluates compatibility from that single range (earlier runtimes fall
|
|
353
|
+
back to the `@deepseek-ai/dsh-*` peer versions listed in `package.json`).
|
|
345
354
|
- **Disposable-Profile evidence (real, `dsh` CLI `0.1.1-rc.1`)**: on a temp
|
|
346
355
|
`$DSH_HOME`, `dsh plugin --profile <p> add <this-repo>` installed the bundle in
|
|
347
356
|
~449 ms; `dsh --profile <p> --dump-config` composed the `iterate-plugin` bundle
|
|
@@ -379,8 +388,62 @@ following capabilities; elevated capability means a DSH Profile install stays
|
|
|
379
388
|
|
|
380
389
|
### Dependencies
|
|
381
390
|
|
|
382
|
-
Runtime
|
|
383
|
-
|
|
391
|
+
Runtime dependency: `js-yaml` (pinned exact version). The three DSH host
|
|
392
|
+
modules the plugin is built against — `@deepseek-ai/cordis`,
|
|
393
|
+
`@deepseek-ai/dsh-tools`, `@deepseek-ai/dsh-util-values` — are declared as
|
|
394
|
+
**optional `peerDependencies`** so the host's own copies are used instead of a
|
|
395
|
+
second one bundled with the plugin (no duplicate module instances; `dsh`
|
|
396
|
+
supplies them). They stay pinned in `devDependencies` for local builds and
|
|
397
|
+
tests, in lockstep with the newest `dsh.compatibility.dshReleases` entry.
|
|
398
|
+
|
|
399
|
+
#### Upstream adoption (v3.8.0 → dsh `0.2.1-alpha.4`)
|
|
400
|
+
|
|
401
|
+
- **Adopted — the `engines.dsh` compatibility contract.** As of `0.2.1-alpha.4`
|
|
402
|
+
the dsh runtime decides a third-party plugin's compatibility **from
|
|
403
|
+
`engines.dsh` when it is declared** (a semver range, prereleases included),
|
|
404
|
+
ignoring `peerDependencies`; when it is absent the runtime falls back to
|
|
405
|
+
checking the exact `@deepseek-ai/dsh-*` peer versions. The plugin now declares
|
|
406
|
+
`engines.dsh: ">=0.1.1-rc.1"` — the same lower bound as the oldest
|
|
407
|
+
`dshReleases` entry — so a current runtime loads it without flagging the
|
|
408
|
+
exact-pinned peer list, while every host-specific capability stays
|
|
409
|
+
feature-detected (a missing `WorkingDirectoryService`, `ask.displayReason`,
|
|
410
|
+
`ctx.jobs`, etc. still degrade to the safe fallback). Earlier runtimes keep
|
|
411
|
+
using the `@deepseek-ai/dsh-*` peer versions, which are extended through
|
|
412
|
+
`0.2.1-alpha.4`.
|
|
413
|
+
- **`0.2.1-alpha.3` / `0.2.1-alpha.4` are otherwise host-level releases** — Web
|
|
414
|
+
sign-in, secret/token masking, minimal-mode image reading, `/compact`, plugin
|
|
415
|
+
hot-reload, custom plugin index sources, and per-model reasoning-level UI. None
|
|
416
|
+
expose a tool/agent/session API this plugin consumes (`defineTool`, `JsonValue`,
|
|
417
|
+
`exec.*`, `tools/pre-execute`, `presentCall`), so no further adoption is
|
|
418
|
+
required this cycle.
|
|
419
|
+
- All five `@deepseek-ai/*` packages are pinned to the exact `0.2.1-alpha.4`
|
|
420
|
+
(`@deepseek-ai/cordis` stays `4.0.5-alpha.2`).
|
|
421
|
+
|
|
422
|
+
#### Upstream adoption (v3.7.0 → dsh `0.2.1-alpha.2`)
|
|
423
|
+
|
|
424
|
+
- **Adopted — the mutable session working directory.** `src/working-directory.ts`
|
|
425
|
+
resolves the project root through `ctx.get('workingDirectory').get(session)`
|
|
426
|
+
when the host offers it (feature-detected; `header.cwd` fallback on a missing
|
|
427
|
+
service, a throwing service, hostile getters or a non-string result). Needed
|
|
428
|
+
because as of `0.2.1-alpha.2` the immutable header keeps the *original*
|
|
429
|
+
project by design, so reading it after a directory switch would anchor the
|
|
430
|
+
review to the wrong checkout.
|
|
431
|
+
- **Evaluated, not adopted:**
|
|
432
|
+
- *Session state-record APIs* (experimental plugins writing/reading their own
|
|
433
|
+
session records) — reaching into shell-owned record internals is fragile
|
|
434
|
+
across alpha releases, and iterate's cross-session state already lives in
|
|
435
|
+
`.iterate/`.
|
|
436
|
+
- *Shared `AGENTS.md` agents directory (`DSH_AGENTS_HOME`)* — a host-level
|
|
437
|
+
instruction concern; the plugin injects its own workflow prompt.
|
|
438
|
+
- *Experimental Git Worktrees plugin* — an upstream agent-workflow feature;
|
|
439
|
+
the plugin's own `git.use_worktree` config predates it and is unrelated
|
|
440
|
+
(per-round git hygiene for the iterate loop).
|
|
441
|
+
- *Removal of the `both` tool-presentation mode* — the plugin never set a
|
|
442
|
+
presentation mode (it renders `presentCall` cards), so nothing to migrate.
|
|
443
|
+
- *Installing the dsh CLI globally from the plugin* — disk cost for a plugin
|
|
444
|
+
that never shells out at runtime.
|
|
445
|
+
- All five `@deepseek-ai/*` packages are pinned to the exact `0.2.1-alpha.2`
|
|
446
|
+
(`@deepseek-ai/cordis` stays `4.0.5-alpha.1`). No
|
|
384
447
|
install-time lifecycle scripts (`preinstall`/`install`/`postinstall`/`prepare`);
|
|
385
448
|
only `prepublishOnly` (build) runs on publish.
|
|
386
449
|
|
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`、`0.2.1-alpha.3`、`0.2.1-alpha.4`(共 21 个版本,`0.1.1-rc.1` … `0.2.1-alpha.4`)。插件基于 `@deepseek-ai/dsh-tools` / `@deepseek-ai/dsh-util-values` `0.2.1-alpha.4` 构建,只使用公开契约(工具注册、客户端 slots + theme、bundle patch)。插件同时声明 `engines.dsh`(`>=0.1.1-rc.1`),使 dsh ≥ 0.2.1-alpha.4 运行时只依据该范围判断兼容性(更早的运行时仍回退校验 `package.json` 中列出的 `@deepseek-ai/dsh-*` peer 版本)。
|
|
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>
|
|
@@ -360,8 +364,48 @@ validation:
|
|
|
360
364
|
|
|
361
365
|
### 依赖
|
|
362
366
|
|
|
363
|
-
|
|
364
|
-
`@deepseek-ai/dsh-util-values
|
|
367
|
+
运行时依赖:`js-yaml`(精确锁版)。本插件构建所依赖的三个 DSH 宿主模块——
|
|
368
|
+
`@deepseek-ai/cordis`、`@deepseek-ai/dsh-tools`、`@deepseek-ai/dsh-util-values`
|
|
369
|
+
——已声明为**可选 `peerDependencies`**,直接复用宿主自带的那一份,避免插件内
|
|
370
|
+
再打包一份造成双模块实例(由 `dsh` 提供);同时在 `devDependencies` 中精确锁版,
|
|
371
|
+
供本地构建与测试,并与 `dsh.compatibility.dshReleases` 的最新条目保持一致。
|
|
372
|
+
|
|
373
|
+
#### 上游采纳记录(v3.8.0 → dsh `0.2.1-alpha.4`)
|
|
374
|
+
|
|
375
|
+
- **已采纳——`engines.dsh` 兼容契约。** 自 `0.2.1-alpha.4` 起,dsh 运行时**在声明了
|
|
376
|
+
`engines.dsh` 时只依据该字段**(一个含预发布版本的 semver 范围)判断第三方插件兼容性,
|
|
377
|
+
忽略 `peerDependencies`;未声明时才回退逐个校验精确的 `@deepseek-ai/dsh-*` peer 版本。
|
|
378
|
+
插件现已声明 `engines.dsh: ">=0.1.1-rc.1"`(与最早的 `dshReleases` 条目同下界),
|
|
379
|
+
使当前运行时加载时不再因精确锁版的 peer 列表被判不兼容;而所有宿主相关能力依旧
|
|
380
|
+
特性探测(缺少 `WorkingDirectoryService`、`ask.displayReason`、`ctx.jobs` 等一律
|
|
381
|
+
降级到安全回退)。更早的运行时仍走 `@deepseek-ai/dsh-*` peer 版本,已扩展至
|
|
382
|
+
`0.2.1-alpha.4`。
|
|
383
|
+
- **`0.2.1-alpha.3` / `0.2.1-alpha.4` 其余均为宿主级变更**——Web 登录、密钥/令牌脱敏、
|
|
384
|
+
极简模式读图、`/compact`、插件热重载、自定义插件索引源、按模型配置推理等级 UI。
|
|
385
|
+
都未暴露本插件消费的 tool/agent/session API(`defineTool`、`JsonValue`、`exec.*`、
|
|
386
|
+
`tools/pre-execute`、`presentCall`),本轮无需进一步采纳。
|
|
387
|
+
- 五个 `@deepseek-ai/*` 包全部精确锁到 `0.2.1-alpha.4`
|
|
388
|
+
(`@deepseek-ai/cordis` 保持 `4.0.5-alpha.2`)。
|
|
389
|
+
|
|
390
|
+
#### 上游采纳记录(v3.7.0 → dsh `0.2.1-alpha.2`)
|
|
391
|
+
|
|
392
|
+
- **已采纳——可变的会话工作目录。** `src/working-directory.ts` 通过
|
|
393
|
+
`ctx.get('workingDirectory').get(session)` 解析项目根(特性探测;服务缺失、
|
|
394
|
+
抛错、敌意 getter 或非字符串结果一律回退 `header.cwd`)。必须采纳的原因:
|
|
395
|
+
自 `0.2.1-alpha.2` 起,不可变的 header 按设计保留*原始*项目,切换目录后再读它
|
|
396
|
+
会把评审锚定到错误的检出。
|
|
397
|
+
- **已评估、未采纳:**
|
|
398
|
+
- *Session 状态记录接口*(实验插件读写自己的会话记录)——依赖 shell 内部记录结构,
|
|
399
|
+
跨 alpha 版本极脆弱;iterate 的跨会话状态本就存放在 `.iterate/`。
|
|
400
|
+
- *共享 agents 目录的 `AGENTS.md`(`DSH_AGENTS_HOME`)*——属于宿主的全局指令机制,
|
|
401
|
+
插件注入的是自己的工作流提示词。
|
|
402
|
+
- *实验性 Git Worktrees 插件*——上游的 agent 工作流特性;插件自己的
|
|
403
|
+
`git.use_worktree` 配置早于它,且是另一回事(iterate 循环的逐轮 git 卫生)。
|
|
404
|
+
- *移除 `both` 混合展示模式*——插件从未设置展示模式(用的是 `presentCall` 卡片),
|
|
405
|
+
无需迁移。
|
|
406
|
+
- *从插件全局安装 dsh CLI*——运行时从不 shell 调用,纯占磁盘。
|
|
407
|
+
- 五个 `@deepseek-ai/*` 包全部精确锁到 `0.2.1-alpha.2`
|
|
408
|
+
(`@deepseek-ai/cordis` 保持 `4.0.5-alpha.1`)。无安装期生命周期脚本
|
|
365
409
|
(`preinstall`/`install`/`postinstall`/`prepare`);仅 `prepublishOnly`(构建)在发布时运行。
|
|
366
410
|
|
|
367
411
|
### 失败边界
|
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
|
@@ -26,10 +26,17 @@
|
|
|
26
26
|
* `withProjectLock` is therefore NOT reentrant and must not be nested for the
|
|
27
27
|
* same (projectRoot, name).
|
|
28
28
|
*/
|
|
29
|
-
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeSync, } from 'node:fs';
|
|
29
|
+
import { closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeSync, } from 'node:fs';
|
|
30
30
|
import { join } from 'node:path';
|
|
31
31
|
const LOCK_DIR = '.iterate';
|
|
32
|
-
|
|
32
|
+
// Bounded wait before degrading to "proceed unlocked". `acquireProjectLock`
|
|
33
|
+
// spins on `Atomics.wait`, which BLOCKS the single plugin thread for the whole
|
|
34
|
+
// budget — so a long default wedges unrelated tool calls and the UI heartbeat
|
|
35
|
+
// whenever a sibling process holds the lock. Healthy holders release in
|
|
36
|
+
// microseconds, so 1000ms is very generous; anything slower is treated as
|
|
37
|
+
// contention and degrades to the historical unlocked behavior rather than
|
|
38
|
+
// stalling the event loop for five seconds.
|
|
39
|
+
const DEFAULT_WAIT_MS = 1000;
|
|
33
40
|
const DEFAULT_STALE_MS = 10000;
|
|
34
41
|
const POLL_MS = 25;
|
|
35
42
|
/** True when a pid refers to a live process (or we lack permission to tell). */
|
|
@@ -60,14 +67,17 @@ function lockFileName(name) {
|
|
|
60
67
|
return `.${name}.lock`;
|
|
61
68
|
}
|
|
62
69
|
/**
|
|
63
|
-
* Acquire an advisory lock named `name` inside `<projectRoot>/.iterate
|
|
70
|
+
* Acquire an advisory lock named `name` inside `<projectRoot>/.iterate/`,
|
|
71
|
+
* reporting whether it was actually taken.
|
|
64
72
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* created or the wait times out — callers then proceed
|
|
68
|
-
* historical best-effort behavior, rather than failing
|
|
73
|
+
* The returned release function is always callable, idempotent, and never
|
|
74
|
+
* throws. `acquired:false` (with a no-op release) is returned when the lock
|
|
75
|
+
* directory cannot be created or the wait times out — callers then proceed
|
|
76
|
+
* unlocked, which is the historical best-effort behavior, rather than failing
|
|
77
|
+
* the operation. Callers that MUST hold the mutex (e.g. a log rewrite that
|
|
78
|
+
* renames the file) branch on `acquired` and refuse to run unlocked.
|
|
69
79
|
*/
|
|
70
|
-
export function
|
|
80
|
+
export function acquireProjectLockChecked(projectRoot, name, options = {}) {
|
|
71
81
|
const waitMs = Number.isFinite(options.waitMs) && (options.waitMs ?? 0) >= 0
|
|
72
82
|
? options.waitMs
|
|
73
83
|
: DEFAULT_WAIT_MS;
|
|
@@ -80,7 +90,7 @@ export function acquireProjectLock(projectRoot, name, options = {}) {
|
|
|
80
90
|
mkdirSync(dir, { recursive: true });
|
|
81
91
|
}
|
|
82
92
|
catch {
|
|
83
|
-
return () => { };
|
|
93
|
+
return { acquired: false, release: () => { } };
|
|
84
94
|
}
|
|
85
95
|
}
|
|
86
96
|
const lockPath = join(dir, lockFileName(name));
|
|
@@ -92,7 +102,7 @@ export function acquireProjectLock(projectRoot, name, options = {}) {
|
|
|
92
102
|
}
|
|
93
103
|
catch (err) {
|
|
94
104
|
if (err.code !== 'EEXIST')
|
|
95
|
-
return () => { };
|
|
105
|
+
return { acquired: false, release: () => { } };
|
|
96
106
|
// Lock exists — steal it when the owner is gone or the lock is ancient.
|
|
97
107
|
let stale = false;
|
|
98
108
|
try {
|
|
@@ -118,11 +128,21 @@ export function acquireProjectLock(projectRoot, name, options = {}) {
|
|
|
118
128
|
continue;
|
|
119
129
|
}
|
|
120
130
|
if (Date.now() - start > waitMs)
|
|
121
|
-
return () => { };
|
|
131
|
+
return { acquired: false, release: () => { } };
|
|
122
132
|
sleepSync(POLL_MS);
|
|
123
133
|
continue;
|
|
124
134
|
}
|
|
125
|
-
// Owned the lock:
|
|
135
|
+
// Owned the lock: capture the created inode (for the ownership-checked
|
|
136
|
+
// release), stamp the holder pid, then release once on return.
|
|
137
|
+
let ino = 0;
|
|
138
|
+
try {
|
|
139
|
+
ino = fstatSync(fd).ino;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// inode unavailable on this filesystem — the release falls back to the
|
|
143
|
+
// pid-stamp check only.
|
|
144
|
+
ino = 0;
|
|
145
|
+
}
|
|
126
146
|
try {
|
|
127
147
|
writeSync(fd, String(process.pid));
|
|
128
148
|
}
|
|
@@ -135,19 +155,88 @@ export function acquireProjectLock(projectRoot, name, options = {}) {
|
|
|
135
155
|
catch {
|
|
136
156
|
// best-effort
|
|
137
157
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
158
|
+
return { acquired: true, release: makeLockRelease(lockPath, ino) };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Acquire an advisory lock named `name` inside `<projectRoot>/.iterate/`.
|
|
163
|
+
*
|
|
164
|
+
* Returns a release function: always callable, idempotent, never throws.
|
|
165
|
+
* A no-op (but callable) release is returned when the lock directory cannot be
|
|
166
|
+
* created or the wait times out — callers then proceed unlocked, which is the
|
|
167
|
+
* historical best-effort behavior, rather than failing the operation.
|
|
168
|
+
*/
|
|
169
|
+
export function acquireProjectLock(projectRoot, name, options = {}) {
|
|
170
|
+
return acquireProjectLockChecked(projectRoot, name, options).release;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Build the release function for a lock file this process just acquired.
|
|
174
|
+
*
|
|
175
|
+
* Advisory locks here are STOLEN when the recorded owner dies or the mtime
|
|
176
|
+
* goes ancient: the steal unlinks our file and creates a new one stamped with
|
|
177
|
+
* the STEALER's pid. An unguarded release would then unlink the new holder's
|
|
178
|
+
* lock — turning our release into a double-release that admits a third process
|
|
179
|
+
* into a "held" critical section. So the release re-reads the stamp and only
|
|
180
|
+
* unlinks a file that is still ours (same pid) or unstamped (the shape left
|
|
181
|
+
* when our own advisory pid write failed, or when the content is not a pid).
|
|
182
|
+
*
|
|
183
|
+
* Always callable, idempotent, and never throws.
|
|
184
|
+
*/
|
|
185
|
+
/**
|
|
186
|
+
* Build the release function for a lock file this process just acquired.
|
|
187
|
+
*
|
|
188
|
+
* Advisory locks here are STOLEN when the recorded owner dies or the mtime
|
|
189
|
+
* goes ancient: the steal unlinks our file and creates a new one stamped with
|
|
190
|
+
* the STEALER's pid. An unguarded release would then unlink the new holder's
|
|
191
|
+
* lock — turning our release into a double-release that admits a third process
|
|
192
|
+
* into a "held" critical section. So the release re-reads the stamp and only
|
|
193
|
+
* unlinks a file that is still ours: same pid (or unstamped) AND, when we know
|
|
194
|
+
* the inode we created, the SAME inode still at the path. The inode check closes
|
|
195
|
+
* the read-then-unlink window where the stamp check alone could race a steal
|
|
196
|
+
* (our pid is alive, but the file was already replaced between the two calls).
|
|
197
|
+
*
|
|
198
|
+
* Always callable, idempotent, and never throws.
|
|
199
|
+
*
|
|
200
|
+
* @param lockPath - the lock file path.
|
|
201
|
+
* @param ino - the inode captured at acquisition (0 = unknown; skip the check).
|
|
202
|
+
*/
|
|
203
|
+
export function makeLockRelease(lockPath, ino) {
|
|
204
|
+
let released = false;
|
|
205
|
+
return () => {
|
|
206
|
+
if (released)
|
|
207
|
+
return;
|
|
208
|
+
released = true;
|
|
209
|
+
let raw = '';
|
|
210
|
+
try {
|
|
211
|
+
raw = readFileSync(lockPath, 'utf-8').trim();
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// Already gone (or unreadable) — nothing of ours left to remove.
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
// A stamp naming another pid means the lock was stolen and reassigned:
|
|
218
|
+
// it is no longer ours to delete.
|
|
219
|
+
if (raw !== '' && Number(raw) !== process.pid)
|
|
220
|
+
return;
|
|
221
|
+
// The path must still refer to the file WE created. A stealer unlinked our
|
|
222
|
+
// file and created a fresh one; that replacement carries a different inode,
|
|
223
|
+
// so unlinking by path would delete the NEW holder's lock.
|
|
224
|
+
if (typeof ino === 'number' && ino !== 0) {
|
|
143
225
|
try {
|
|
144
|
-
|
|
226
|
+
if (statSync(lockPath).ino !== ino)
|
|
227
|
+
return;
|
|
145
228
|
}
|
|
146
229
|
catch {
|
|
147
|
-
//
|
|
230
|
+
return; // vanished mid-release — nothing of ours left
|
|
148
231
|
}
|
|
149
|
-
}
|
|
150
|
-
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
unlinkSync(lockPath);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// already gone
|
|
238
|
+
}
|
|
239
|
+
};
|
|
151
240
|
}
|
|
152
241
|
/**
|
|
153
242
|
* Run `fn` while holding the advisory lock named `name`.
|
package/dist/git-scope.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* git repo.
|
|
22
22
|
*/
|
|
23
23
|
import { execFile } from 'node:child_process';
|
|
24
|
-
import {
|
|
24
|
+
import { lstatSync, realpathSync } from 'node:fs';
|
|
25
25
|
import { join, sep } from 'node:path';
|
|
26
26
|
/**
|
|
27
27
|
* Parse `git diff --name-only -z` stdout into a list of relative paths.
|
|
@@ -112,6 +112,15 @@ export function parseChangedFiles(stdout) {
|
|
|
112
112
|
* Keep only entries that resolve to an existing regular file under `root`.
|
|
113
113
|
* Traversal-safe: rejects absolute paths and any relative path that would
|
|
114
114
|
* escape `root` via `..` (resolved against the root before stat).
|
|
115
|
+
*
|
|
116
|
+
* Symlink handling: `statSync` (and `existsSync`) FOLLOW symlinks, so a git
|
|
117
|
+
* entry that is a link to a file OUTSIDE the project would pass a path-only
|
|
118
|
+
* containment check and enter the changed-only scope. Such a file is then
|
|
119
|
+
* rejected by the evidence gate (realpath out of bounds → `file_not_found`),
|
|
120
|
+
* so every finding on it would fail closed as `EVIDENCE_VIOLATION` forever —
|
|
121
|
+
* a permanent false positive for repos with dotfile or shared-config links.
|
|
122
|
+
* `lstatSync` distinguishes the link itself; a link whose REAL target escapes
|
|
123
|
+
* `root` is dropped here instead.
|
|
115
124
|
*/
|
|
116
125
|
export function filterExistingFiles(root, files) {
|
|
117
126
|
const out = [];
|
|
@@ -123,7 +132,18 @@ export function filterExistingFiles(root, files) {
|
|
|
123
132
|
if (!candidate.startsWith(rootPrefix) && candidate !== root)
|
|
124
133
|
continue;
|
|
125
134
|
try {
|
|
126
|
-
|
|
135
|
+
const st = lstatSync(candidate);
|
|
136
|
+
if (st.isFile()) {
|
|
137
|
+
out.push(rel);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (!st.isSymbolicLink())
|
|
141
|
+
continue;
|
|
142
|
+
// A link whose real target stays inside the project is a legitimate,
|
|
143
|
+
// reviewable file; one that escapes is excluded from scope (fail-closed
|
|
144
|
+
// at the source instead of as a permanent evidence violation).
|
|
145
|
+
const real = realpathSync(candidate);
|
|
146
|
+
if (real === root || real.startsWith(rootPrefix))
|
|
127
147
|
out.push(rel);
|
|
128
148
|
}
|
|
129
149
|
catch {
|
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)
|
package/dist/skill-prompt.js
CHANGED
|
@@ -134,7 +134,7 @@ for (let r = 1; r <= maxRounds; r++) {
|
|
|
134
134
|
(attachments.length > 0 ? '\\n User-attached images are part of the evidence; use their descriptions when judging (you see the metadata/descriptions below, not the pixels): ' + JSON.stringify(attachments) + '.' : '') +
|
|
135
135
|
'\\n Already-known findings (do NOT re-report): ' +
|
|
136
136
|
JSON.stringify(known) + nudge + '\\nReturn the findings JSON object.'
|
|
137
|
-
return agent(base + extra, Object.assign({ label: 'review:' + dim + ':r' + r, schema: meta
|
|
137
|
+
return agent(base + extra, Object.assign({ label: 'review:' + dim + ':r' + r, schema: meta?.findingsSchema }, backend))
|
|
138
138
|
}))
|
|
139
139
|
// A per-dimension reviewer subagent resolves \`null\` on child failure; the
|
|
140
140
|
// aggregate below would report an all-failed round as an empty round, and
|
|
@@ -463,7 +463,7 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
463
463
|
(steering ? '\\n STEERING — read this first: ' + steering : '') +
|
|
464
464
|
(attachments.length > 0 ? '\\n User-attached images are part of the evidence; use their descriptions when judging (you see the metadata/descriptions below, not the pixels): ' + JSON.stringify(attachments) + '.' : '') +
|
|
465
465
|
'\\n Do NOT re-report already-known architectural findings: ' + JSON.stringify(architectural) + nudge + '\\nReturn the findings JSON object.'
|
|
466
|
-
return agent(base + extra, Object.assign({ label: 'review:' + dim + ':r' + r, schema: meta
|
|
466
|
+
return agent(base + extra, Object.assign({ label: 'review:' + dim + ':r' + r, schema: meta?.findingsSchema }, backend))
|
|
467
467
|
}))
|
|
468
468
|
// ALL reviewers returning null (child failures) means this round produced
|
|
469
469
|
// no usable output — an empty aggregate must not read like a clean
|
package/dist/tools/checkpoint.js
CHANGED
|
@@ -12,6 +12,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
|
12
12
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
13
13
|
import { resolveProjectRootForExec } from "../config-loader.js";
|
|
14
14
|
import { writeJsonAtomic } from "../atomic-fs.js";
|
|
15
|
+
import { withProjectLock } from "../file-lock.js";
|
|
15
16
|
import { checkpointPath, iterateDir, transcriptPath } from "../paths.js";
|
|
16
17
|
import { readRegistry } from "./fix.js";
|
|
17
18
|
import { readDecisionEntries } from "./decision-log.js";
|
|
@@ -289,23 +290,29 @@ export function registerCheckpointTool(ctx) {
|
|
|
289
290
|
return { operation: 'load', ok: true, checkpoint: checkpoint };
|
|
290
291
|
}
|
|
291
292
|
if (args.operation === 'resume') {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
293
|
+
// Read-modify-write across processes: a concurrent resume in a
|
|
294
|
+
// sibling process would read the same resumeCount and write it back,
|
|
295
|
+
// losing one increment. Serialize on a checkpoint lock (the same
|
|
296
|
+
// advisory pattern the config/triage/transcript writers use).
|
|
297
|
+
return withProjectLock(projectRoot, 'checkpoint', () => {
|
|
298
|
+
const current = readCheckpoint(projectRoot);
|
|
299
|
+
if (!current) {
|
|
300
|
+
return { operation: 'resume', ok: false, error: 'no checkpoint to resume — run `save` first' };
|
|
301
|
+
}
|
|
302
|
+
const resumed = {
|
|
303
|
+
...current,
|
|
304
|
+
resumeCount: (current.resumeCount ?? 0) + 1,
|
|
305
|
+
updatedAt: new Date().toISOString(),
|
|
306
|
+
};
|
|
307
|
+
try {
|
|
308
|
+
mkdirSync(iterateDir(projectRoot), { recursive: true });
|
|
309
|
+
writeJsonAtomic(checkpointPath(projectRoot), resumed);
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
return { operation: 'resume', ok: false, error: `failed to persist resumed checkpoint: ${String(err)}` };
|
|
313
|
+
}
|
|
314
|
+
return { operation: 'resume', ok: true, checkpoint: resumed };
|
|
315
|
+
});
|
|
309
316
|
}
|
|
310
317
|
if (args.operation === 'clear') {
|
|
311
318
|
const existed = existsSync(checkpointPath(projectRoot));
|