iterate-plugin 3.5.2 → 3.5.4
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 +3 -3
- package/README.zh-CN.md +3 -3
- package/dist/session-hooks.js +48 -4
- package/dist/skill-prompt.js +63 -29
- package/dist/tools/checkpoint.js +39 -5
- package/dist/tools/config.js +11 -2
- package/dist/tools/context.js +5 -1
- package/dist/tools/defense-events.js +48 -9
- package/dist/tools/defense-store.js +63 -5
- package/dist/tools/experience-bank.js +14 -11
- package/dist/tools/experience-store.js +79 -14
- package/dist/tools/fix.js +30 -10
- package/dist/tools/history.js +5 -3
- package/dist/tools/prune.js +197 -29
- package/dist/tools/quality-gate.js +3 -0
- package/dist/tools/quality-store.js +8 -2
- package/dist/tools/review.js +17 -3
- package/dist/tools/transcript.js +72 -15
- package/dist/tools/triage.js +35 -2
- package/dist/transcript.js +30 -7
- package/lib/client.js +89 -21
- package/lib/parse.js +27 -2
- package/package.json +9 -7
- package/src/client/index.ts +53 -8
- package/src/session-hooks.ts +50 -4
- package/src/skill-prompt.ts +63 -29
- package/src/tools/checkpoint.ts +45 -6
- package/src/tools/config.ts +11 -2
- package/src/tools/context.ts +5 -1
- package/src/tools/defense-events.ts +50 -9
- package/src/tools/defense-store.ts +64 -8
- package/src/tools/experience-bank.ts +14 -11
- package/src/tools/experience-store.ts +88 -23
- package/src/tools/fix.ts +32 -12
- package/src/tools/history.ts +5 -3
- package/src/tools/prune.ts +204 -28
- package/src/tools/quality-gate.ts +4 -0
- package/src/tools/quality-store.ts +7 -2
- package/src/tools/review.ts +16 -3
- package/src/tools/transcript.ts +71 -14
- package/src/tools/triage.ts +36 -2
- package/src/transcript.ts +32 -7
- package/src/types.ts +2 -0
package/README.md
CHANGED
|
@@ -106,7 +106,7 @@ Both modes share:
|
|
|
106
106
|
### UI layer (build-free client slots, v3.2: 10 tabs)
|
|
107
107
|
|
|
108
108
|
- **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 **v3.2 task_mode indicator (code / iterate)**, now backed by the persisted transcript (`iterate_status` / `iterate_transcript` emit `taskMode`).
|
|
109
|
-
- **ObservatoryPanel** (`conversation.input.dock`) — a **ten-tab** runtime observatory below the input: live activity stream (type filter), review threads (expand / collapse all), convergence trend, finding locations (severity / dimension / search filter), fixes + rollback, checkpoint resume, decision timeline (type / round filter + search). **v3.2 tabs: Quality Gate (F8), Experience Bank (F9), Defense Events (F10)**; one-click JSON export of all observatory data (download, copy fallback). **v3.3
|
|
109
|
+
- **ObservatoryPanel** (`conversation.input.dock`) — a **ten-tab** runtime observatory below the input: live activity stream (type filter), review threads (expand / collapse all), convergence trend, finding locations (severity / dimension / search filter), fixes + rollback, checkpoint resume, decision timeline (type / round filter + search). **v3.2 tabs: Quality Gate (F8), Experience Bank (F9), Defense Events (F10)**; one-click JSON export of all observatory data (download, copy fallback). **v3.3+: 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), degrading to copy-able query instructions only when the session has none. **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.
|
|
110
110
|
- **TriagePanel** (`conversation.chat.turnTail`) — per-finding y/n/a triage, filtering, batch ops (incl. select-all), keyboard shortcuts, localStorage persistence, copy-YAML / apply-instruction. **v3.2: native command buttons** (approve architectural fix, trigger new round, rollback to checkpoint). **v3.3: §8 指派修复 (assign)** — one click copies an `iterate_fix` instruction carrying every in-scope finding (respecting the batch toggle).
|
|
111
111
|
- **StatsCard** (`conversation.chat.turnTail`) — convergence stats, round history, trend chart and completion summary when no findings remain.
|
|
112
112
|
- **iterate theme skin** (`theme.overrideTokens`) — a warm-amber 13-token `--dsw-*` override, light/dark modes, togglable in settings.
|
|
@@ -255,14 +255,14 @@ validation:
|
|
|
255
255
|
- `iterate_fix` — apply **one atomic fix**: validates the relative path, backs up the original file, enforces atomicity via `atomic.max_lines` (skippable with `force`), writes new content, records a FixRecord and an `atomic_fix` log. The only legal file-modifying entry in normal mode
|
|
256
256
|
- `iterate_diff` — view accumulated fix changes: with `file`, returns the unified diff against the first backup; without it, a per-fixed-file summary
|
|
257
257
|
- `iterate_rollback` — roll back an applied fix: restore the file from backup, remove that FixRecord from the registry, append a `revert` log. Used after a failed round validation
|
|
258
|
-
- `iterate_checkpoint` — iteration breakpoint: `save` persists progress to `.iterate/checkpoint.json`, `load` reads it back, `resume` loads + bumps the resume counter (interruption recovery), `clear` removes it. Resumable interrupted long iterations
|
|
258
|
+
- `iterate_checkpoint` — iteration breakpoint: `save` persists progress to `.iterate/checkpoint.json`, `load` reads it back, `resume` loads + bumps the resume counter (interruption recovery), `clear` removes it (bounded findings payload, `round ≤ maxRounds` enforced). Resumable interrupted long iterations
|
|
259
259
|
- `iterate_status` — summarize current iteration state: mode, current/last round, fixes applied, remaining architectural, decision-log entry count, whether a checkpoint exists; **v3.4: also surfaces the persisted quality-gate snapshot, experience-bank summary, and defense-events summary** (`qualityGate` / `experienceBank` / `defenseEvents`)
|
|
260
260
|
- `iterate_history` — read iteration history (read-only): decision-log entries (filter by `type` / `since` / `limit`, default latest 50, cap 200) + fix-registry summary (per-round fixed/failed counts). For auditing the run, tracing logs, and inventorying fixes
|
|
261
261
|
- `iterate_prune` — clean runtime artifacts: stale decision-log entries (by `retainDays`, default 30), stale checkpoints, orphaned fix backups, empty rounds, stray temp files (crashed atomic writes). Dry-run by default (report-only); real cleanup with `dryRun:false`, each cleanup logged
|
|
262
262
|
- `iterate_transcript` — runtime observatory: persist review transcripts, threads, fixes, and nudge directions to `.iterate/transcript.json` for the client observatory
|
|
263
263
|
- `iterate_experience` — **v3.2** query the experience bank (list / search / get), `add` a new verified fix (re-adding the same pattern+dimension bumps its hit count instead of duplicating it), or **v3.5 remove a stale/incorrect entry by `id`** so bad experiences never resurface. Persists to `.iterate/experience.json`
|
|
264
264
|
- `iterate_quality_gate` — **v3.2** read the quality certificate (`read`), or recompute + persist a fresh one (`compute`) from findings, validation results, `findingsByRound`, and `fixedByDimension`. Real per-dimension convergence rates. **v3.5: `clear` removes the persisted certificate to reset a stale gate before a fresh iteration**
|
|
265
|
-
- `iterate_defense_events` — **v3.2** query defense events (list / counts),
|
|
265
|
+
- `iterate_defense_events` — **v3.2** query defense events (list / counts), `record` a new one, or **v3.5.3 `clear` the persisted stream** so a fresh iteration starts without stale defensive data. Human-readable labels follow the project language (en / zh)
|
|
266
266
|
|
|
267
267
|
---
|
|
268
268
|
|
package/README.zh-CN.md
CHANGED
|
@@ -105,7 +105,7 @@ dsh plugin --profile web add iterate-plugin
|
|
|
105
105
|
### UI 层(客户端免构建槽位,v3.2:10 个标签页)
|
|
106
106
|
|
|
107
107
|
- **收敛看板 `ConvergenceDashboard`**(`conversation.input.dock`)— 输入框上方实时显示轮次进度条、严重度统计、维度徽章、趋势迷你图;normal 模式另显示修复计数徽章;还有运行阶段芯片(当前工作流阶段 + 运行中/已结束)与 **v3.2 task_mode 指示器(code / iterate)**。
|
|
108
|
-
- **运行时观测台 `ObservatoryPanel`**(`conversation.input.dock`)— 输入框下方**十个标签页**运行时观测台:实时活动流(支持按活动类型筛选)、审查线程(支持全部展开/全部收起)、收敛趋势、发现定位(支持按严重度/维度/关键词筛选)、修复与回滚、断点恢复、决策时间线(支持按类型/轮次筛选与关键词搜索)。**v3.2 新增标签:质量门禁(F8)、经验银行(F9)、防御事件(F10)**;支持一键导出全部观测数据为 JSON
|
|
108
|
+
- **运行时观测台 `ObservatoryPanel`**(`conversation.input.dock`)— 输入框下方**十个标签页**运行时观测台:实时活动流(支持按活动类型筛选)、审查线程(支持全部展开/全部收起)、收敛趋势、发现定位(支持按严重度/维度/关键词筛选)、修复与回滚、断点恢复、决策时间线(支持按类型/轮次筛选与关键词搜索)。**v3.2 新增标签:质量门禁(F8)、经验银行(F9)、防御事件(F10)**;支持一键导出全部观测数据为 JSON(优先下载,失败回退复制)。**v3.5.3 新增重置操作**:F5 复制 `iterate_checkpoint clear`、F8 复制 `iterate_quality_gate clear`、F10 复制 `iterate_defense_events clear` 指令。
|
|
109
109
|
- **Findings 分诊面板 `TriagePanel`**(`conversation.chat.turnTail`)— 逐条 y/n/a 判定,支持筛选、批量(含一键全选所有 findings)、键盘快捷键、localStorage 持久化、复制 YAML / 应用指令。**v3.2:原生命令按钮**(批准架构修复、触发新一轮、回滚到断点)。
|
|
110
110
|
- **收敛统计卡片 `StatsCard`**(`conversation.chat.turnTail`)— 无 findings 时显示收敛统计、历史轮次、趋势图、完成摘要。
|
|
111
111
|
- **iterate 主题皮肤**(`theme.overrideTokens`)— 暖琥珀配色的 13 个 `--dsw-*` token 覆盖,明暗双模式,可在设置页开关。
|
|
@@ -254,14 +254,14 @@ validation:
|
|
|
254
254
|
- `iterate_fix` — 应用**一个原子修复**:校验相对路径、备份原文件、按 `atomic.max_lines` 强制原子性(可 `force` 跳过)、写入新内容、记录 FixRecord 与 `atomic_fix` 日志。normal 模式唯一合法的改文件入口
|
|
255
255
|
- `iterate_diff` — 查看修复累积变更:指定 `file` 返回相对首个备份的 unified diff;省略则返回每个已修复文件的汇总
|
|
256
256
|
- `iterate_rollback` — 回滚一个已应用的修复:从备份还原文件、从注册表移除该 FixRecord、追加 `revert` 日志。用于某轮验证失败后
|
|
257
|
-
- `iterate_checkpoint` — 迭代断点:`save` 保存当前进度到 `.iterate/checkpoint.json`,`load` 读回,`resume` 加载并累加恢复计数(中断恢复),`clear`
|
|
257
|
+
- `iterate_checkpoint` — 迭代断点:`save` 保存当前进度到 `.iterate/checkpoint.json`,`load` 读回,`resume` 加载并累加恢复计数(中断恢复),`clear` 清除(findings 载荷有上限,强制 `round ≤ maxRounds`)。长迭代可中断续跑
|
|
258
258
|
- `iterate_status` — 汇总当前迭代状态:模式、当前轮/总轮、已修复数、剩余 architectural、决策日志条数、是否存在 checkpoint;**v3.4:同时返回持久化的质量门禁快照、经验银行摘要与防御事件摘要**(`qualityGate` / `experienceBank` / `defenseEvents`)
|
|
259
259
|
- `iterate_history` — 读取迭代历史(只读):决策日志条目(可按 `type` / `since` / `limit` 过滤,默认取最新 50 条,上限 200 条)+ 修复注册表汇总(各轮 fixed/failed 计数)。用于审查运行过程、审计日志、盘点修复
|
|
260
260
|
- `iterate_prune` — 清理运行时产物:过期决策日志条目(按 `retainDays`,默认 30 天)、陈旧断点、孤儿修复备份、空轮次、崩溃原子写入残留的临时文件。默认 dry-run 只报告不删除;`dryRun:false` 才真正清理,每次清理写入决策日志
|
|
261
261
|
- `iterate_transcript` — 运行时观测台:把审查转录、线程、修复与 nudge 指令持久化到 `.iterate/transcript.json`,供客户端观测台读取
|
|
262
262
|
- `iterate_experience` — **v3.2** 查询经验银行(list / search / get),或 `add` 一条新的已验证修复:重复添加同一 pattern + dimension 累加命中次数而非重复写入。**v3.5 新增 `remove`:按 `id` 删除过时或错误的经验条目**。持久化到 `.iterate/experience.json`
|
|
263
263
|
- `iterate_quality_gate` — **v3.2** 读取质量凭证(`read`),或基于 findings、验证结果、`findingsByRound` 与 `fixedByDimension` 重新计算并持久化一份新凭证(`compute`)。真实的逐维度收敛率。**v3.5 新增 `clear`:删除持久化的质量凭证**,用于新一轮迭代前重置陈旧的门禁
|
|
264
|
-
- `iterate_defense_events` — **v3.2** 查询防御事件(list / counts
|
|
264
|
+
- `iterate_defense_events` — **v3.2** 查询防御事件(list / counts),`record` 记录一条新事件,或 **v3.5.3 `clear` 清除持久化事件流**,让新一轮迭代不带陈旧防御数据起跑。可读标签跟随项目语言(en / zh)
|
|
265
265
|
|
|
266
266
|
---
|
|
267
267
|
|
package/dist/session-hooks.js
CHANGED
|
@@ -7,12 +7,22 @@
|
|
|
7
7
|
*
|
|
8
8
|
* - `allow` policy → the call runs.
|
|
9
9
|
* - `deny` policy → the call is refused (fail-closed), surfaced as an
|
|
10
|
-
* error to the model.
|
|
10
|
+
* error to the model. Since dsh 0.1.6-alpha.1 the
|
|
11
|
+
* denial carries a structured `info` (name
|
|
12
|
+
* `iterate-approval-gate`, code `APPROVAL_DENIED`,
|
|
13
|
+
* plus the human-readable reason) so durable
|
|
14
|
+
* projections can route it distinctly from a normal
|
|
15
|
+
* tool failure.
|
|
11
16
|
* - `ask` policy → return `{ kind: 'ask', reason }`; dsh's own
|
|
12
17
|
* scheduler routes it through the `approval` service
|
|
13
18
|
* (see `@deepseek-ai/dsh-user-approval`), which
|
|
14
19
|
* prompts the human and audits an approve/deny pair
|
|
15
20
|
* on the session.
|
|
21
|
+
* - canceled → if the caller aborted the invocation before
|
|
22
|
+
* dispatch, return `{ kind: 'cancel' }` (the
|
|
23
|
+
* canonical 0.1.6-alpha.1 cancellation result) so a
|
|
24
|
+
* destructive call never runs (or asks) for a dead
|
|
25
|
+
* request.
|
|
16
26
|
*
|
|
17
27
|
* We deliberately do NOT also add `approved` flags inside the tool bodies:
|
|
18
28
|
* the pre-execute waterfall consumes the human decision before the tool runs,
|
|
@@ -27,6 +37,13 @@
|
|
|
27
37
|
*/
|
|
28
38
|
import { loadEffectiveConfig, resolveProjectRoot } from "./config-loader.js";
|
|
29
39
|
import { decideApproval, isDestructiveIterateTool } from "./approval-gate.js";
|
|
40
|
+
/** Structured error identity attached to policy denials (dsh 0.1.6-alpha.1:
|
|
41
|
+
* `deny.info` lets durable projections / retry / replay distinguish an
|
|
42
|
+
* approval-policy denial from a tool-thrown failure). */
|
|
43
|
+
const DENY_INFO = {
|
|
44
|
+
name: 'iterate-approval-gate',
|
|
45
|
+
code: 'APPROVAL_DENIED',
|
|
46
|
+
};
|
|
30
47
|
/**
|
|
31
48
|
* Build the per-call approval decision for a tool execution.
|
|
32
49
|
* Returns a dsh `PreToolDecision` so the caller can short-circuit the caller.
|
|
@@ -46,6 +63,20 @@ export function gateDecision(exec) {
|
|
|
46
63
|
}
|
|
47
64
|
if (!isDestructiveIterateTool(name))
|
|
48
65
|
return { kind: 'allow' };
|
|
66
|
+
// dsh 0.1.6-alpha.1: if the caller already aborted this invocation before
|
|
67
|
+
// dispatch, there is nothing to gate — select the canonical cancellation
|
|
68
|
+
// result (`cancel`) rather than prompting a human for a dead call or running
|
|
69
|
+
// `next()` (which would allow) on an aborted one.
|
|
70
|
+
let signal;
|
|
71
|
+
try {
|
|
72
|
+
signal = exec.signal;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
signal = undefined;
|
|
76
|
+
}
|
|
77
|
+
if (signal && typeof signal.aborted === 'boolean' && signal.aborted) {
|
|
78
|
+
return { kind: 'cancel' };
|
|
79
|
+
}
|
|
49
80
|
// Resolve the project root (use the call's own `path` arg, else the agent's
|
|
50
81
|
// session cwd) to read the effective observatory policy.
|
|
51
82
|
let argPath;
|
|
@@ -74,8 +105,18 @@ export function gateDecision(exec) {
|
|
|
74
105
|
// anything else (including a corrupt/missing `ask`) → 'ask'
|
|
75
106
|
}
|
|
76
107
|
const decision = decideApproval(exec, policy);
|
|
77
|
-
if (decision.kind === 'deny')
|
|
78
|
-
return {
|
|
108
|
+
if (decision.kind === 'deny') {
|
|
109
|
+
return {
|
|
110
|
+
kind: 'deny',
|
|
111
|
+
reason: decision.reason,
|
|
112
|
+
info: {
|
|
113
|
+
...DENY_INFO,
|
|
114
|
+
// ToolErrorInfo.reason (0.1.6-alpha.1): raw user-facing detail kept in
|
|
115
|
+
// durable state; the model-facing text still only sees `reason` above.
|
|
116
|
+
reason: decision.reason,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
79
120
|
if (decision.kind === 'ask')
|
|
80
121
|
return { kind: 'ask', reason: decision.reason };
|
|
81
122
|
return { kind: 'allow' };
|
|
@@ -106,7 +147,10 @@ export function registerSessionHooks(ctx) {
|
|
|
106
147
|
// to allow, which would bypass consent — so return our ask decision.
|
|
107
148
|
return Promise.resolve(decision);
|
|
108
149
|
}
|
|
109
|
-
if (decision.kind === 'deny') {
|
|
150
|
+
if (decision.kind === 'deny' || decision.kind === 'cancel') {
|
|
151
|
+
// deny: refuse the call. cancel: the caller already aborted this
|
|
152
|
+
// invocation — selecting the canonical cancellation result instead of
|
|
153
|
+
// letting `next()` allow it or prompting consent for a dead request.
|
|
110
154
|
return Promise.resolve(decision);
|
|
111
155
|
}
|
|
112
156
|
return next();
|
package/dist/skill-prompt.js
CHANGED
|
@@ -94,10 +94,11 @@ for (let r = 1; r <= maxRounds; r++) {
|
|
|
94
94
|
log('round ' + r + ' of ' + maxRounds + ' — finding NEW issues only')
|
|
95
95
|
let agg = null
|
|
96
96
|
let schemaInvalid = false
|
|
97
|
-
let
|
|
97
|
+
let attempts = 0
|
|
98
98
|
do {
|
|
99
|
+
attempts += 1
|
|
99
100
|
// Schema validation retry: on the 2nd+ pass, nudge reviewers toward strict JSON.
|
|
100
|
-
const nudge =
|
|
101
|
+
const nudge = attempts > 1
|
|
101
102
|
? '\\nSTRICT JSON REQUIRED: your previous output failed schema validation. Return ONLY a JSON object {"findings":[...]} where EVERY finding has dimension, file, line (non-negative integer; 0 = whole-file), severity (critical|high|medium|low), summary, failure_scenario, suggested_fix, is_atomic (boolean).'
|
|
102
103
|
: ''
|
|
103
104
|
const raw = await parallel(dims.map(dim => () => {
|
|
@@ -134,11 +135,15 @@ for (let r = 1; r <= maxRounds; r++) {
|
|
|
134
135
|
schemaInvalid = agg && agg.schemaValidation && agg.schemaValidation.length > 0
|
|
135
136
|
? agg.schemaValidation[agg.schemaValidation.length - 1].valid === false
|
|
136
137
|
: false
|
|
137
|
-
if (schemaInvalid &&
|
|
138
|
-
|
|
139
|
-
log('retry ' + retries + ': round ' + r + ' output failed schema validation — re-running reviewers with strict-JSON emphasis')
|
|
138
|
+
if (schemaInvalid && attempts < 3) {
|
|
139
|
+
log('retry ' + attempts + ': round ' + r + ' output failed schema validation — re-running reviewers with strict-JSON emphasis')
|
|
140
140
|
}
|
|
141
|
-
} while (schemaInvalid &&
|
|
141
|
+
} while (schemaInvalid && attempts < 3)
|
|
142
|
+
if (schemaInvalid) {
|
|
143
|
+
// Bounded exit: never loop forever. Surface the failure so the run's
|
|
144
|
+
// final summary can explain WHY the round was inconclusive.
|
|
145
|
+
log('round ' + r + ' output STILL schema-invalid after 3 attempts — marking round inconclusive (NOT converged)')
|
|
146
|
+
}
|
|
142
147
|
// Feed the DEDUPED + already-filtered set back (not raw findings) so the known
|
|
143
148
|
// list stays bounded and reviewers never see the same issue twice.
|
|
144
149
|
if (agg && agg.report && Array.isArray(agg.report.findings)) known = agg.report.findings
|
|
@@ -269,6 +274,7 @@ let fixedCount = (checkpoint && typeof checkpoint.fixedCount === 'number') ? che
|
|
|
269
274
|
let converged = false
|
|
270
275
|
let abortedByValidation = false
|
|
271
276
|
let failedCommands = []
|
|
277
|
+
let configErrors = [] // validation commands NOT in validation.commands (config gap, no rollback)
|
|
272
278
|
const fixRecords = [] // observatory fix records collected round by round
|
|
273
279
|
|
|
274
280
|
// Read any steering nudge intended for this run's reviewers.
|
|
@@ -288,10 +294,11 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
288
294
|
)
|
|
289
295
|
let agg = null
|
|
290
296
|
let schemaInvalid = false
|
|
291
|
-
let
|
|
297
|
+
let attempts = 0
|
|
292
298
|
do {
|
|
299
|
+
attempts += 1
|
|
293
300
|
// Schema validation retry: on the 2nd+ pass, nudge reviewers toward strict JSON.
|
|
294
|
-
const nudge =
|
|
301
|
+
const nudge = attempts > 1
|
|
295
302
|
? '\\nSTRICT JSON REQUIRED: your previous output failed schema validation. Return ONLY a JSON object {"findings":[...]} where EVERY finding has dimension, file, line (non-negative integer; 0 = whole-file), severity (critical|high|medium|low), summary, failure_scenario, suggested_fix, is_atomic (boolean).'
|
|
296
303
|
: ''
|
|
297
304
|
const raw = await parallel(dims.map(dim => () => {
|
|
@@ -327,11 +334,15 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
327
334
|
schemaInvalid = agg && agg.schemaValidation && agg.schemaValidation.length > 0
|
|
328
335
|
? agg.schemaValidation[agg.schemaValidation.length - 1].valid === false
|
|
329
336
|
: false
|
|
330
|
-
if (schemaInvalid &&
|
|
331
|
-
|
|
332
|
-
log('retry ' + retries + ': round ' + r + ' output failed schema validation — re-running reviewers with strict-JSON emphasis')
|
|
337
|
+
if (schemaInvalid && attempts < 3) {
|
|
338
|
+
log('retry ' + attempts + ': round ' + r + ' output failed schema validation — re-running reviewers with strict-JSON emphasis')
|
|
333
339
|
}
|
|
334
|
-
} while (schemaInvalid &&
|
|
340
|
+
} while (schemaInvalid && attempts < 3)
|
|
341
|
+
if (schemaInvalid) {
|
|
342
|
+
// Bounded exit: never loop forever. Surface the failure so the run's
|
|
343
|
+
// final summary can explain WHY the round was inconclusive.
|
|
344
|
+
log('round ' + r + ' output STILL schema-invalid after 3 attempts — marking round inconclusive (NOT converged)')
|
|
345
|
+
}
|
|
335
346
|
const findings = (agg && agg.report && agg.report.findings) ? agg.report.findings : thisRound.findings
|
|
336
347
|
const atomic = findings.filter(f => f.is_atomic === true)
|
|
337
348
|
const remaining = findings.filter(f => f.is_atomic !== true)
|
|
@@ -386,18 +397,33 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
386
397
|
if (seenKeys.indexOf(key) < 0) { architectural.push(f); seenKeys.push(key) }
|
|
387
398
|
}
|
|
388
399
|
|
|
389
|
-
// Validate every configured command; on ANY failure roll back this
|
|
400
|
+
// Validate every configured command; on ANY *executed* failure roll back this
|
|
401
|
+
// round's fixes. iterate_validate returns \`allowed:false\` + \`rejectReason\`
|
|
402
|
+
// when a command is NOT in validation.commands (a config gap, NOT a code
|
|
403
|
+
// failure) — those abort the run WITHOUT rolling back (the code changes are
|
|
404
|
+
// fine; the trust list just needs fixing).
|
|
390
405
|
const valRes = await agent(
|
|
391
406
|
'Read iterate.config.yaml validation.commands, then call iterate_validate({ command: <cmd> }) for EACH configured command ' +
|
|
392
|
-
'(one tool call per command). Return all results as {command, exitCode} entries.',
|
|
407
|
+
'(one tool call per command). Return all results as {command, exitCode, allowed, rejectReason} entries — include \`allowed\` and \`rejectReason\` exactly as returned by iterate_validate (allowed:false means the command is not in validation.commands).',
|
|
393
408
|
Object.assign({ label: 'validate:r' + r, phase: 'validate', schema: {
|
|
394
409
|
type: 'object', additionalProperties: false,
|
|
395
410
|
properties: {
|
|
396
|
-
results: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { command: { type: 'string' }, exitCode: { type: 'integer' } }, required: ['command', 'exitCode'] } }
|
|
411
|
+
results: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { command: { type: 'string' }, exitCode: { type: 'integer' }, allowed: { type: 'boolean' }, rejectReason: { type: 'string' } }, required: ['command', 'exitCode'] } }
|
|
397
412
|
},
|
|
398
413
|
required: ['results'] } }, backend)
|
|
399
414
|
)
|
|
400
|
-
|
|
415
|
+
const valResults = (valRes && Array.isArray(valRes.results)) ? valRes.results : []
|
|
416
|
+
configErrors = valResults.filter(v => v.allowed === false).map(v => v.rejectReason || (v.command + ' was rejected'))
|
|
417
|
+
failedCommands = valResults.filter(v => !(v.allowed === false) && v.exitCode !== 0).map(v => v.command)
|
|
418
|
+
if (configErrors.length > 0) {
|
|
419
|
+
log('round ' + r + ' validation CONFIG error (not in validation.commands): ' + configErrors.join(' | ') + ' — aborting WITHOUT rollback (code changes are kept; the command trust list needs updating)')
|
|
420
|
+
abortedByValidation = true
|
|
421
|
+
await agent(
|
|
422
|
+
'Call iterate_decision_log({operation:"append", type:"round_failed", round:' + r + ', data:{configErrors:' + JSON.stringify(configErrors) + ', rolledBack:0, reason:"command not in validation.commands"}})',
|
|
423
|
+
Object.assign({ label: 'log:configError:r' + r }, backend)
|
|
424
|
+
)
|
|
425
|
+
break
|
|
426
|
+
}
|
|
401
427
|
if (failedCommands.length > 0) {
|
|
402
428
|
log('round ' + r + ' validation FAILED on: ' + failedCommands.join(', ') + ' — rolling back this round')
|
|
403
429
|
abortedByValidation = true
|
|
@@ -458,17 +484,23 @@ if (!abortedByValidation) {
|
|
|
458
484
|
}
|
|
459
485
|
// Persist the run's observatory transcript (threads, trend, fixes, checkpoint)
|
|
460
486
|
// so the client observatory panel reflects the run. Writes ONLY .iterate/transcript.json.
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
487
|
+
// The checkpoint survives on disk for aborted runs (that is how resumption
|
|
488
|
+
// works), so the transcript must mirror that reality — a null checkpoint here
|
|
489
|
+
// would hide the very thing F5 is meant to resume. Reflect what is on disk.
|
|
490
|
+
const obsCheckpoint = {
|
|
491
|
+
mode: 'normal',
|
|
492
|
+
round: rounds.length,
|
|
493
|
+
maxRounds: maxRounds,
|
|
494
|
+
fixedCount: fixedCount,
|
|
495
|
+
resumeCount: effectiveResumeCount,
|
|
496
|
+
}
|
|
497
|
+
const stopReason = abortedByValidation
|
|
498
|
+
? (configErrors.length > 0 ? 'aborted_by_config' : 'aborted_by_validation')
|
|
499
|
+
: (converged ? 'converged' : 'max_rounds_reached')
|
|
500
|
+
await agent(
|
|
501
|
+
'Call iterate_transcript({operation:"capture", mode:"normal", goal:' + JSON.stringify(plan.goal) + ', maxRounds:' + maxRounds + ', roundsExecuted:' + rounds.length + ', findingsByRound:' + JSON.stringify(rounds.map(rr => (rr.findings && rr.findings.length) ? rr.findings.length : 0)) + ', stoppedReason:"' + stopReason + '", fixes:' + JSON.stringify(fixRecords) + ', checkpoint:' + JSON.stringify(obsCheckpoint) + ', rounds:' + JSON.stringify(rounds.map(rr => ({ round: rr.round, findings: rr.findings, readFiles: rr.readFiles }))) + '}). Return {operation:"ok"}.',
|
|
502
|
+
{ label: 'transcript:capture' }
|
|
503
|
+
)
|
|
472
504
|
return {
|
|
473
505
|
mode: 'normal',
|
|
474
506
|
goal: plan.goal,
|
|
@@ -477,6 +509,8 @@ return {
|
|
|
477
509
|
converged: converged,
|
|
478
510
|
abortedByValidation: abortedByValidation,
|
|
479
511
|
failedCommands: failedCommands,
|
|
512
|
+
configErrors: configErrors,
|
|
513
|
+
stoppedReason: stopReason,
|
|
480
514
|
findingsFixed: fixedCount,
|
|
481
515
|
remainingArchitecturalCount: architectural.length,
|
|
482
516
|
remainingArchitectural: architectural,
|
|
@@ -494,10 +528,10 @@ return {
|
|
|
494
528
|
Key rules for normal mode:
|
|
495
529
|
- Fixers are the ONLY agents allowed to write files, and they must go through \`iterate_fix\` — never edit files directly. That is what gives every change a backup, a diff, and a rollback path. Reviewers read only. Architectural findings are reported, never auto-fixed.
|
|
496
530
|
- Aggregate the current round deterministically (\`report.findings\`) before fixing, so fixes act on deduped/filtered/sorted findings.
|
|
497
|
-
- **Schema validation & retry**: when \`reviewer.output_schema_validation\` is on (default), retry the round's reviewers up to 2 times when \`aggregate\` reports \`schemaValidation\` valid=false for it, then re-aggregate. Never forward schema-invalid findings into \`iterate_fix\`.
|
|
531
|
+
- **Schema validation & retry**: when \`reviewer.output_schema_validation\` is on (default), retry the round's reviewers up to 2 times (3 attempts total) when \`aggregate\` reports \`schemaValidation\` valid=false for it, then re-aggregate. Never forward schema-invalid findings into \`iterate_fix\`. If the output is still schema-invalid after the 3rd attempt, mark the round inconclusive (never report it as a clean convergence).
|
|
498
532
|
- Apply atomic fixes **per file**: one fixer agent handles all findings for a given file serially (so the same file is never edited concurrently); different files are fixed in parallel.
|
|
499
533
|
- **Resume**: load the checkpoint first; if a previous run left one, continue from \`checkpoint.round + 1\` (its \`fixedCount\` and deduped \`findings\` are carried forward).
|
|
500
|
-
- **Validate after every round** of fixes; on ANY validation failure, roll back the round's fixes via \`iterate_rollback\` and stop (the checkpoint is left in place so the run can be resumed).
|
|
534
|
+
- **Validate after every round** of fixes; on ANY validation failure, roll back the round's fixes via \`iterate_rollback\` and stop (the checkpoint is left in place so the run can be resumed). EXCEPTION: \`iterate_validate\` returning \`allowed:false\` means the command is simply not in \`validation.commands\` — a config gap, not broken code. In that case abort WITHOUT rolling back the round's fixes and surface the missing command + file so the user can fix the trust list.
|
|
501
535
|
- **Checkpoint after every round**; clear it only when the iteration completes cleanly.
|
|
502
536
|
- Stop when a round produces nothing to fix (converged) or maxReviewRounds is reached.
|
|
503
537
|
- Every round, every rollback, and the final report go to the append-only decision log.
|
package/dist/tools/checkpoint.js
CHANGED
|
@@ -19,6 +19,9 @@ import { readQualityGate } from "./quality-store.js";
|
|
|
19
19
|
import { readExperienceBank } from "./experience-store.js";
|
|
20
20
|
import { readDefenseEvents } from "./defense-store.js";
|
|
21
21
|
// ─── Pure helpers (exported for unit tests) ─────────────────────────────────
|
|
22
|
+
/** Max findings persisted in a checkpoint (a model-authored findings payload is
|
|
23
|
+
* bounded, mirroring MAX_FIX_CONTENT_CHARS for fix content). */
|
|
24
|
+
export const MAX_CHECKPOINT_FINDINGS = 1000;
|
|
22
25
|
/** Read the current checkpoint from disk (missing/corrupt → null). */
|
|
23
26
|
export function readCheckpoint(projectRoot) {
|
|
24
27
|
const file = checkpointPath(projectRoot);
|
|
@@ -30,9 +33,28 @@ export function readCheckpoint(projectRoot) {
|
|
|
30
33
|
return null;
|
|
31
34
|
if (parsed.mode !== 'dry-run' && parsed.mode !== 'normal')
|
|
32
35
|
return null;
|
|
33
|
-
|
|
36
|
+
// Strict round gate: non-numeric (incl. NaN — `NaN <= 0` is false and
|
|
37
|
+
// `NaN < 0` also false) round values must not survive into the status.
|
|
38
|
+
const round = typeof parsed.round === 'number' && Number.isFinite(parsed.round)
|
|
39
|
+
? Math.floor(parsed.round)
|
|
40
|
+
: null;
|
|
41
|
+
if (round === null || round < 0)
|
|
34
42
|
return null;
|
|
35
|
-
|
|
43
|
+
const num = (v, fallback) => typeof v === 'number' && Number.isFinite(v) ? v : fallback;
|
|
44
|
+
// Normalize every field so a hand-edited/corrupt checkpoint (string
|
|
45
|
+
// maxRounds, missing fixedCount, …) can never leak a non-numeric value
|
|
46
|
+
// into the status output or crash a consumer.
|
|
47
|
+
return {
|
|
48
|
+
mode: parsed.mode,
|
|
49
|
+
round,
|
|
50
|
+
maxRounds: num(parsed.maxRounds, round),
|
|
51
|
+
fixedCount: num(parsed.fixedCount, 0),
|
|
52
|
+
architecturalCount: num(parsed.architecturalCount, 0),
|
|
53
|
+
resumeCount: num(parsed.resumeCount, 0),
|
|
54
|
+
findings: Array.isArray(parsed.findings) ? parsed.findings : [],
|
|
55
|
+
startedAt: typeof parsed.startedAt === 'string' ? parsed.startedAt : new Date().toISOString(),
|
|
56
|
+
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : new Date().toISOString(),
|
|
57
|
+
};
|
|
36
58
|
}
|
|
37
59
|
catch {
|
|
38
60
|
return null;
|
|
@@ -73,6 +95,15 @@ export function validateCheckpoint(input) {
|
|
|
73
95
|
(typeof input.resumeCount !== 'number' || !Number.isInteger(input.resumeCount) || input.resumeCount < 0)) {
|
|
74
96
|
return 'resumeCount must be a non-negative integer';
|
|
75
97
|
}
|
|
98
|
+
// A round that claims to be past the configured cap is an inconsistent save —
|
|
99
|
+
// the status would render `Round X / Y` with X > Y, which is never legitimate
|
|
100
|
+
// for a resumed run (resume continues AT the checkpoint, it does not exceed
|
|
101
|
+
// the cap).
|
|
102
|
+
if (typeof input.round === 'number' &&
|
|
103
|
+
typeof input.maxRounds === 'number' &&
|
|
104
|
+
input.round > input.maxRounds) {
|
|
105
|
+
return 'round must not exceed maxRounds';
|
|
106
|
+
}
|
|
76
107
|
return null;
|
|
77
108
|
}
|
|
78
109
|
/**
|
|
@@ -95,8 +126,11 @@ export function computeStatus(input) {
|
|
|
95
126
|
}
|
|
96
127
|
}
|
|
97
128
|
const totalRounds = checkpoint?.maxRounds ?? currentRound;
|
|
98
|
-
|
|
99
|
-
|
|
129
|
+
// Coerce the registry's per-round counts defensively: a hand-edited registry
|
|
130
|
+
// round missing fixedCount/failedCount would otherwise feed NaN into the
|
|
131
|
+
// status integer fields.
|
|
132
|
+
const registryFixed = registry.rounds.reduce((sum, r) => sum + (Number(r.fixedCount) || 0), 0);
|
|
133
|
+
const failedCount = registry.rounds.reduce((sum, r) => sum + (Number(r.failedCount) || 0), 0);
|
|
100
134
|
// When a checkpoint exists, its snapshot fields are authoritative for resume
|
|
101
135
|
// (fixedCount / architecturalCount / findings); otherwise derive from the
|
|
102
136
|
// live fix registry and decision log.
|
|
@@ -230,7 +264,7 @@ export function registerCheckpointTool(ctx) {
|
|
|
230
264
|
fixedCount: args.fixedCount,
|
|
231
265
|
architecturalCount: args.architecturalCount,
|
|
232
266
|
resumeCount: (typeof args.resumeCount === 'number' ? args.resumeCount : 0),
|
|
233
|
-
findings: (Array.isArray(args.findings) ? args.findings : []),
|
|
267
|
+
findings: (Array.isArray(args.findings) ? args.findings.slice(0, MAX_CHECKPOINT_FINDINGS) : []),
|
|
234
268
|
startedAt: readCheckpoint(projectRoot)?.startedAt ?? new Date().toISOString(),
|
|
235
269
|
updatedAt: new Date().toISOString(),
|
|
236
270
|
};
|
package/dist/tools/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
2
3
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
3
|
-
import { loadEffectiveConfig, validateConfig, resolveProjectRootForExec } from "../config-loader.js";
|
|
4
|
+
import { defaultConfig, loadEffectiveConfig, validateConfig, resolveProjectRootForExec } from "../config-loader.js";
|
|
4
5
|
import { applyConfigUpdates, readRawConfig, validateConfigUpdates, writeConfigFile, } from "../config-write.js";
|
|
5
6
|
/**
|
|
6
7
|
* Register the `iterate_config` tool.
|
|
@@ -91,7 +92,15 @@ export function registerConfigTool(ctx) {
|
|
|
91
92
|
}
|
|
92
93
|
let base;
|
|
93
94
|
try {
|
|
94
|
-
|
|
95
|
+
// Partial updates must be mergeable on a FRESH project (no config
|
|
96
|
+
// file yet): otherwise `{max_rounds: 5}` alone fails schema
|
|
97
|
+
// validation with a misleading "missing goal". Merge against the
|
|
98
|
+
// built-in defaults when there is no file to read, so the written
|
|
99
|
+
// config captures the effective config (defaults + update) and the
|
|
100
|
+
// documented "write a partial update" contract works out of the box.
|
|
101
|
+
base = existsSync(join(projectRoot, 'iterate.config.yaml'))
|
|
102
|
+
? readRawConfig(join(projectRoot, 'iterate.config.yaml'))
|
|
103
|
+
: defaultConfig();
|
|
95
104
|
}
|
|
96
105
|
catch (err) {
|
|
97
106
|
return { operation: 'write', ok: false, found: false, error: `failed to read config: ${String(err)}` };
|
package/dist/tools/context.js
CHANGED
|
@@ -306,7 +306,11 @@ export function registerContextTool(ctx) {
|
|
|
306
306
|
// skillDir point at arbitrary filesystem locations (e.g. /etc)
|
|
307
307
|
// whose contents would be injected wholesale into the context.
|
|
308
308
|
if (isAllowedSkillDir(args.skillDir, [projectRoot, PLUGIN_SRC_DIR])) {
|
|
309
|
-
|
|
309
|
+
// Read from the REAL (symlink-resolved) path that was actually
|
|
310
|
+
// validated — never the raw path. Validating one path and then
|
|
311
|
+
// reading through a different (swappable) symlink would leave a
|
|
312
|
+
// check-vs-read TOCTOU gap; using the realpath here closes it.
|
|
313
|
+
candidates.push(realpathSync(resolve(args.skillDir)));
|
|
310
314
|
}
|
|
311
315
|
}
|
|
312
316
|
catch {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
12
12
|
import { resolveProjectRootForExec, loadEffectiveConfig } from "../config-loader.js";
|
|
13
|
-
import { readDefenseEvents, writeDefenseEvents, addDefenseEvent } from "./defense-store.js";
|
|
13
|
+
import { readDefenseEvents, writeDefenseEvents, addDefenseEvent, clearDefenseEvents } from "./defense-store.js";
|
|
14
14
|
const DEFAULT_LIMIT = 50;
|
|
15
15
|
const MAX_LIMIT = 100;
|
|
16
16
|
const EVENT_TYPES = [
|
|
@@ -72,19 +72,25 @@ function validateRecordInput(args) {
|
|
|
72
72
|
export function registerDefenseEventsTool(ctx) {
|
|
73
73
|
ctx.tools.register(defineTool({
|
|
74
74
|
name: 'iterate_defense_events',
|
|
75
|
-
// List/counts never write;
|
|
76
|
-
// join a parallel
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
// List/counts never write; `record` persists an event and `clear` removes
|
|
76
|
+
// the persisted stream, so only the read shapes may join a parallel
|
|
77
|
+
// dispatch group.
|
|
78
|
+
isConcurrencySafe: (args) => {
|
|
79
|
+
const op = args.operation;
|
|
80
|
+
return op !== 'record' && op !== 'clear';
|
|
81
|
+
},
|
|
82
|
+
description: 'Query, record, or clear defense events: precondition failures, rollbacks, invariant violations, ' +
|
|
79
83
|
'and assumption falsifications. ' +
|
|
80
84
|
'List/counts return events with descriptions, outcomes, and summary counts; ' +
|
|
81
|
-
'"record" persists a new event to .iterate/defense-events.json
|
|
85
|
+
'"record" persists a new event to .iterate/defense-events.json; ' +
|
|
86
|
+
'"clear" resets the persisted event stream (use it to start a fresh iteration without stale ' +
|
|
87
|
+
'defense data). ' +
|
|
82
88
|
'Use it to review defensive actions taken, or to log one when a defense fires.',
|
|
83
89
|
parameters: {
|
|
84
90
|
operation: {
|
|
85
91
|
type: 'string',
|
|
86
|
-
description: 'Operation: list (browse all), counts (summary by type), record (log a new event). Default: list.',
|
|
87
|
-
enum: ['list', 'counts', 'record'],
|
|
92
|
+
description: 'Operation: list (browse all), counts (summary by type), record (log a new event), clear (reset the stream). Default: list.',
|
|
93
|
+
enum: ['list', 'counts', 'record', 'clear'],
|
|
88
94
|
},
|
|
89
95
|
type: {
|
|
90
96
|
type: 'string',
|
|
@@ -141,6 +147,7 @@ export function registerDefenseEventsTool(ctx) {
|
|
|
141
147
|
kind: { type: 'string' },
|
|
142
148
|
operation: { type: 'string' },
|
|
143
149
|
count: { type: 'integer' },
|
|
150
|
+
counted: { type: 'boolean', description: 'clear only: true when a persisted stream was removed.' },
|
|
144
151
|
events: { type: 'json' },
|
|
145
152
|
counts: { type: 'json' },
|
|
146
153
|
event: { type: 'json' },
|
|
@@ -153,6 +160,16 @@ export function registerDefenseEventsTool(ctx) {
|
|
|
153
160
|
if (!value.ok)
|
|
154
161
|
return [{ type: 'text', text: `defense events query failed: ${value.error}` }];
|
|
155
162
|
const language = value.language === 'zh' ? 'zh' : 'en';
|
|
163
|
+
if (value.operation === 'clear') {
|
|
164
|
+
// Fresh stream: counts are all zero and the event list is empty.
|
|
165
|
+
return [{ type: 'text', text: [
|
|
166
|
+
value.counted === true
|
|
167
|
+
? 'Defense event stream cleared — a fresh iteration starts with an empty stream.'
|
|
168
|
+
: 'No persisted defense event stream to clear.',
|
|
169
|
+
'Defense Event Summary:',
|
|
170
|
+
...EVENT_TYPES.map((type) => ` ${labelFor(type, language)}: 0`),
|
|
171
|
+
].join('\n') }];
|
|
172
|
+
}
|
|
156
173
|
if (value.operation === 'counts' && value.counts) {
|
|
157
174
|
const counts = value.counts;
|
|
158
175
|
const lines = [
|
|
@@ -193,10 +210,32 @@ export function registerDefenseEventsTool(ctx) {
|
|
|
193
210
|
if (!resolved.ok)
|
|
194
211
|
return { ok: false, kind: 'defense_events', error: resolved.reason };
|
|
195
212
|
const projectRoot = resolved.root;
|
|
196
|
-
|
|
213
|
+
// A hand-edited `language` value outside zh/en must not leak out of the
|
|
214
|
+
// declared 'zh' | 'en' enum (labelFor would return undefined for it) —
|
|
215
|
+
// normalize any non-'zh' config value to 'en'.
|
|
216
|
+
const rawConfigLang = loadEffectiveConfig(projectRoot).config.language;
|
|
217
|
+
const configLang = rawConfigLang === 'zh' ? 'zh' : 'en';
|
|
197
218
|
const language = args.language === 'zh' || args.language === 'en' ? args.language : configLang;
|
|
198
219
|
const operation = typeof args.operation === 'string' ? args.operation : 'list';
|
|
199
220
|
const limit = clampLimit(args.limit);
|
|
221
|
+
if (operation === 'clear') {
|
|
222
|
+
// Reset the persisted event stream so a fresh iteration does not carry
|
|
223
|
+
// stale defensive data (mirrors iterate_quality_gate clear / clearQualityGate).
|
|
224
|
+
const result = clearDefenseEvents(projectRoot);
|
|
225
|
+
if (!result.ok) {
|
|
226
|
+
return { ok: false, kind: 'defense_events', operation: 'clear', error: result.error };
|
|
227
|
+
}
|
|
228
|
+
const empty = readDefenseEvents(projectRoot);
|
|
229
|
+
return {
|
|
230
|
+
ok: true,
|
|
231
|
+
kind: 'defense_events',
|
|
232
|
+
operation: 'clear',
|
|
233
|
+
counted: result.existed,
|
|
234
|
+
language,
|
|
235
|
+
counts: empty.counts,
|
|
236
|
+
events: empty.events,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
200
239
|
if (operation === 'record') {
|
|
201
240
|
const errors = validateRecordInput(args);
|
|
202
241
|
if (errors.length > 0) {
|