lume-dsh-plugin 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -5
- package/lib/core/ledger.js +211 -0
- package/lib/core/signals.js +64 -0
- package/lib/host/methods.js +64 -0
- package/lib/host/project.js +222 -0
- package/lib/host/reflection.js +27 -6
- package/lib/host/rpc.js +15 -0
- package/lib/host/session-runtime.js +10 -0
- package/lib/host/triggers.js +133 -0
- package/lib/index.js +358 -16
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
人设只影响自然语言表达,不介入任务执行,也不影响代码、命令与工具调用的结果。
|
|
29
29
|
|
|
30
30
|
[](https://github.com/cayan0x/Lume/actions/workflows/ci.yml)
|
|
31
|
-
[](./CHANGELOG.md)
|
|
32
32
|
[](./LICENSE)
|
|
33
33
|
|
|
34
34
|
*人设系统:内置角色卡、蒸馏与管理入口,以及记忆星图*
|
|
@@ -104,6 +104,10 @@ DSH 本身不带 Office / PDF 读写能力:附件只接受图片,工具名
|
|
|
104
104
|
- [x] 文档能力感知:探测文档工具并按需注入——有工具要求先读后写与回读验证,没工具要求如实说明边界、不硬解二进制
|
|
105
105
|
- [x] 系统提示词轮内稳定:意图冻结(只认真实用户消息)+ 会变的内容走 runtime-context 通道,一轮只产生一份系统提示词
|
|
106
106
|
- [x] 注入分层:system 段在会话内逐字节恒定(协议/契约/身份/纪律),记忆、语料、播报与任务指令全部走对话尾部快照——每一步都吃住前缀缓存
|
|
107
|
+
- [x] 任务载具:任务契约(数量先估后回填、交付按原始判据对账)+ 改动台账 + 假设台账(含已排除项)+ 跨会话项目知识
|
|
108
|
+
- [x] 行为触发器:按轨迹纠偏——撒网不收敛 / 连写不验 / 死路重撞(环境类给验证降级阶梯)/ 契约对账 / 项目知识采集
|
|
109
|
+
- [x] 方法层:文档编辑方法(结构 → 最小编辑 → 一致性 → 回读)、改动影响面清单、结构定位提示
|
|
110
|
+
- [x] 反思日志第五维「诊断深度与假设管理」:跨会话提醒能指向思维方式而非只有纪律
|
|
107
111
|
- [x] 角色卡算法自动升级且保留记忆、风格和认可语料
|
|
108
112
|
|
|
109
113
|
### 为什么不接管宿主的历史压缩
|
|
@@ -206,6 +210,40 @@ Lume 因此选择「观察 + 重锚」:压缩发生时记录规模,在随后
|
|
|
206
210
|
- **导入校验** —— 解析时校验格式、版本、键名合法性,内置人设名受保护,不可覆盖
|
|
207
211
|
- **跨设备迁移** —— 一张卡片即可还原人设的完整身份(记忆、风格、档案名),无需额外配置
|
|
208
212
|
|
|
213
|
+
## 任务载具:把量化与台账变成可检查的产出
|
|
214
|
+
|
|
215
|
+
纪律(协议正文)教的是「不越权、要验证、要复核」;**方法**教的是「怎么把一个任务收敛成可核对的产物」。后者需要载体——模型通常不是不知道要量化,而是**没有地方放量化结果**。四个载具都由模型自己写(工具调用、零额外 LLM 调用),每轮按状态回显到对话尾部:
|
|
216
|
+
|
|
217
|
+
| 载具 | 工具 | 内容 | 生命周期 |
|
|
218
|
+
|---|---|---|---|
|
|
219
|
+
| **任务契约** | `lume_contract` | 目标 / 范围 / **数量(先估后回填)** / 完成判据 / 非目标 / 待确认 | 会话内;交付轮自动切成**对账口径**——对照的是开工时写下的原始判据,防「判据漂移」 |
|
|
220
|
+
| **改动台账** | `lume_change` | 文件·符号·章节 → 改什么 → 为什么 → 怎么验 → 状态 | 会话内;文档任务用章节名当 target,形成分节记账 |
|
|
221
|
+
| **假设台账** | `lume_hypothesis` | 假设 + 证据 + 状态(含**已排除**) | 会话内;已排除项照常回显,避免重复验证同一个假设 |
|
|
222
|
+
| **项目知识** | `lume_project_note` | 构建/测试命令、模块链路、仓库约定、死路记录 | **跨会话**,按工作目录归属 |
|
|
223
|
+
|
|
224
|
+
项目知识为什么单独存放:代码路径与仓库约定是**工作事实**,换人设不该失忆,也不该随人设卡被导出分享。环境性死路会被自动记入(例如「本机 Maven 离线仓库为空」),下次会话不必重踩。
|
|
225
|
+
|
|
226
|
+
## 行为触发器:把「元决策」从用户手里接过来
|
|
227
|
+
|
|
228
|
+
一次真实会话的轨迹复盘(2 轮 / 58 步 / 106 次工具调用)显示:**不是不知道该收敛,而是在压力下没有执行**——34 次广度探查不收敛、25 次改动里 18 次连击无验证、17 次命令全在撞同一个不可用的构建环境、交付前靠用户发话才自审。协议正文管不了这种,因为文本是静态的,而症状是**轨迹**的。所以只在行为模式成立时注入一句带具体数字的提醒:
|
|
229
|
+
|
|
230
|
+
| 触发器 | 触发条件 | 注入 |
|
|
231
|
+
|---|---|---|
|
|
232
|
+
| 收敛提醒 | 连续 ≥12 次只读探查且尚未写契约 | 停止撒网,先复述「入口 → 数据流 → 影响面」并写台账 |
|
|
233
|
+
| 增量验证 | 连续 ≥6 次改动没有任何验证动作 | 改一处验一处,先验证前一批 |
|
|
234
|
+
| 死路重撞 | 同一验证连续失败 ≥3 次 | 环境类占多数 → **验证降级阶梯**(编译器 → 语法检查 → 静态交叉引用 → 手工走读 + 风险清单);否则要求先归因并写假设台账 |
|
|
235
|
+
| 假设维护 | 诊断模式下验证失败但假设未更新 | 更新假设状态,标出已排除项 |
|
|
236
|
+
| 契约对账 | 有契约且每 3 轮 / 压缩后 | 用原始判据逐项对账,标注已验证 / 未验证 / 偏离 |
|
|
237
|
+
| 项目知识采集 | 无契约且步数 ≥20(每会话一次) | 提醒把稳定项目事实记下来 |
|
|
238
|
+
|
|
239
|
+
每类触发器每轮最多一次且有轮级冷却(提示一多就变噪音);工具按**行为类别**(inspect / mutate / verify / plan)归类而非按名字,宿主或扩展换名不失效。
|
|
240
|
+
|
|
241
|
+
## 方法层:按任务形态注入
|
|
242
|
+
|
|
243
|
+
- **文档任务**:先取结构(标题层级 / 表格清单 / 编号体系)→ 最小编辑保留格式与编号 → 术语与称谓全文一致 → 交付前回读改动区域并列出「改了什么 / 没动什么 / 未核对什么」
|
|
244
|
+
- **执行轮**:改动影响面清单(谁调用它、被谁实现、配置与 SQL 映射、前端引用)+ 每处改动的验证方式
|
|
245
|
+
- **有结构分析工具时**:建议用符号级定位替代通篇 read
|
|
246
|
+
|
|
209
247
|
## 分层注入与 Token 预算
|
|
210
248
|
|
|
211
249
|
注入分两层。这不是洁癖,是**前缀缓存的前提**:
|
|
@@ -228,6 +266,9 @@ Lume 因此选择「观察 + 重锚」:压缩发生时记录规模,在随后
|
|
|
228
266
|
| 风格层 | 易变 | 10 条 ~250 | top5 ~120 | 同上 |
|
|
229
267
|
| 任务指令(路由/阶段/护栏/锚点) | 易变 | ~600 | 按需 | 6 轮前不注入护栏与锚点,闲聊不注入任务条款 |
|
|
230
268
|
| 文档能力指引 | 易变 | 常驻 ~120 | 文档任务轮 ~120,其余 0 | 工具能力探测 + 按轮触发 |
|
|
269
|
+
| 任务载具(契约 / 台账 / 假设 / 项目知识) | 易变 | 0 | 任务轮 ~150-400,**只在内容变化时付费** | 模型主动写入;尾部快照按内容差异提交 |
|
|
270
|
+
| 方法块(文档方法 / 影响面 / 结构提示) | 易变 | 常驻 ~400 | 按形态 80-200 | 文档轮 / 执行轮 / 有分析工具时才注入 |
|
|
271
|
+
| 触发器提醒 | 易变 | 0 | 触发时 80-120 | 六类行为模式 + 轮级冷却 |
|
|
231
272
|
|
|
232
273
|
- 恒定段(协议 + 契约 + 身份)约 **900 tok**,在一个会话里建一次、之后每步都是缓存命中
|
|
233
274
|
- 易变段稳态约 **700~1,200 tok/步**,全部落在对话尾部:改它只花自己那点 token,不动前面的任何前缀
|
|
@@ -246,6 +287,9 @@ Lume 因此选择「观察 + 重锚」:压缩发生时记录规模,在随后
|
|
|
246
287
|
| `injectionStrategy` | `"topk"` | `"topk"` 相关性检索 / `"full"` 全量注入 |
|
|
247
288
|
| `personaOrder` | 10000 | 人设契约段(恒定段)在 system prompt 中的排序:贴着对话历史的注意力最强位 |
|
|
248
289
|
| `layeredInjection` | `true` | 分层注入:system 段只留会话恒定文本,易变内容走 runtime-context(对话尾部快照)。置 `false` 退回旧行为做对照;宿主不支持该通道时自动降级 |
|
|
290
|
+
| `projectMemory` | `true` | 项目知识(构建/测试命令、模块链路、约定、死路)按工作目录跨会话累积;与人格记忆分开存放 |
|
|
291
|
+
| `behaviorTriggers` | `true` | 行为触发器:撒网不收敛 / 连写不验 / 死路重撞 / 判据漂移的按轨迹提醒 |
|
|
292
|
+
| `triggerInspectStreak` / `triggerChangeStreak` / `triggerDeadPathFails` | 12 / 6 / 3 | 三类触发器的阈值(只读探查连击 / 改动连击 / 验证失败连击) |
|
|
249
293
|
| `switchBoundaryTurns` | 2 | 切换播报边界窗口(按用户轮计) |
|
|
250
294
|
| `extractionEnabled` | `true` | 被动提取开关 |
|
|
251
295
|
| `extractionCooldownMs` | 600000 | 被动提取冷却(毫秒) |
|
|
@@ -278,7 +322,7 @@ dsh plugin add lume-dsh-plugin
|
|
|
278
322
|
若网络无法访问 npm,也可直接从仓库安装:
|
|
279
323
|
|
|
280
324
|
```bash
|
|
281
|
-
dsh plugin add github:cayan0x/Lume#v0.
|
|
325
|
+
dsh plugin add github:cayan0x/Lume#v0.7.0
|
|
282
326
|
```
|
|
283
327
|
|
|
284
328
|
安装后需**完全重启 DSH(包含托盘进程)**方可加载;启动日志中出现 `lume: 已加载(builtins=loli,senpai,butler,tsundere,none)` 即表示加载成功。构建产物随仓库发布,两种路径都不需要本地构建。
|
|
@@ -290,7 +334,7 @@ dsh plugin add github:cayan0x/Lume#v0.6.2
|
|
|
290
334
|
```bash
|
|
291
335
|
dsh plugin add lume-dsh-plugin # npm(推荐)
|
|
292
336
|
# 或
|
|
293
|
-
dsh plugin add github:cayan0x/Lume#v0.
|
|
337
|
+
dsh plugin add github:cayan0x/Lume#v0.7.0 # GitHub(备选)
|
|
294
338
|
```
|
|
295
339
|
|
|
296
340
|
人设选择、记忆与风格数据存放在 `storages/` 目录,升级不会丢失。
|
|
@@ -300,7 +344,7 @@ dsh plugin add github:cayan0x/Lume#v0.6.2 # GitHub(备选)
|
|
|
300
344
|
### 指定其他版本
|
|
301
345
|
|
|
302
346
|
```bash
|
|
303
|
-
dsh plugin add lume-dsh-plugin@0.
|
|
347
|
+
dsh plugin add lume-dsh-plugin@0.7.0 # npm 指定版本
|
|
304
348
|
dsh plugin add lume-dsh-plugin@latest # npm 最新
|
|
305
349
|
dsh plugin add github:cayan0x/Lume # GitHub 最新 main
|
|
306
350
|
dsh plugin add github:cayan0x/Lume#v0.6.0 # GitHub 任意历史标签
|
|
@@ -322,7 +366,7 @@ npm run watch # 客户端 bundle 增量构建
|
|
|
322
366
|
```
|
|
323
367
|
src/index.ts 宿主入口:注入 + RPC + 工具 + 事件接线
|
|
324
368
|
src/core/ 纯逻辑:种子采样、检索打分、衰减、对话挖掘、manifest 解析、文本组装
|
|
325
|
-
src/host/
|
|
369
|
+
src/host/ 存储(选择/身份/项目)、蒸馏管线、提取器、工具、协议与文档能力、任务载具、行为触发器、RPC、注册表
|
|
326
370
|
src/client/ 前端:人设菜单、蒸馏弹窗、管理弹窗(插槽 conversation.input.left)
|
|
327
371
|
lib/ 构建产物(随仓库提交,GitHub 安装路径依赖它)
|
|
328
372
|
test/ vitest 单元测试 + storage 栈集成测试(含带数据重开域回归)
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 任务载具的纯逻辑层:任务契约、改动台账、假设台账、项目知识。
|
|
3
|
+
*
|
|
4
|
+
* 为什么是「载具」而不是再写协议条款:模型在长任务里丢的通常不是「不知道要量化」,
|
|
5
|
+
* 而是**没有一个地方放量化结果**。这四类结构化状态正好补上:
|
|
6
|
+
* - 由模型自己写(工具调用),所以与它的真实理解一致,而不是外部猜测;
|
|
7
|
+
* - 存在项目域里,跨轮次、跨压缩、跨会话存活(协议文本只能活在上下文里);
|
|
8
|
+
* - 每轮按状态渲染回尾部快照,让「原始判据」不会随进展漂移——这是可靠性最关键的
|
|
9
|
+
* 一环:交付时对照的必须是**开工时写下的判据**,而不是模型现在记的版本。
|
|
10
|
+
*
|
|
11
|
+
* 本模块只做纯逻辑(类型/解析/归一/渲染/上限),IO 在 host/project.ts。
|
|
12
|
+
*/
|
|
13
|
+
import { fnv1a32 } from "./sampling.js";
|
|
14
|
+
/** 契约字段长度上限:契约是「一屏能看完」的东西,写长了自己也不看。 */
|
|
15
|
+
export const CONTRACT_TEXT_CAP = 240;
|
|
16
|
+
export const CONTRACT_LIST_CAP = 8;
|
|
17
|
+
export const CONTRACT_ITEM_CAP = 120;
|
|
18
|
+
/** 台账条目上限:超了先挤掉「计划中」的旧条目,保留已改动过的(那是交付依据)。 */
|
|
19
|
+
export const CHANGE_CAP = 60;
|
|
20
|
+
export const CHANGE_TEXT_CAP = 160;
|
|
21
|
+
export const HYPOTHESIS_CAP = 20;
|
|
22
|
+
/** 项目知识上限:按时间挤旧,死路记录优先保留(它最省时间)。 */
|
|
23
|
+
export const PROJECT_FACT_CAP = 40;
|
|
24
|
+
export const FACT_TEXT_CAP = 200;
|
|
25
|
+
const FACT_LABEL = {
|
|
26
|
+
build: "构建",
|
|
27
|
+
test: "测试",
|
|
28
|
+
module: "模块链路",
|
|
29
|
+
convention: "约定",
|
|
30
|
+
deadend: "死路(不要重复)",
|
|
31
|
+
};
|
|
32
|
+
function clip(value, cap) {
|
|
33
|
+
return String(value ?? "").trim().replace(/\s+/g, " ").slice(0, cap);
|
|
34
|
+
}
|
|
35
|
+
function clipList(value, cap = CONTRACT_LIST_CAP) {
|
|
36
|
+
const list = Array.isArray(value) ? value : value === undefined || value === null ? [] : [value];
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const item of list) {
|
|
39
|
+
const text = clip(item, CONTRACT_ITEM_CAP);
|
|
40
|
+
if (text && !out.includes(text))
|
|
41
|
+
out.push(text);
|
|
42
|
+
if (out.length >= cap)
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function asCount(value) {
|
|
48
|
+
const n = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
|
|
49
|
+
return Number.isFinite(n) && n >= 0 ? Math.trunc(n) : null;
|
|
50
|
+
}
|
|
51
|
+
/** 项目键:跨会话共享的项目知识按工作目录归属(同一仓库的多个会话共用一份)。 */
|
|
52
|
+
export function projectKeyOf(cwd) {
|
|
53
|
+
const normalized = clip(cwd, 240).replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
54
|
+
if (!normalized)
|
|
55
|
+
return "unknown";
|
|
56
|
+
return fnv1a32(normalized).toString(16).padStart(8, "0");
|
|
57
|
+
}
|
|
58
|
+
/** 从工具入参归一化契约(截断 + 去重 + 上限)。 */
|
|
59
|
+
export function normalizeContract(input, at, turn) {
|
|
60
|
+
return {
|
|
61
|
+
goal: clip(input.goal, CONTRACT_TEXT_CAP),
|
|
62
|
+
scope: clipList(input.scope),
|
|
63
|
+
expectCount: asCount(input.expectCount),
|
|
64
|
+
actualCount: asCount(input.actualCount),
|
|
65
|
+
criteria: clipList(input.criteria),
|
|
66
|
+
nonGoals: clipList(input.nonGoals),
|
|
67
|
+
open: clipList(input.open, 4),
|
|
68
|
+
at,
|
|
69
|
+
turn,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export function normalizeChange(input, at) {
|
|
73
|
+
const target = clip(input.target, CONTRACT_ITEM_CAP);
|
|
74
|
+
const change = clip(input.change, CHANGE_TEXT_CAP);
|
|
75
|
+
if (!target || !change)
|
|
76
|
+
return null;
|
|
77
|
+
const status = input.status;
|
|
78
|
+
return {
|
|
79
|
+
target,
|
|
80
|
+
change,
|
|
81
|
+
why: clip(input.why, CHANGE_TEXT_CAP),
|
|
82
|
+
verify: clip(input.verify, CHANGE_TEXT_CAP),
|
|
83
|
+
status: status === "done" || status === "verified" || status === "skipped" ? status : "planned",
|
|
84
|
+
at,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function normalizeHypothesis(input, at) {
|
|
88
|
+
const text = clip(input.text, CHANGE_TEXT_CAP);
|
|
89
|
+
if (!text)
|
|
90
|
+
return null;
|
|
91
|
+
const status = input.status;
|
|
92
|
+
return {
|
|
93
|
+
text,
|
|
94
|
+
evidence: clip(input.evidence, CHANGE_TEXT_CAP),
|
|
95
|
+
status: status === "testing" || status === "confirmed" || status === "excluded" ? status : "open",
|
|
96
|
+
at,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export function normalizeProjectFact(input, at) {
|
|
100
|
+
const text = clip(input.text, FACT_TEXT_CAP);
|
|
101
|
+
if (!text)
|
|
102
|
+
return null;
|
|
103
|
+
const kind = input.kind;
|
|
104
|
+
return {
|
|
105
|
+
kind: kind === "test" || kind === "module" || kind === "convention" || kind === "deadend" ? kind : "build",
|
|
106
|
+
text,
|
|
107
|
+
at,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** 台账计数:渲染与触发器都要用("x 项已改未验" 是增量验证的判据)。 */
|
|
111
|
+
export function countByStatus(items) {
|
|
112
|
+
const out = { planned: 0, done: 0, verified: 0, skipped: 0 };
|
|
113
|
+
for (const item of items)
|
|
114
|
+
out[item.status]++;
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
/** 超限时挤掉最旧的「计划中」条目;已改动过的条目是交付依据,先保留。 */
|
|
118
|
+
export function trimChanges(items, cap = CHANGE_CAP) {
|
|
119
|
+
if (items.length <= cap)
|
|
120
|
+
return items;
|
|
121
|
+
const planned = items.filter((item) => item.status === "planned");
|
|
122
|
+
const rest = items.filter((item) => item.status !== "planned");
|
|
123
|
+
const keepPlanned = planned.slice(-Math.max(0, cap - rest.length));
|
|
124
|
+
return [...rest, ...keepPlanned].sort((a, b) => a.at - b.at).slice(-cap);
|
|
125
|
+
}
|
|
126
|
+
export function trimFacts(facts, cap = PROJECT_FACT_CAP) {
|
|
127
|
+
if (facts.length <= cap)
|
|
128
|
+
return facts;
|
|
129
|
+
const deadends = facts.filter((fact) => fact.kind === "deadend");
|
|
130
|
+
const rest = facts.filter((fact) => fact.kind !== "deadend");
|
|
131
|
+
const keepRest = rest.slice(-Math.max(0, cap - deadends.length));
|
|
132
|
+
return [...keepRest, ...deadends.slice(-cap)].sort((a, b) => a.at - b.at).slice(-cap);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 渲染契约。`delivery=true` 时切换成**对账口径**——这是防「判据漂移」的关键:
|
|
136
|
+
* 交付前看到的是开工时写下的原始判据,而不是模型此刻的记忆版本。
|
|
137
|
+
*/
|
|
138
|
+
export function renderContract(contract, delivery = false) {
|
|
139
|
+
if (!contract || !contract.goal)
|
|
140
|
+
return null;
|
|
141
|
+
const lines = [];
|
|
142
|
+
lines.push(delivery ? "〔契约对账〕交付前逐项对账(以下是开工时写下的原始判据,不是你现在的记忆版本):" : `〔任务契约|第 ${contract.turn} 轮写入〕`);
|
|
143
|
+
lines.push(`目标:${contract.goal}`);
|
|
144
|
+
if (contract.scope.length > 0)
|
|
145
|
+
lines.push(`范围:${contract.scope.join(";")}`);
|
|
146
|
+
if (contract.expectCount !== null || contract.actualCount !== null) {
|
|
147
|
+
const expect = contract.expectCount === null ? "?" : contract.expectCount;
|
|
148
|
+
const actual = contract.actualCount === null ? "未回填" : contract.actualCount;
|
|
149
|
+
lines.push(`数量:预计 ${expect} → 实际 ${actual}`);
|
|
150
|
+
}
|
|
151
|
+
if (contract.criteria.length > 0)
|
|
152
|
+
lines.push(`完成判据:${contract.criteria.map((item, i) => `${i + 1}. ${item}`).join(" ")}`);
|
|
153
|
+
if (contract.nonGoals.length > 0)
|
|
154
|
+
lines.push(`非目标(不动):${contract.nonGoals.join(";")}`);
|
|
155
|
+
if (contract.open.length > 0)
|
|
156
|
+
lines.push(`待确认:${contract.open.join(";")}`);
|
|
157
|
+
if (delivery) {
|
|
158
|
+
lines.push("逐项标注:已验证 / 未验证 / 偏离;数量对不上或判据没验的,直接说没做到,不要把动作完成说成判据达成。");
|
|
159
|
+
}
|
|
160
|
+
return lines.join("\n");
|
|
161
|
+
}
|
|
162
|
+
/** 渲染改动台账:计数在前(完整性可核对),明细在后(超长时只列未完成项)。 */
|
|
163
|
+
export function renderChangeLedger(items, limit = 12) {
|
|
164
|
+
if (items.length === 0)
|
|
165
|
+
return null;
|
|
166
|
+
const counts = countByStatus(items);
|
|
167
|
+
const head = `〔改动台账〕共 ${items.length} 项:已验证 ${counts.verified} / 已改未验 ${counts.done} / 计划中 ${counts.planned}${counts.skipped > 0 ? ` / 跳过 ${counts.skipped}` : ""}`;
|
|
168
|
+
const open = items.filter((item) => item.status !== "verified" && item.status !== "skipped");
|
|
169
|
+
const shown = (open.length > 0 ? open : items).slice(-limit);
|
|
170
|
+
const lines = shown.map((item) => {
|
|
171
|
+
const mark = item.status === "verified" ? "[已验证]" : item.status === "done" ? "[已改未验]" : item.status === "skipped" ? "[跳过]" : "[计划]";
|
|
172
|
+
const verify = item.verify ? `(验:${item.verify})` : "";
|
|
173
|
+
return `- ${mark} ${item.target} — ${item.change}${verify}`;
|
|
174
|
+
});
|
|
175
|
+
const foot = counts.planned > 0 || counts.done > 0 ? "\n台账里仍有未验证项:继续之前先补齐验证,或明确标注为未验证。" : "";
|
|
176
|
+
return `${head}\n${lines.join("\n")}${foot}`;
|
|
177
|
+
}
|
|
178
|
+
/** 渲染假设台账:已排除项照常显示——它们的作用就是「不要再试一遍」。 */
|
|
179
|
+
export function renderHypotheses(list, limit = 8) {
|
|
180
|
+
if (list.length === 0)
|
|
181
|
+
return null;
|
|
182
|
+
const lines = list.slice(-limit).map((item) => {
|
|
183
|
+
const mark = item.status === "excluded" ? "[已排除]" : item.status === "confirmed" ? "[已证实]" : item.status === "testing" ? "[验证中]" : "[待验证]";
|
|
184
|
+
const evidence = item.evidence ? `(证据:${item.evidence})` : "";
|
|
185
|
+
return `- ${mark} ${item.text}${evidence}`;
|
|
186
|
+
});
|
|
187
|
+
const excluded = list.filter((item) => item.status === "excluded").length;
|
|
188
|
+
const foot = excluded > 0 ? "\n已排除的假设不要重提;要推翻它必须给出新的证据。" : "";
|
|
189
|
+
return `〔假设台账〕\n${lines.join("\n")}${foot}`;
|
|
190
|
+
}
|
|
191
|
+
/** 渲染项目知识:按类别归组;死路单独成节(它最省时间)。 */
|
|
192
|
+
export function renderProjectFacts(facts, limit = 14) {
|
|
193
|
+
if (facts.length === 0)
|
|
194
|
+
return null;
|
|
195
|
+
const order = ["build", "test", "convention", "module", "deadend"];
|
|
196
|
+
const picked = facts.slice(-limit);
|
|
197
|
+
const lines = [];
|
|
198
|
+
for (const kind of order) {
|
|
199
|
+
const group = picked.filter((fact) => fact.kind === kind);
|
|
200
|
+
if (group.length === 0)
|
|
201
|
+
continue;
|
|
202
|
+
lines.push(`${FACT_LABEL[kind]}:`);
|
|
203
|
+
for (const fact of group)
|
|
204
|
+
lines.push(`- ${fact.text}`);
|
|
205
|
+
}
|
|
206
|
+
return `〔项目知识|本目录,跨会话累积〕\n${lines.join("\n")}`;
|
|
207
|
+
}
|
|
208
|
+
/** 台账/契约是否存在未验证项——触发器「连写不验」与交付对账都要用。 */
|
|
209
|
+
export function hasUnverified(items) {
|
|
210
|
+
return items.some((item) => item.status === "done" || item.status === "planned");
|
|
211
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const PLAN_TOKENS = new Set(["todo", "plan", "contract", "change", "ledger", "hypothesis", "note"]);
|
|
2
|
+
const LUME_TOKENS = new Set(["lume"]);
|
|
3
|
+
const VERIFY_TOKENS = new Set([
|
|
4
|
+
"bash", "shell", "pwsh", "powershell", "cmd", "terminal", "run", "exec", "job", "make", "mvn", "gradle",
|
|
5
|
+
"npm", "pnpm", "yarn", "bun", "deno", "node", "tsc", "tsdown", "vite", "vitest", "jest", "pytest", "cargo",
|
|
6
|
+
"go", "dotnet", "msbuild", "compile", "build", "test", "lint", "typecheck", "check", "verify",
|
|
7
|
+
]);
|
|
8
|
+
const MUTATE_TOKENS = new Set(["edit", "write", "multiedit", "patch", "apply", "replace", "create", "delete", "remove", "rename", "move", "append", "insert", "mkdir", "apply_patch"]);
|
|
9
|
+
const INSPECT_TOKENS = new Set(["read", "view", "cat", "grep", "search", "glob", "find", "ls", "list", "tree", "analyze", "symbol", "reference", "web", "fetch", "browser", "screenshot", "image", "git", "status", "diff", "log", "show", "stat", "head", "tail", "query", "sql", "map"]);
|
|
10
|
+
/** 把工具名切成小写词元:`lume_contract` → [lume, contract];`mcp__fs__read_file` → [mcp, fs, read, file]。 */
|
|
11
|
+
function tokens(name) {
|
|
12
|
+
return String(name ?? "")
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.split(/[^a-z0-9]+/)
|
|
15
|
+
.filter(Boolean);
|
|
16
|
+
}
|
|
17
|
+
export function classifyTool(name) {
|
|
18
|
+
const parts = tokens(name);
|
|
19
|
+
if (parts.length === 0)
|
|
20
|
+
return "other";
|
|
21
|
+
const has = (set) => parts.some((part) => set.has(part));
|
|
22
|
+
const isLume = has(LUME_TOKENS);
|
|
23
|
+
// lume 自家工具单独归类:载具(契约/台账/假设/项目知识)= plan;人格工具(记忆/风格/人设)
|
|
24
|
+
// = other——它们写的是人格数据,不该被算成「文件改动」,否则会污染增量验证的连击。
|
|
25
|
+
if (isLume)
|
|
26
|
+
return has(PLAN_TOKENS) ? "plan" : "other";
|
|
27
|
+
// todo_write 的 write 是「写清单」不是「改文件」,因此 plan 判定在 mutate 之前。
|
|
28
|
+
if (parts.includes("todo") || parts.includes("plan"))
|
|
29
|
+
return "plan";
|
|
30
|
+
if (has(VERIFY_TOKENS))
|
|
31
|
+
return "verify";
|
|
32
|
+
if (has(MUTATE_TOKENS))
|
|
33
|
+
return "mutate";
|
|
34
|
+
if (has(INSPECT_TOKENS))
|
|
35
|
+
return "inspect";
|
|
36
|
+
return "other";
|
|
37
|
+
}
|
|
38
|
+
/** 通用失败迹象:工具结果里出现这些词,就当这一步没成功。 */
|
|
39
|
+
const FAILURE_RE = /失败|报错|错误|异常|无法|找不到|不存在|没找到|\berror\b|\bfailed\b|\bfailure\b|\bexception\b|traceback|\bpanic\b|\bcannot\b|\bunable\b|permission denied|timed out|timeout|超时/i;
|
|
40
|
+
const UNKNOWN_RE = /结果未知|outcome unknown|tool_not_started|tool_outcome_unknown|仍在运行|still running|no output/i;
|
|
41
|
+
/**
|
|
42
|
+
* 环境故障迹象(区别于「代码写错了」):依赖解析不了、命令不存在、离线仓库、
|
|
43
|
+
* 网络/权限受阻。命中它才给「验证降级阶梯」——普通编译错误该归因到代码,
|
|
44
|
+
* 给环境阶梯反而会误导。
|
|
45
|
+
*/
|
|
46
|
+
const ENV_FAILURE_RE = /could not resolve dependencies|could not find artifact|cannot find module|module_not_found|command not found|not recognized as an internal|不是内部或外部命令|系统找不到指定的路径|no such file or directory|enoent|offline mode|cannot access .* in offline|本地仓库|repository.*(?:empty|missing)|network is unreachable|econnrefused|etimedout|proxy|self-signed certificate|eacces/i;
|
|
47
|
+
/** 从工具结果文本判定成败。`explicitError` 为宿主上报的错误字段。 */
|
|
48
|
+
export function readResultSignals(text, explicitError = false) {
|
|
49
|
+
const body = String(text ?? "");
|
|
50
|
+
const unknown = UNKNOWN_RE.test(body);
|
|
51
|
+
if (unknown)
|
|
52
|
+
return { failure: false, unknown: true, env: false };
|
|
53
|
+
// 环境故障本身就是失败:单独判定,避免「命令不存在」这类英文输出因通用失败词表
|
|
54
|
+
// 不含 "not found" 而被漏掉(实测 mvn: command not found 就踩过这个洞)。
|
|
55
|
+
const env = ENV_FAILURE_RE.test(body);
|
|
56
|
+
const failure = explicitError || env || FAILURE_RE.test(body);
|
|
57
|
+
return { failure, unknown: false, env: failure && env };
|
|
58
|
+
}
|
|
59
|
+
/** 连续失败序列的归类:环境故障占多数时才给降级阶梯。 */
|
|
60
|
+
export function deadPathKind(envHits, failStreak) {
|
|
61
|
+
if (failStreak < 3)
|
|
62
|
+
return null;
|
|
63
|
+
return envHits >= 2 ? "env" : "retry";
|
|
64
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 方法层:把「怎么做得聪明」写成可注入的短块。
|
|
3
|
+
*
|
|
4
|
+
* 与协议正文(thinking.ts)的分工:
|
|
5
|
+
* - 协议正文=**纪律**(不越权、要验证、要复核),会话恒定、吃前缀缓存、对所有任务生效;
|
|
6
|
+
* - 本模块=**方法**(怎么量化需求、怎么改文档、怎么评估影响面),只在对应形态的任务轮
|
|
7
|
+
* 出现——文档方法论不该出现在调试会话里,影响面清单也不该出现在闲聊里。
|
|
8
|
+
*
|
|
9
|
+
* 成本模型:这些都注入到尾部快照,因此只在**内容变化时**付费(实测 58 步只产生 9 条快照),
|
|
10
|
+
* 每轮多几百字是可接受的;真正要避免的是把它们塞进 system 段(那会作废整段前缀)。
|
|
11
|
+
*/
|
|
12
|
+
/** 任务型请求且尚无契约时注入:把「先量化」变成一次具体的产出。 */
|
|
13
|
+
export function buildContractMethodDirective() {
|
|
14
|
+
return [
|
|
15
|
+
"〔先量化后动手〕本轮是任务型请求。开工前先写任务契约(lume_contract):",
|
|
16
|
+
"- 目标:一句话,可观察的结果(不是「优化一下」这种动词)",
|
|
17
|
+
"- 范围:精确到路径 / 模块 / 章节 / 表",
|
|
18
|
+
"- 数量:先估一个数,探索后回填实际值——交付时要用实际数量对账",
|
|
19
|
+
"- 完成判据:可执行、可核对(命令 / 回读 / 对照),不是「改完」",
|
|
20
|
+
"- 非目标:明确不动什么,防止范围蔓延",
|
|
21
|
+
"- 待确认:只列真正阻塞的(≤2 个);不阻塞的按默认假设前进并写明假设",
|
|
22
|
+
].join("\n");
|
|
23
|
+
}
|
|
24
|
+
/** 文档任务轮注入:文档的失败模式是静默内容丢失,所以方法围绕「结构 + 最小编辑 + 回读」。 */
|
|
25
|
+
export function buildDocumentMethodDirective() {
|
|
26
|
+
return [
|
|
27
|
+
"〔文档编辑方法〕",
|
|
28
|
+
"1. 先取结构:标题层级、表格/图表清单、编号体系,复述一遍再动;长文档按大纲逐节记账(lume_change 的 target 用章节名),避免漏节或反复处理同一节。",
|
|
29
|
+
"2. 最小编辑:只改目标区域,保留原有格式、编号、交叉引用与样式——不要整份重写。",
|
|
30
|
+
"3. 术语与称谓全文一致:改一个术语前先全文检索它的全部出现位置,否则会留下半新半旧。",
|
|
31
|
+
"4. 交付前回读改动区域,列出「改了什么 / 没动什么 / 未核对什么」;没有回读证据不要说已改好。",
|
|
32
|
+
].join("\n");
|
|
33
|
+
}
|
|
34
|
+
/** 执行轮注入:改动之前的影响面清单,治「边写边想」。 */
|
|
35
|
+
export function buildImpactDirective() {
|
|
36
|
+
return [
|
|
37
|
+
"〔改动影响面〕动手前列出:要改的符号 → 谁调用它、它实现或被实现于谁、配置或 SQL 映射、前端/模板引用;并标出「不打算改但需一并确认」的位置。",
|
|
38
|
+
"每处改动写明验证方式;同一文件的相关改动一次做完,不要反复回来改同一个文件。",
|
|
39
|
+
].join("\n");
|
|
40
|
+
}
|
|
41
|
+
/** 环境里有结构分析/符号工具时的一句提示:用符号级定位替代通篇 read。 */
|
|
42
|
+
export function buildStructureHint(toolName) {
|
|
43
|
+
if (!toolName)
|
|
44
|
+
return null;
|
|
45
|
+
return `〔定位工具〕当前环境有结构分析工具(${toolName}):优先用它做符号级定位(谁调用、被谁调用、结构概览),比通篇 read 更省 token 也更准。`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 拼装尾部快照块:超预算时优先丢掉**可丢**的块(从后往前),而不是截断中间的句子。
|
|
49
|
+
* 预算存在的意义是防止"载具越积越多,把注意力挤没"——实测尾部快照约 1.8-2k 字符时
|
|
50
|
+
* 命中率与合规都健康,这里给到 4200 字符仍有充足余量。
|
|
51
|
+
*/
|
|
52
|
+
export function composeBlocks(blocks, budgetChars = 4200) {
|
|
53
|
+
const present = blocks.filter((block) => Boolean(block.text));
|
|
54
|
+
let out = present.map((block) => block.text).join("\n\n");
|
|
55
|
+
if (out.length <= budgetChars)
|
|
56
|
+
return out;
|
|
57
|
+
for (let i = present.length - 1; i >= 0 && out.length > budgetChars; i--) {
|
|
58
|
+
if (!present[i].droppable)
|
|
59
|
+
continue;
|
|
60
|
+
present.splice(i, 1);
|
|
61
|
+
out = present.map((block) => block.text).join("\n\n");
|
|
62
|
+
}
|
|
63
|
+
return out.length > budgetChars ? out.slice(0, budgetChars) : out;
|
|
64
|
+
}
|