mingdao-harness 0.2.8 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,109 @@
1
+ # v0.3.0 具体规划:记忆与长跑
2
+
3
+ > 依据:`docs/STRATEGY-0.3.md`(好用×省钱×省心·纵深路线)
4
+ > 定稿:2026-09-03(v0.2.8 之后)
5
+ > 主题一句话:**从「一轮 24 步的失忆问答」升级为「记得项目、续得上任务、省得明白的执行体」。**
6
+ > 说明:本文件是 v0.3.0 的任务级规划(含验收/量级/风险/降级);v0.3.1 及以后的序列见文末。
7
+
8
+ > **落地状态(2026-09-03)**:P0-1 ✅ · P0-2 ✅ · P0-3 ✅ · P2-5 ✅ · **P0-4 ⏸ 顺延 v0.3.1**(理由见 P0-4 项:风险高、验收苛刻,且 P0-2 的「续跑进度摘要注入」已覆盖省钱主场景;完整基线+增量放 v0.3.1 用省钱基准实测收益后再定)。
9
+
10
+ ---
11
+
12
+ ## 一、v0.3.0 选型理由(为什么先做这个)
13
+
14
+ 按 STRATEGY 六:**先定度量尺 → 再打"记忆/上下文"这一口井 → 能力在其上长出来**。
15
+ v0.3.0 落地的三条主线共享同一个地基——**「记忆/上下文」**:项目记忆、任务检查点、增量上下文三件事本质是同一口井(状态怎么存、怎么在有状态的基础上续跑、怎么让续跑不烧钱)。一次把地基打对,语义检索(v0.3.1)、长程任务(v0.3.1)、省钱归因(v0.3.2)都是在地基上长功能,而不是返工。
16
+
17
+ 不进 v0.3.0 的大项(明确顺延,防漂移):跨平台沙箱、WebUI 大改、全模型省钱、SaaS 化——见 STRATEGY「明确不做」。
18
+
19
+ ---
20
+
21
+ ## 二、v0.3.0 任务清单(按优先级)
22
+
23
+ ### P0-1 省钱基准任务集 + bench-savings(度量尺 · STRATEGY 三.6 / 五)
24
+ - **目标**:把"同一件事,MingDao 比裸调 DeepSeek API 省 X%、且任务做得到底"变成**可复现的数字**,纳入回归门禁(防止未来改动悄悄让省钱倒退而不自知)。
25
+ - **内容**:
26
+ 1. 定义 8–12 个代表性任务(代码生成/修复/重构/批量问答/长会话续写),覆盖:中英文、pro/flash、纯读/写文件/混合。
27
+ 2. 新建 `test/bench/bench-savings.mjs`:对每个任务统计并断言——输入/输出/缓存命中 token、费用(峰谷与 Batch 口径)、步数、成功标志;对比基线记录。
28
+ 3. 输出 `docs/SAVINGS-BENCHMARK.md`:口径、任务集、首期基线数字(省 X% 的可引用出处)。
29
+ - **验收**:CI 可复现;指标随版本有对比且允许只降不升(或每次下降触发说明);成功标志不依赖真实 API(用既有 mock 路由/供应商 stub 口径,保证离线)。
30
+ - **量级**:小(纯测试/脚本/文档)。**风险**:低。
31
+
32
+ ### P0-2 任务检查点 + 续跑 v1(好用·能打 · STRATEGY 三.1)
33
+ - **目标**:软化"24 步即死"墙——任务跑到步数上限/被中断时,**留下可续的状态**;`--continue` 与 WebUI 恢复时检测未完成任务,**先看进度再继续,不重来**。
34
+ - **内容**:
35
+ 1. agent 层:runTurn 结果带「任务状态」(`taskState: { goal, done[], next, artifacts[], capHit|interrupted }`),由 agent 在每轮收尾自动维护(新增 `taskStateRef` 参数,与 undoStore 同款进程内共享槽,落盘由会话层做)。
36
+ 2. 会话层:检查点随会话 JSONL 原子落盘(文件名同会话,`{…, checkpoint: {...}}` 或独立 `.taskstate.json` + 原子写)。
37
+ 3. CLI `--continue` / WebUI 恢复会话:检测到未完成 checkpoint → 提示「续跑(注入进度摘要)还是新开」;续跑把进度摘要注入 system(模型先确认目标再继续)。
38
+ 4. 步数语义:命中上限不再是硬终止——收尾总结(v0.2.8 已做)后标记 `capHit: true`,由续跑接管下一轮。
39
+ - **涉及**:`src/agent.js`(stepLimit 收尾分支 + taskState 维护)、`src/session.js`、`src/cli.js --continue`、`src/web/server.js + sessions 域`、`src/prompts.js`(进度摘要注入)。
40
+ - **验收**:真实 >24 步任务可跨 2+ 轮完成;中途 Ctrl+C/断线后 `--continue` 能续跑且不重复已完成写操作(幂等:已完成文件不重写);e2e 增加续跑回归。
41
+ - **量级**:中偏大(跨 CLI/WebUI/agent)。**风险**:状态与真实文件系统的"已完成 vs 未完成"判定是难点——降级方案:续跑首轮强制 `read`/`git status` 类核对再行动(宁可慢不要错)。
42
+
43
+ ### P0-3 项目级自动记忆 v1(记忆地基 · STRATEGY 三.2 前置)
44
+ - **目标**:**换会话/换项目不重来**——每个工作空间沉淀该项目的「关键决定/事实/结构/教训」,下个会话自动注入。
45
+ - **内容**:
46
+ 1. 新增「项目记忆」存储:工作空间目录下 `.mingdao/` 或会话库按工作空间分档;格式与用户记忆同构(markdown/JSONL 条目 + 时间戳 + 来源会话)。
47
+ 2. 自动提取:任务收尾(或每 N 轮)用 executor 模型从"已回填的 assistant/工具结果"提炼 1–3 条可复用事实(与现有 `memory.js` 自动提炼用户偏好同款,`config.autoProjectMemory` 默认开可关)。
48
+ 3. 注入:新任务/会话开始时注入该工作空间项目记忆(v1 先"最近 + 关键词命中",语义排序 v0.3.1);去重合并防膨胀(复用 `dedupeMemory` 思路)。
49
+ - **涉及**:`src/memory.js`(新增 workspaceMemory 系列)、`src/workspace.js`(与工作空间映射联动)、`src/prompts.js`、`src/agent.js`(收尾提取钩子)、WebUI `/api/memory` 展示入口。
50
+ - **验收**:接力测试——会话 A 改 `config` 结构并说明原因 → 新会话 B 同工作空间问起该结构时模型引用 A 的决定;项目记忆不泄漏到其他工作空间;`browse-x/` 这类杂目录不写入(目录卫生)。
51
+ - **量级**:中。**风险**:误提炼/冗余噪音——降级:默认只存"用户显式/强信号"条目,弱信号走 journal 不自动入项目记忆。
52
+
53
+ ### P0-4 增量上下文 v1 = 续跑只传「基线 + 变化」(省钱 · STRATEGY 三.3)
54
+ - **目标**:续跑(P0-2)时不把整段历史重放——发送「上一次已达成的基线快照 + 自基线起的变化」,长会话续跑 prompt 成本从 O(全长) 降为 O(增量)。
55
+ - **内容**:续跑场景复用 v0.2.8 的 trim/压缩结果做"基线",只把 checkpoint 之后的 assistant/tool 段作为增量附在压缩摘要后;非续跑的新会话行为不变。
56
+ - **涉及**:`src/context.js`、`src/compact.js`(基线/增量分界已有雏形)、`src/agent.js`。
57
+ - **验收**:续跑第 2 轮起 prompt_tokens 相比整传显著下降(bench-savings 加断言);关键约束(目标/约束/用户最新要求)不丢——用"关键事实存活"断言(复用压缩质量集思路)。
58
+ - **量级**:大。**风险**:丢了用户最新约束比省钱更糟——**本项允许降级顺延 v0.3.1**:若与 P0-2 冲突,v0.3.0 先只做"续跑注入进度摘要"(不裁剪历史),完整基线+增量放 v0.3.1。验收门槛不达标就不进 v0.3.0。
59
+
60
+ ### P2-5 省心收口:`mingdao diagnose` + CI Node 版本硬门禁(STRATEGY 三.10 / 三.11)
61
+ - **目标**:出事能查、兼容面是真话。
62
+ - **内容**:
63
+ 1. `mingdao diagnose`:一键打包 `logs/` + 最近 `audit` + config(**脱敏**:去 key/密码/token/私网路径)+ 版本/平台/Node 版本/沙箱可用性 → 输出单一文件路径,可直接贴反馈渠道。
64
+ 2. CI 把 Node 18 / 20 / 22 三腿真跑全绿收口(当前矩阵含 18/20/22,但要把"≥18.17"变成显式硬门禁,含 smoke 全量——回应自评报告 Node 20.15.1 崩溃的"兼容面信用"问题)。
65
+ - **涉及**:`src/commands/`(新 diagnose 命令)、`scripts/`(脱敏打包)、`.github/workflows/ci.yml`。
66
+ - **验收**:`mingdao diagnose` 输出可脱敏可用;CI 三 Node 版本全绿为强制(任一腿红则拦发布)。
67
+ - **量级**:小。**风险**:低。
68
+
69
+ ---
70
+
71
+ ## 三、v0.3.0 之外(顺延序列,防漂移备忘)
72
+
73
+ - **v0.3.1「检索与武器」**:零依赖语义检索(项目记忆从"全量→相关条目"注入,`retrieveRelevant` 分词 Jaccard);`git` 只读分析(diff/log/status 等,无 shell 防注入)与 HTTP 只读抓取(SSRF 白名单兜底)工具;省钱仪表盘从设置移出、顶部费用徽标点击展开(KPI + 命中率环状仪表 + 14 天趋势面积图 + 模型/工具 Top 条形 + 最近缓存)。**完整基线+增量上下文(P0-4)顺延 v0.3.2**(风险高,语义检索 + 续跑进度摘要已覆盖主场景)。
74
+ - **v0.3.2「看得见的省钱 + 省心」**:省钱归因面板(单任务省多少/花在哪一步/缓存命中贡献,WebUI 内 SVG 折线已有基础);`diagnose` 的自动反馈模板;覆盖率阈值随版本上调。
75
+ - **v0.3.3「纵深能力」**:跨平台沙箱补位(Windows Job Object / macOS seatbelt,平台无关敏感命令兜底);WebUI「完成任务的舒服工作台」深化(过程可回放/结论可复现)。
76
+ - **明确不进任何近期版本**:全模型省钱平台、React/框架化重构、SQLite(Node ≥22.5 破坏 18/20)、SaaS 托管化。
77
+
78
+ ---
79
+
80
+ ## 四、依赖与顺序
81
+
82
+ ```
83
+ P0-1 省钱基准(度量尺,可最先,纯独立)
84
+
85
+ P0-3 项目记忆 v1(记忆地基,独立) → P0-2 任务续跑 v1(好用主线,核心)
86
+ │ │ (续跑需进度摘要注入)
87
+ └────────────────────────────────────────┴→ P0-4 增量上下文 v1(省钱,可降级顺延)
88
+ P2-5 省心收口(全程可穿插,不阻塞主线)
89
+ ```
90
+
91
+ 实施顺序建议:**P0-1 → P0-2 → P0-3 →(P0-4 视验收决定)→ P2-5**;P0-3 与 P0-2 可并行(一个偏存储/注入、一个偏 agent 状态机),若人力单线程则先 P0-2(好用优先)。
92
+
93
+ ---
94
+
95
+ ## 五、验收总则与发布门禁(沿用既有,不放松)
96
+
97
+ 1. 六套测试全绿 + **bench 并入 P0-1 省钱基准**(194 → 200+ 断言)。
98
+ 2. strict 棘轮 0/0 不回升;coverage ≥60%。
99
+ 3. 发布前自检(release-checklist 第 5 步):纯代码审查找 bug,文件+行号证据,严重度排序。
100
+ 4. 桌面打包冒烟(CI 已内置)与三平台矩阵全绿。
101
+ 5. **防漂移对照**:每项验收前问一次——它服务「好用 / 省钱 / 省心」三词中的哪个?与 STRATEGY-0.3「明确不做」是否冲突?冲突则砍或改,不带着漂移发布。
102
+ 6. 发布后对照 `docs/SAVINGS-BENCHMARK.md` 记录一次基线数字,作为下版"省 X%"的对比起点。
103
+
104
+ ---
105
+
106
+ ## 六、备注(对历史遗留的处置)
107
+
108
+ - 工程卫生顺手清理(不占主线、随 P2-5 一并做):`browse-x/` 杂目录入库问题(加 .gitignore 或删除);`docs/ROADMAP-NEXT.md` 与本文档重叠部分以本文 + STRATEGY-0.3 为准(可后续归档)。
109
+ - 本规划若某一期体量过大,允许 v0.3.0 内部再拆 0.3.0-a / 0.3.0-b 两批(a=好用主线 P0-2/P0-3,b=省钱 P0-1 完成 + P0-4),每批仍走完整发布门禁。
@@ -0,0 +1,42 @@
1
+ # 省钱基准(SAVINGS-BENCHMARK)
2
+
3
+ > 对应规划:`docs/PLAN-v0.3.0.md` P0-1
4
+ > 运行:`node test/bench/bench-savings.mjs`(已并入 `npm run bench`)
5
+ > 首期基线:v0.2.8 代码,2026-09-03 记录
6
+ > 一句话承诺:**「同一件事,MingDao 比裸调 DeepSeek API 省 X%,且任务做得到底」——本文件就是把 X 变成可复现、可回归的数字。**
7
+
8
+ ## 一、口径(怎么测、凭什么可信)
9
+
10
+ - **离线零 API 成本**:全部用项目内置的计价函数(`pricing.js`)、精确词表(`tokenizer.js`)、工具 Schema 生成器(`tools/index.js`)、结果截断(`context.js clampText`)做「朴素基线 vs MingDao 优化」对照,不调用任何真实模型端点。
11
+ - **确定性**:计价时点固定为**闲时**(`2026-08-15T04:00:00Z`,周六 12:00 北京时间)与**高峰**(`2026-09-02T02:00:00Z`,周三 10:00 北京时间),避免真实时钟跨峰谷导致断言漂移。
12
+ - **回归门禁**:每个省钱杠杆都有阈值断言(省 X% 只降不升);未来任何改动若让省钱幅度跌破阈值,`npm run bench` 直接红灯。
13
+ - **诚实边界**:各杠杆是**正交可叠加**的,但不会全部同时生效;「综合 63%」是简单平均(衡量各杠杆各自的省钱幅度),不是"总费用打 6 折"的承诺。实际一次请求同时命中几个杠杆,省多少,取决于任务形态。
14
+
15
+ ## 二、任务集(8 项,随版本复测)
16
+
17
+ | # | 任务(省钱杠杆) | 口径 | 首期基线 |
18
+ |---|---|---|---|
19
+ | ① | 缓存命中计价 | 90% 命中 vs 全未命中(命中价 1/30) | **省 84.5%**(¥0.15450 → ¥0.02400) |
20
+ | ② | 峰谷避峰 | 闲时 vs 高峰(闲时半价) | **省 50.0%**(¥0.11700 → ¥0.05850) |
21
+ | ③ | Batch 半价 | 批量通道 vs 标准(半价) | **省 50.0%** |
22
+ | ④ | 工具 Schema 瘦身 | 已用工具剥描述 vs 全量 | **省 52.1%**(1145 → 549 tokens,每轮 ≈¥0.00089) |
23
+ | ⑤ | 只读阶段收缩 | 只读子集 vs 全量工具 | **省 52.0%**(1145 → 550 tokens) |
24
+ | ⑥ | 工具结果截断 | clampText 上限 vs 原文 | **省 91.7%**(186150 → 15488 tokens) |
25
+ | ⑦ | 精确 tokenizer 不虚高 | 精确 BPE ≤ 启发式上界 | exact 740 ≤ heuristic 795(不漏计、不虚高) |
26
+ | ⑧ | 推理分级契约 | pro 支持 low/high/max(off 可关)、flash 不发 | 契约通过 |
27
+
28
+ **综合(简单平均)**:省 **63%**(各杠杆独立口径,非叠加承诺)。
29
+
30
+ ## 三、与其它 bench 的分工
31
+
32
+ - `bench-cost.mjs`:工具 Schema 瘦身的 **token 数**、A1 两态冻结、护栏前置拦截、Batch 半价的断言。
33
+ - `bench-tokenizer.mjs`:官方词表黄金值(12 组 + 长文本精确断言)。
34
+ - `bench-routing.mjs`:路由标注集(118 条)。
35
+ - `bench-compaction.mjs`:滞回压缩质量集(46 条)。
36
+ - **`bench-savings.mjs`(本文件)**:把以上散点的省钱幅度,收敛成一张"¥ 口径 + 任务集 + 基线数字"的统一账本。
37
+
38
+ ## 四、未来如何更新基线
39
+
40
+ 1. 每个版本发布后,跑一次 `npm run bench`,把本文件「首期基线」列更新为该版本的实测数字,并备注版本号。
41
+ 2. 新增省钱杠杆(如续跑增量上下文、路由省钱归因)时,在 `bench-savings.mjs` 增任务、在本文件增行,阈值对齐「只降不升」。
42
+ 3. 对外引用"省 X%"时,一律指向本文件,不写死营销数字。
@@ -0,0 +1,121 @@
1
+ # MingDao-Harness 战略评估(v0.3.0 方向性依据)
2
+
3
+ > 成文:2026-09-03(v0.2.8 发布后)
4
+ > 性质:战略策划,非具体迭代任务。以后按本文件一步步升级迭代;每期迭代前先对照本文件确认方向不漂移。
5
+ > 一句话定位:**不做"通用 Agent 平台",做 DeepSeek 重度用户/中小团队开箱即用的高可用 Coding Agent 终端——DeepSeek 生态里最好用、最能省、最省心的那一个。**
6
+
7
+ ---
8
+
9
+ ## 〇、核心判断:省钱是"果",好用是"因"
10
+
11
+ 项目里每个省钱手段本质上都在"限制模型自由度"来换取 token:
12
+
13
+ - 只读阶段只发只读工具 → 写意图需多绕一轮
14
+ - 工具 schema 剥描述 → 省输入,但模型参数记忆压力变大
15
+ - 步数上限 24 → 复杂任务到点就停
16
+ - 滞回压缩 → 早期细节变摘要,可能丢关键约束
17
+
18
+ **一旦这些手段让任务做不成、做不对、做一半,省的就不是钱,是用户的信任。** 实证:v0.2.7→v0.2.8 修的"跑满 24 步静默收尾"——文件明明写完了,用户看到"没有总结",体验等于失败。
19
+
20
+ **因此迭代顺序必须倒过来定:好用托底 → 省钱做乘数 → 省心护城河。绝不允许省钱手段以牺牲任务成功率/多轮灵活性为代价。**
21
+
22
+ ---
23
+
24
+ ## 一、现状根因诊断
25
+
26
+ ### 1.1 真正值钱的东西(护城河材料,已实证)
27
+
28
+ | 材料 | 证据 | 定位 |
29
+ |---|---|---|
30
+ | 零依赖 | `src/` 39 文件 ~8.8K 行纯 Node 内置 API;装完即用无 node_modules | 真·稀缺;安装/分发/审计成本全降 |
31
+ | DeepSeek 省钱纵深 | 官方词表 BPE tokenizer(黄金值精确命中)· 缓存命中 1/30 计价 · 峰谷双窗口 · Batch 半价 · 前缀两态冻结 · 滞回压缩 | 同类(含大厂 CLI)没有做到这条链的闭环 |
32
+ | 工程质量闭环 | strict 棘轮 0/0 · coverage 60% · bench 194 断言 · 三平台 CI · release-checklist 自检 · 打包冒烟 · 桌面签名公证 | "省心"的真护城河 |
33
+ | 双界面 + IDE + 同步 | TUI / WebUI(PWA) / VS Code / JetBrains / 桌面 / 云同步 / 36 技能 / MCP | 好用面的广度已够 |
34
+
35
+ ### 1.2 真实短板(按严重度)
36
+
37
+ **好用线(最痛,决定"能不能打"):**
38
+ 1. **能力天花板 = 24 步单回合**:无任务级计划/检查点/续跑,复杂任务(大 repo 迁移、多文件重构、长时任务)到点即断——"能打"的最大瓶颈。
39
+ 2. **长会话上下文整传**:prompt 成本随对话线性涨;压缩靠"丢细节换摘要";大 repo 场景很快撑爆预算。
40
+ 3. **项目理解靠 grep/glob 硬扫**:无语义检索/增量索引,长代码库理解成本高、命中率低。
41
+ 4. **工具面偏窄**:只读集仅 read/ls/glob/grep/skill;缺 git 只读分析、HTTP 只读抓取、文档检索——真实 Coding 任务抓取面不够。
42
+ 5. **新会话失忆**:换会话/换项目 = 重来;无跨会话项目记忆。
43
+ 6. **非 Linux 沙箱空缺**:Windows/macOS 用户多数时间裸跑(ask 兜底,但缺"auto+沙箱"这档)。
44
+
45
+ **省钱线(方向对,但"省"还没被看见):**
46
+ 7. 省钱证据链未外化:用户看不到"这单省了多少 token、缓存命中率贡献"→ 缺钱的可视化归因。
47
+ 8. 路由分类器样本仅 100+ 条;路由错误 = 隐性浪费且用户无感。
48
+
49
+ **省心线(相对最好,仍有一刀):**
50
+ 9. Node 版本兼容面信用:自评实测 smoke 在 Node 20.15.1 尾部崩溃(全项目零 WASM → 判定环境问题),但项目声明"≥18.17"必须真守得住。
51
+ 10. 诊断靠手动翻 `~/.mingdao/logs`,无"一键诊断包"。
52
+
53
+ **工程卫生(小事但真实):** `browse-x/` 未忽略入库的杂目录;`app.js` 仍 ~1000 行单体(v0.2.8 已拆出 util/constants,可继续渐进拆分,但非护城河、低优先)。
54
+
55
+ ---
56
+
57
+ ## 二、差异化定位
58
+
59
+ - 对标物**不是** Claude Code / Codex 的功能全集,而是「用户打开它:DeepSeek 的钱花得明白、任务做得完、装完不操心」。
60
+ - 差异化 = **DeepSeek 垂直切面比谁深 × 工程可信度比谁稳**。
61
+ - 护城河三根桩:**零依赖(分发端)× DeepSeek 成本经济学(省钱端)× 工程质量闭环(信任端)**。
62
+
63
+ ### 避坑提醒(已认真考虑)
64
+ - ❌ 不扩成"全模型省钱平台"(其他模型省钱是伪命题,那是 LiteLLM 赛道)。
65
+ - ❌ 不做 React/框架化 UI 重构(有收益但非护城河,且违背零构建;v0.2.8 原生 ESM 拆分已够)。
66
+ - ❌ 不因"SQLite 内置查询"强上 SQLite(需 Node ≥22.5,破坏 18/20 兼容矩阵,v0.2.8 已确认,忍住)。
67
+
68
+ ---
69
+
70
+ ## 三、纵深路线(三线 + 一层底)
71
+
72
+ ### 线 1|好用 = 能打:Harness 实际能力纵深
73
+ 1. **任务级长程执行**:"24 步单回合" → 「任务意图 → 计划/检查点 → 子任务编排 → 中断可续」;允许任务跨会话续跑、多轮保持上下文焦点。这是"能打"的地基。
74
+ 2. **项目理解升级**:零依赖语义检索(内存向量 + 余弦/哈希相似度),read/grep 从 O(全扫) 降到 O(相关)。
75
+ 3. **上下文增量**:滞回压缩之上再做请求侧增量(只回传变化段,不做整段历史重放)。
76
+ 4. **工具面扩到 Coding 真实场景**:git 只读分析(diff/log/status)、HTTP 只读抓取(SSRF 白名单兜底)、文档结构化检索。
77
+ 5. **WebUI 做"完成任务的舒服工作台"**:任务过程可回放、结论可复现、交付物可跳转。
78
+
79
+ ### 线 2|省钱 = 极限 Token 经济学
80
+ 6. **省钱基准(度量尺,最重要)**:同一任务集下对比 MingDao vs 裸 DeepSeek API vs 对标 CLI,比 token/费用/成功率。护城河用数字证明。
81
+ 7. **钱的可视化归因**:从"今日 ¥x"→"这一单省多少/花在哪一步/缓存命中贡献",让用户看见省钱。
82
+ 8. **推理 token 治理深化**:reasoning effort 按任务复杂度自适应(v0.2.8 已做按模型独立 → 下一步按任务);简单任务默认关思考。
83
+ 9. **路由与压缩前置预算**:分类器样本集扩到数百条,路由/压缩的隐性浪费纳入基准。
84
+
85
+ ### 线 3|省心 = 开箱即用、出事能查
86
+ 10. **一键诊断包**:`mingdao diagnose` → 自动打包 logs + audit + config(脱敏)+ 版本/平台/Node 信息,贴给反馈即查。
87
+ 11. **兼容面信用**:"Node ≥18.17" 变成 CI 真跑 18/20/22 三版本全绿的硬门禁。
88
+ 12. 桌面/安装/更新链路已极成熟,保持即可,不返工。
89
+
90
+ ### 底层|护城河沉淀
91
+ 13. **公开省钱基准 + 工程质量故事**:把"零依赖 + 194 断言 + 棘轮归零 + 打包冒烟 + 三平台"写成可被第三方验证的工程叙事。
92
+ 14. 零依赖保持"单一 `node src/cli.js` 即跑"的纯净;任何新能力先问:能否用 Node 内置实现?不能则不进核心。
93
+
94
+ ---
95
+
96
+ ## 四、明确不做(防漂移清单)
97
+
98
+ - ❌ 全模型省钱平台(LiteLLM 赛道)
99
+ - ❌ React/框架化 UI 重构、SQLite 强上
100
+ - ❌ 云 SaaS / 托管平台化(保持自建优先、用户掌控)
101
+ - ❌ 与主流大厂拼模型能力、拼插件市场广度
102
+ - ✅ 保持:零依赖、零构建、DeepSeek 为中心、自建优先
103
+
104
+ ---
105
+
106
+ ## 五、终极目标与一句话承诺
107
+
108
+ > **"同一件事,MingDao 比直接调 DeepSeek API 省 X%,且任务做得到底。"**
109
+
110
+ 这句话一旦被**公开、可复现的省钱基准**坐实,就是护城河本身:
111
+ 零依赖 → 易分发;工程闭环 → 可信;省钱基准 → 被看见。
112
+ **一切迭代都围绕把这句话做得更硬。**
113
+
114
+ ---
115
+
116
+ ## 六、迭代落地顺序原则
117
+
118
+ 1. **先定「省钱基准任务集」**(这句话的度量尺,决定后面一切数字)。
119
+ 2. **再排 v0.3.0 主线**:长程执行 vs 语义检索 vs 上下文增量——三者共享"记忆/上下文"地基,先做统一设计再分步实现。
120
+ 3. **穿插省心小迭代**:诊断包 / CI Node 版本硬门禁 / 省钱仪表盘。
121
+ 4. 每期迭代发布前对照本文件:新功能是否服务于「好用、省钱、省心」三词之一?是否引入与定位冲突的漂移?——是则调整,否则不做。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mingdao-harness",
3
- "version": "0.2.8",
3
+ "version": "0.3.1",
4
4
  "description": "MingDao Harness —— 开源智能体框架(Agent Harness)。零依赖、开箱即用,针对 DeepSeek-V4 系列优化,开放主流模型接入。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -54,7 +54,7 @@
54
54
  "desktop": "npm --prefix desktop start",
55
55
  "desktop:dist": "node scripts/sync-versions.mjs && npm --prefix desktop run dist:dir",
56
56
  "desktop:sync": "node scripts/sync-versions.mjs",
57
- "bench": "node test/bench/bench-tokenizer.mjs && node test/bench/bench-routing.mjs && node test/bench/bench-compaction.mjs && node test/bench/bench-cost.mjs",
57
+ "bench": "node test/bench/bench-tokenizer.mjs && node test/bench/bench-routing.mjs && node test/bench/bench-compaction.mjs && node test/bench/bench-cost.mjs && node test/bench/bench-savings.mjs",
58
58
  "coverage": "node test/run-all.mjs --coverage && node scripts/coverage-report.mjs",
59
59
  "typecheck:strict": "node scripts/strict-ratchet.mjs",
60
60
  "preversion": "node test/smoke.js && node test/e2e-local.js && npm run bench",
package/src/agent.js CHANGED
@@ -10,13 +10,17 @@ import { makeTokenCounter } from './tokenizer.js';
10
10
  import { createHooks } from './hooks.js';
11
11
  import { createIO, style, C } from './ui.js';
12
12
  import { subagentModel } from './routing.js';
13
- import { writeAudit, redactSecrets } from './audit.js';
13
+ import { writeAudit } from './audit.js';
14
+ import { redactSecrets } from './redact.js';
14
15
  import { checkCostGuard, costGuardConfig, todayCost } from './cost-guard.js';
15
16
  import { estimateCost } from './pricing.js';
16
17
  import { resolveProviderConfig } from './providers/index.js';
17
18
 
18
19
  const MAX_STEPS = 24;
19
- const SUBAGENT_MAX_STEPS = 12;
20
+ // 子代理步数上限:审计/精读类只读子任务需要读多个文件 + 交叉引用,12 步易在「读不全」时被截断
21
+ // (v0.3.0 桌面版审计实测:多个只读子代理报「因达到步数上限停止读取」或「子任务无输出」)。
22
+ // 提到与主循环一致(24),只读子任务每步是 read/grep(输入便宜、无输出 token),成本增量可忽略。
23
+ const SUBAGENT_MAX_STEPS = 24;
20
24
 
21
25
  /**
22
26
  * 创建 Agent 循环(调用方只需传 provider/permission/io/modelName/workingDir,其余可选)
@@ -34,8 +38,8 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
34
38
  // 会话级共享:调用方传入则复用(/model 切换、子代理均共享,undo 不丢失)
35
39
  const undo = undoStore || { backups: new Map() };
36
40
  const stepLimit = maxSteps || MAX_STEPS;
37
- // 只读工具集合(子代理只读模式 + 并行批次共用)
38
- const READONLY_TOOLS_SET = new Set(['read', 'ls', 'glob', 'grep', 'skill']);
41
+ // 只读工具集合(子代理只读模式 + 并行批次共用)。v0.3.1 起含 git/fetch(只读、审计常用)
42
+ const READONLY_TOOLS_SET = new Set(['read', 'ls', 'glob', 'grep', 'skill', 'git', 'fetch']);
39
43
  // 精确 token 计数:DeepSeek 词表,其他模型回退启发式
40
44
  const count = makeTokenCounter(modelName);
41
45
  // MCP 工具集(每次取,服务器晚就绪也能在后续轮次出现)
@@ -45,7 +49,7 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
45
49
  const usedToolNames = new Set();
46
50
  // 省钱 B1(按需挂载):回合起始为「只读阶段」时只发只读工具(read/ls/glob/grep/skill/todo)
47
51
  // + 已用过的工具;检测到写意图(用户消息或模型明说需要写/改/建)后注入全量工具。
48
- const READONLY_TIER_SET = new Set(['read', 'ls', 'glob', 'grep', 'skill', 'todo']);
52
+ const READONLY_TIER_SET = new Set(['read', 'ls', 'glob', 'grep', 'skill', 'todo', 'git', 'fetch']);
49
53
  // 中英双语写意图(CodeArts 报告:纯中文正则让英文会话整回合只读死锁)
50
54
  const WRITE_INTENT_RE = /写|建|创|改|修|删|装|加|添|增|补|换|移|部署|执行|运行|实现|重构|生成|迁移|安装|更新|升级|发布|调整|优化|修复|提交|推送|打包|编译|测试|implement|fix|create|modify|update|delete|deploy|build|make|generate|install|write|refactor|migrate|test|run|commit|push|remove|add|change|patch/i;
51
55
  const hasWriteIntent = (/** @type {any} */ text) => WRITE_INTENT_RE.test(String(text || ''));
@@ -121,6 +125,10 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
121
125
  async function runTurn(/** @type {any} */ messages) {
122
126
  let steps = 0;
123
127
  let finish = null;
128
+ // v0.3.1 自动续跑(长程执行):跑满 stepLimit 步后不再直接中断,而是注入进度摘要再续跑,
129
+ // 最多 maxRounds 轮(默认 3,可用 cfg.maxRounds 调);审计/重构等大任务不再「一步中断」。
130
+ const maxRounds = Math.max(1, Number(cfg.maxRounds) || 3);
131
+ let round = 0;
124
132
  const usage = /** @type {{ prompt_tokens: number, completion_tokens: number, prompt_cache_hit_tokens?: number, prompt_cache_miss_tokens?: number }} */ ({ prompt_tokens: 0, completion_tokens: 0 });
125
133
  const startedAt = Date.now();
126
134
  // 回合性能指标(状态栏:LLM 时长 / 工具时长 / 首 token 延迟 / 步数)
@@ -130,6 +138,7 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
130
138
  // 省钱 B3(费用二级分账):推理 token 估算(按增量累计)与逐工具调用/耗时累加
131
139
  let reasoningTokens = 0;
132
140
  const toolStats = /** @type {Map<string, {calls: number, ms: number}>} */ (new Map());
141
+ const deliverables = /** @type {string[]} */ ([]); // 本回合 write/edit 成功落盘的文件路径(去重)
133
142
  const perf = () => ({
134
143
  llmMs: llmMsTotal,
135
144
  toolMs: toolMsTotal,
@@ -138,6 +147,7 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
138
147
  reasoningTokens,
139
148
  toolStats: [...toolStats.entries()].map(([tool, s]) => ({ tool, calls: s.calls, ms: s.ms })),
140
149
  usedModel: activeModel, // 省钱 B4:本回合实际使用模型(降级后归属它)
150
+ deliverables: [...deliverables], // v0.3.1:CLI/REPL 续跑检查点复用(此前 artifacts 恒空)
141
151
  });
142
152
  let aborted = false;
143
153
  let emptyRounds = 0; // 连续空/截断输出计数(防止无限续写)
@@ -174,7 +184,9 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
174
184
  }
175
185
  };
176
186
  try {
177
- while (steps < stepLimit) {
187
+ for (round = 0; round < maxRounds; round++) {
188
+ steps = 0;
189
+ while (steps < stepLimit) {
178
190
  steps += 1;
179
191
  // 同回合只读工具去重(Hermes C4):相同 name+args 的只读调用只执行一次,结果复用回填
180
192
  const turnToolCache = new Map();
@@ -484,6 +496,11 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
484
496
  ts.ms += ms;
485
497
  toolStats.set(prep.name, ts);
486
498
  io.renderTool(prep.name, prep.args, result, ms);
499
+ // v0.3.1 P1-2 修复:write/edit 成功落盘的路径记入交付物(CLI/REPL 续跑检查点用)
500
+ if ((prep.name === 'write' || prep.name === 'edit') && prep.args?.path && result && result.ok !== false) {
501
+ const p = String(prep.args.path);
502
+ if (p && !deliverables.includes(p)) deliverables.push(p);
503
+ }
487
504
  if (prep.name === 'todo' && result?.todos) io.renderTodo(result.todos);
488
505
  hooks.post(prep.name, prep.args, typeof result === 'string' ? { output: result } : result).catch(() => {});
489
506
  // 审计(P3-5):执行结果摘要(含退出码/超时/输出大小)
@@ -563,9 +580,9 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
563
580
  }
564
581
  }
565
582
  }
566
- // v0.2.8 步数预留收尾(对齐 DSH):仅剩最后一轮时,工具结果回填后追加收尾指令,
567
- // 让模型在末轮输出「总结文字 + 交付物清单」,而非跑满步数后静默结束。
568
- if (steps === stepLimit - 1) {
583
+ // v0.2.8 步数预留收尾(对齐 DSH):仅「最后一轮」的末步追加收尾指令,
584
+ // 让模型在末轮输出总结;中间轮不注入(交给自动续跑继续干活,而非提前收尾中断)。
585
+ if (steps === stepLimit - 1 && round === maxRounds - 1) {
569
586
  messages.push({
570
587
  role: 'user',
571
588
  content:
@@ -642,6 +659,16 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
642
659
  };
643
660
  }
644
661
  }
662
+ // v0.3.1 自动续跑(长程执行):还有剩余轮次且未中断 → 注入进度摘要直接续跑,不落收尾总结
663
+ if (round < maxRounds - 1 && !aborted) {
664
+ const art = deliverables.length ? '已交付文件:' + deliverables.join('、') + '。' : '';
665
+ messages.push({
666
+ role: 'user',
667
+ content: `(系统提示)已连续执行 ${stepLimit} 步工具操作,任务尚未完成,请继续完成剩余工作。${art}先核对已完成部分(勿重复),再做未完成的部分。`,
668
+ });
669
+ io.print(style(`♻ 步数上限,自动续跑第 ${round + 2} 轮…`, C.dim));
670
+ continue;
671
+ }
645
672
  io.endTurn();
646
673
  // 步数上限:清掉未执行的 tool_calls,避免下一轮/恢复后 API 400
647
674
  stripOrphanCalls();
@@ -674,10 +701,14 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
674
701
  usage.completion_tokens += wrapRes.usage.completion_tokens || 0;
675
702
  }
676
703
  if (wrapRes.text) messages.push({ role: 'assistant', content: wrapRes.text });
677
- return { text: wrapRes.text || null, reasoning: wrapRes.reasoning || '', usage, steps, finish, truncated: false, aborted: false, durationMs: Date.now() - startedAt, perf: perf() };
704
+ // capHit:本轮因步数上限被迫收尾(任务可能未真正完成)→ 供上层落检查点续跑
705
+ return { text: wrapRes.text || null, reasoning: wrapRes.reasoning || '', usage, steps, finish, truncated: false, aborted: false, capHit: true, durationMs: Date.now() - startedAt, perf: perf() };
678
706
  } catch {}
679
707
  }
680
- return { text: null, reasoning: '', usage, steps, finish, truncated: true, aborted: false, durationMs: Date.now() - startedAt, perf: perf() };
708
+ return { text: null, reasoning: '', usage, steps, finish, truncated: true, aborted: false, capHit: true, durationMs: Date.now() - startedAt, perf: perf() };
709
+ }
710
+ // 理论不可达(for 循环末轮必 return);给 tsc 一个兜底,保证 runTurn 恒有返回值
711
+ return { text: null, reasoning: '', usage, steps, finish, truncated: true, aborted: false, capHit: true, durationMs: Date.now() - startedAt, perf: perf() };
681
712
  } finally {
682
713
  currentAc = null;
683
714
  offSigint();
package/src/audit.js CHANGED
@@ -7,6 +7,7 @@
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { mingdaoHome, ensureHome } from './config.js';
10
+ import { redactSecrets } from './redact.js';
10
11
 
11
12
  const MAX_LINES = 20000;
12
13
  const KEEP_LINES = 10000;
@@ -16,10 +17,8 @@ export function auditFile() {
16
17
  return path.join(mingdaoHome(), 'audit.jsonl');
17
18
  }
18
19
 
19
- // 轻量脱敏:sk- API Key 掩码(审计日志可安全共享排查)
20
- export function redactSecrets(/** @type {any} */ text) {
21
- return String(text ?? '').replace(/(sk-[A-Za-z0-9_-]{6,})/g, 'sk-***');
22
- }
20
+ // 轻量脱敏(统一单一来源 v0.3.1 P1-1):sk-/ghp_ 等常见前缀掩码,见 src/redact.js
21
+ export { redactSecrets };
23
22
 
24
23
  export function writeAudit(/** @type {any} */ entry) {
25
24
  try {
package/src/cli.js CHANGED
@@ -34,6 +34,7 @@ import {
34
34
  formatScheduleRow,
35
35
  } from './schedule.js';
36
36
  import { createAgent } from './agent.js';
37
+ import { saveTaskStateMerge, clearTaskState } from './task-state.js';
37
38
  import { createPermission } from './permissions.js';
38
39
  import { buildSystemPrompt } from './prompts.js';
39
40
  import { listSkills, tamperedSkillNames } from './skills.js';
@@ -82,6 +83,7 @@ const HELP_LINES = [
82
83
  [' mingdao update [--check] 一键自更新(git 安装形态;--check 只对比版本)', null],
83
84
  [' mingdao rollback 回滚到上次 update 之前的提交', null],
84
85
  [' mingdao audit [数量] 查看工具调用审计日志(默认最近 20 条)', null],
86
+ [' mingdao diagnose 一键生成诊断报告(脱敏打包日志/审计/配置,便于反馈排查)', null],
85
87
  [' mingdao desktop 启动桌面版(Electron,任意目录可用,托盘常驻)', null],
86
88
  [' mingdao --help / --version 帮助 / 版本', null],
87
89
  ['', null],
@@ -188,6 +190,7 @@ async function main() {
188
190
  sessions: { module: 'skill', handler: 'handleSessions' },
189
191
  key: { module: 'key', handler: 'handleKey' },
190
192
  desktop: { module: 'desktop', handler: 'handleDesktop' },
193
+ diagnose: { module: 'diagnose', handler: 'handleDiagnose' },
191
194
  });
192
195
  const hit = dispatchTable[opts.prompt[0]];
193
196
  if (hit) {
@@ -469,6 +472,18 @@ async function main() {
469
472
  if (renamed) io.print(style(`✓ 会话标题:${path.basename(renamed)}`, C.dim));
470
473
  }
471
474
  }
475
+ // v0.3.0 P0-2:单次提问跑满步数/中断落检查点(--continue 可续跑),正常完成清除
476
+ if (res.capHit || res.aborted) {
477
+ saveTaskStateMerge(path.basename(session.file), {
478
+ goal: question,
479
+ progress: res.text || '',
480
+ artifacts: res.perf?.deliverables || [],
481
+ status: res.capHit ? 'cap' : 'interrupted',
482
+ updatedAt: new Date().toISOString(),
483
+ });
484
+ } else {
485
+ clearTaskState(path.basename(session.file));
486
+ }
472
487
  try {
473
488
  await finalizeSession({ cfg, provider, model: titleModel(cfg, modelName), home, workingDir, messages, turns: 1, lastText: res.text || '' });
474
489
  } catch {}
@@ -0,0 +1,95 @@
1
+ // 命令族:mingdao diagnose(v0.3.0 P2-5):一键打包诊断信息(脱敏),便于贴反馈渠道排查。
2
+ // 只读 + 单文件输出:环境/版本/config(脱敏)/日志尾/审计尾/工作空间,凭证库只注明存在不读内容。
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { createIO, style, C } from '../ui.js';
8
+ import { mingdaoHome, ensureHome, loadConfig } from '../config.js';
9
+ import { credentialsPath } from '../credentials.js';
10
+ import { listAudit } from '../audit.js';
11
+ import { redactSensitive } from '../redact.js';
12
+ import { projectMemoryFile, loadProjectMemory } from '../memory.js';
13
+ import { listWorkspaces } from '../workspace.js';
14
+ import { detectSandbox } from '../tools/bash.js';
15
+
16
+ function tailFile(/** @type {any} */ file, /** @type {number} */ lines = 60) {
17
+ try {
18
+ const raw = fs.readFileSync(file, 'utf8');
19
+ return raw.split('\n').filter(Boolean).slice(-lines).join('\n');
20
+ } catch {
21
+ return '';
22
+ }
23
+ }
24
+
25
+ export async function handleDiagnose(/** @type {any} */ _cmd, /** @type {any} */ _args) {
26
+ const io = createIO();
27
+ try {
28
+ ensureHome();
29
+ const home = mingdaoHome();
30
+ let version = '未知';
31
+ try {
32
+ const pkg = JSON.parse(fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json'), 'utf8'));
33
+ version = pkg.version || '未知';
34
+ } catch {}
35
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
36
+ const outFile = path.join(home, `diagnose-${stamp}.txt`);
37
+ const L = /** @type {string[]} */ ([]);
38
+ const push = (/** @type {string} */ s) => L.push(s);
39
+
40
+ push('# MingDao Harness 诊断报告');
41
+ push('生成时间:' + new Date().toISOString());
42
+ push('');
43
+ push('## 环境');
44
+ push(`- 版本:${version}`);
45
+ push(`- Node:${process.version} · 平台:${process.platform} · 架构:${process.arch}`);
46
+ push(`- 系统:${os.type()} ${os.release()}`);
47
+ push(`- 工作目录:${redactSensitive(process.cwd())}`);
48
+ push(`- 沙箱(bubblewrap):${detectSandbox()}`);
49
+
50
+ push('');
51
+ push('## 配置(config.json,已脱敏)');
52
+ const cfg = loadConfig();
53
+ push(cfg ? redactSensitive(JSON.stringify(cfg, null, 2)) : '(不存在或读取失败)');
54
+
55
+ push('');
56
+ push('## 凭证库');
57
+ push(`${redactSensitive(credentialsPath())}:${fs.existsSync(credentialsPath()) ? '已存在(内容不打包)' : '无'}`);
58
+
59
+ push('');
60
+ push('## 服务端日志(logs/web-server.log 尾部 60 行)');
61
+ const srvLog = tailFile(path.join(home, 'logs', 'web-server.log'), 60);
62
+ push(srvLog ? redactSensitive(srvLog) : '(无日志)');
63
+
64
+ push('');
65
+ push('## 审计(最近 20 条,已脱敏)');
66
+ const audits = listAudit(20);
67
+ if (!audits.length) push('(无审计记录)');
68
+ for (const a of audits) {
69
+ push(redactSensitive(JSON.stringify(a)));
70
+ }
71
+
72
+ push('');
73
+ push('## 工作空间与项目记忆');
74
+ const wss = listWorkspaces();
75
+ if (!wss.length) push('(未登记工作空间)');
76
+ for (const w of wss) {
77
+ push(`- ${w.name} → ${redactSensitive(w.dir || '')}`);
78
+ const mem = loadProjectMemory(w.dir);
79
+ const memPath = redactSensitive(projectMemoryFile(w.dir));
80
+ push(` 项目记忆:${memPath}(${mem ? mem.split('\n').filter(Boolean).length + ' 条' : '无'})`);
81
+ }
82
+ // 当前目录的项目记忆(即使未登记为工作空间也能定位,便于排查「记忆写哪了」)
83
+ const cwdMem = projectMemoryFile(process.cwd());
84
+ push(`当前目录项目记忆:${redactSensitive(cwdMem)}(${fs.existsSync(cwdMem) ? '存在' : '无'})`);
85
+
86
+ fs.writeFileSync(outFile, L.join('\n') + '\n');
87
+ io.print(style(`✓ 诊断报告已生成:${outFile}`, C.green));
88
+ io.print(style('请把该文件内容贴到反馈渠道排查;密钥/token/私网路径已脱敏。', C.dim));
89
+ } catch (/** @type {any} */ err) {
90
+ io.print(style('[错误] ' + (err?.message || err), C.red));
91
+ } finally {
92
+ io.close();
93
+ }
94
+ return true;
95
+ }
@@ -54,6 +54,7 @@ import {
54
54
  relativeTime,
55
55
  searchSessions,
56
56
  } from '../session.js';
57
+ import { loadTaskState, saveTaskStateMerge, clearTaskState, resumePrompt } from '../task-state.js';
57
58
 
58
59
  const pkg = JSON.parse(fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
59
60
 
@@ -223,6 +224,12 @@ export async function runRepl(ctx) {
223
224
  let messages = hasOldSystem
224
225
  ? [{ role: 'system', content: systemPrompt }, ...loadedMsgs.slice(1)]
225
226
  : [{ role: 'system', content: systemPrompt }, ...loadedMsgs];
227
+ // v0.3.0 P0-2:续跑——载入有未完成检查点的会话时注入进度摘要,让模型从断点继续
228
+ const resumeTs = loadTaskState(path.basename(session.file));
229
+ if (resumeTs && (resumeTs.status === 'cap' || resumeTs.status === 'interrupted')) {
230
+ messages.push({ role: 'user', content: resumePrompt(resumeTs) });
231
+ io.print(style('⚠ 检测到未完成任务,已注入续跑提示(完成后自动清除)。', C.yellow));
232
+ }
226
233
  tuiState.persisted = messages.length;
227
234
  let lastUsage = null;
228
235
  let lastText = '';
@@ -673,6 +680,18 @@ export async function runRepl(ctx) {
673
680
  if (renamed) io.print(style(`✓ 会话标题:${path.basename(renamed)}`, C.dim));
674
681
  }
675
682
  }
683
+ // v0.3.0 P0-2:跑满步数(capHit)或中断(aborted)落检查点,正常完成清除
684
+ if (res.capHit || res.aborted) {
685
+ saveTaskStateMerge(path.basename(session.file), {
686
+ goal: input,
687
+ progress: res.text || '',
688
+ artifacts: res.perf?.deliverables || [],
689
+ status: res.capHit ? 'cap' : 'interrupted',
690
+ updatedAt: new Date().toISOString(),
691
+ });
692
+ } else {
693
+ clearTaskState(path.basename(session.file));
694
+ }
676
695
  if (res.aborted) {
677
696
  io.print(style('(已中断)', C.dim));
678
697
  }