@fanchao8609/agent_brain_sync 1.16.1 → 1.16.2
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 +1 -1
- package/bin/abs.js +6 -9
- package/bin/mcp.js +7 -7
- package/hooks/abs.pi.ts +108 -5
- package/package.json +1 -1
- package/src/lint.js +5 -3
- package/src/store.js +13 -3
- package/src/todo.js +7 -2
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ abs update # 升级到最新版并刷新四宿主
|
|
|
125
125
|
```
|
|
126
126
|
|
|
127
127
|
> `abs todo start` 与 `abs todo add` 等价(都登记任务)。
|
|
128
|
-
> 旧版 `abs task ...` / `abs board`
|
|
128
|
+
> 旧版 `abs task ...` / `abs board` 已废,打它们会报「未知命令」并退出码 1。
|
|
129
129
|
> `abs wrapup` / `abs teardown-check` 是 hook 内部命令,无需手动调用。
|
|
130
130
|
> **作者名要填真的**:`tester` / `foo` / `aaa` 这类占位名会被拒 —— 因为 `{user}` 是全局单值,
|
|
131
131
|
> 填错会污染之后所有项目的 `[[作者]]` 标记(`aaa` 这类堆字也拒,但 `oo`/`ee` 这种两字母缩写放行)。
|
package/bin/abs.js
CHANGED
|
@@ -279,10 +279,10 @@ const subUsage = {
|
|
|
279
279
|
'',
|
|
280
280
|
'用法:',
|
|
281
281
|
' abs todo 看板',
|
|
282
|
-
' abs todo add <id> [--note "做什么"] [--section
|
|
282
|
+
' abs todo add <id> [--note "做什么"] [--section 讨论中|滞留中|搁置]',
|
|
283
283
|
' abs todo rename <旧id> --note "<新id>" 改任务 id(起错名/不可读时)',
|
|
284
284
|
' abs todo note <id> --note "断点/进度"',
|
|
285
|
-
' abs todo state <id> --note
|
|
285
|
+
' abs todo state <id> --note 进行中|讨论中|滞留中|搁置',
|
|
286
286
|
' abs todo done <id> [--as 落地|否决|仅方案] [结语文字]',
|
|
287
287
|
' abs todo archive [--keep-days N] [--dry-run]',
|
|
288
288
|
'',
|
|
@@ -336,18 +336,16 @@ function rejectExtra(extra, fix) {
|
|
|
336
336
|
throw new Error(`✗ 不认识多余参数 ${q}\n 正确用法: ${fix}`);
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
board: () => '命令已改名: abs board → abs todo',
|
|
343
|
-
};
|
|
339
|
+
// 已废弃命令 → 报错(而非静默)。曾用 RENAMED 表做"已改名"提示,
|
|
340
|
+
// 2026-10-05 删掉 task/board 两条:未知命令分支本来就报错+退出码 1+打印帮助,
|
|
341
|
+
// 别名不提供额外信息,只多一处要维护的文案。
|
|
344
342
|
|
|
345
343
|
// task/todo 共用的子命令 → 归一化后的 action
|
|
346
344
|
const TODO_ACTIONS = {
|
|
347
345
|
add: 'start',
|
|
348
346
|
start: 'start', // add 的别名(老习惯保留)
|
|
349
347
|
note: 'note',
|
|
350
|
-
state: 'state', //
|
|
348
|
+
state: 'state', // 改行首状态标记:进行中|讨论中|滞留中|搁置(原地,不搬区)
|
|
351
349
|
rename: 'rename', // 改任务 id:人工起错名、或旧 id 不可读时用
|
|
352
350
|
done: 'done',
|
|
353
351
|
};
|
|
@@ -387,7 +385,6 @@ async function cmdConfig({ sub, value }) {
|
|
|
387
385
|
async function main() {
|
|
388
386
|
try {
|
|
389
387
|
const opts = parseArgv(rest);
|
|
390
|
-
if (RENAMED[cmd]) throw new Error(RENAMED[cmd]());
|
|
391
388
|
// 子命令级 --help: 有用法页的命令在此一处拦截(此前只在 install/uninstall 分支里判,
|
|
392
389
|
// 其余命令的 --help 被忽略 —— 曾出现子命令 --help 直接执行动作、挂住终端)。
|
|
393
390
|
if (opts.help && subUsage[cmd] && !(cmd === 'todo' && opts._.length)) {
|
package/bin/mcp.js
CHANGED
|
@@ -112,12 +112,12 @@ tool(
|
|
|
112
112
|
|
|
113
113
|
tool(
|
|
114
114
|
'abs_task',
|
|
115
|
-
'任务实时落盘(幂等键 = id)。start 登记进 Todo / note 补断点(改到哪文件哪行) / state 改状态 / done 完成归位 Done(done 时 note=结语文字,as
|
|
115
|
+
'任务实时落盘(幂等键 = id)。start 登记进 Todo / note 补断点(改到哪文件哪行) / state 改状态 / done 完成归位 Done(done 时 note=结语文字,as=结语类型:落地|否决|仅方案)。必填: cwd, action, id。',
|
|
116
116
|
{
|
|
117
117
|
action: z.enum(['add', 'start', 'done', 'note', 'state']),
|
|
118
118
|
id: z.string().describe('任务幂等键,如 TASK-xxx 或子任务名'),
|
|
119
119
|
cwd: z.string().describe('项目根目录(.brain/ 所在处)'),
|
|
120
|
-
note: z.string().optional().describe('add/start=做什么; note=断点(文件/到哪步); state
|
|
120
|
+
note: z.string().optional().describe('add/start=做什么; note=断点(文件/到哪步); state=进行中|讨论中|滞留中|搁置(搁置=用户改方向/不做了;滞留中=还要做只是卡住); done=结语文字'),
|
|
121
121
|
as: z.enum(['落地', '否决', '仅方案']).optional().describe('仅 done:结语类型。默认 落地。做了又撤/评估后不做用 否决,只设计过用 仅方案 —— 别让假【落地】污染看板'),
|
|
122
122
|
},
|
|
123
123
|
async ({ action, id, cwd, note, as }) => {
|
|
@@ -131,7 +131,7 @@ tool(
|
|
|
131
131
|
|
|
132
132
|
tool(
|
|
133
133
|
'abs_resolve',
|
|
134
|
-
'按页面 id(或页面名 slug)反查文件路径。页改名后 id 不变,引用请用返回的路径/id
|
|
134
|
+
'按页面 id(或页面名 slug)反查文件路径。页改名后 id 不变,引用请用返回的路径/id。必填: cwd, refs(数组,一次可查多个)。',
|
|
135
135
|
{
|
|
136
136
|
cwd: z.string().describe('项目根目录(.brain/ 所在处)'),
|
|
137
137
|
refs: z.array(z.string()).min(1).describe('页面 id 或页面名,如 "file-write-locking"'),
|
|
@@ -151,7 +151,7 @@ tool(
|
|
|
151
151
|
|
|
152
152
|
tool(
|
|
153
153
|
'abs_supersede',
|
|
154
|
-
'标记一条经验/知识页已失效(被推翻),不删文件、保留历史。之后 abs_query 默认不再返回它。核实后发现仍有效可手动把 status 改回 active
|
|
154
|
+
'标记一条经验/知识页已失效(被推翻),不删文件、保留历史。之后 abs_query 默认不再返回它。核实后发现仍有效可手动把 status 改回 active。必填: cwd, refs(数组)。',
|
|
155
155
|
{
|
|
156
156
|
cwd: z.string().describe('项目根目录(.brain/ 所在处)'),
|
|
157
157
|
refs: z.array(z.string()).min(1).describe('页名或 id,如 "old-approach"'),
|
|
@@ -181,7 +181,7 @@ tool(
|
|
|
181
181
|
|
|
182
182
|
tool(
|
|
183
183
|
'abs_query',
|
|
184
|
-
'检索当前项目 .brain/ 知识页(多词 OR):以前踩过什么坑、哪页记了 X
|
|
184
|
+
'检索当前项目 .brain/ 知识页(多词 OR):以前踩过什么坑、哪页记了 X。必填: cwd, terms(数组,不是 query;可传多个词)。',
|
|
185
185
|
{ cwd: z.string().describe('项目根目录(.brain/ 所在处)'), terms: z.array(z.string()).min(1).describe('检索词'), include_superseded: z.boolean().optional().describe('true=连已标记失效的经验一起返回(默认隐藏)') },
|
|
186
186
|
async ({ cwd, terms, include_superseded }) => {
|
|
187
187
|
const root = await findBrainRoot(cwd || process.cwd());
|
|
@@ -233,7 +233,7 @@ tool(
|
|
|
233
233
|
|
|
234
234
|
tool(
|
|
235
235
|
'abs_note',
|
|
236
|
-
'经验实时暂存:把刚踩的坑/技巧/结论一句话落进 sources/(防 context 断了流失)。Teardown 时再提炼进 concepts
|
|
236
|
+
'经验实时暂存:把刚踩的坑/技巧/结论一句话落进 sources/(防 context 断了流失)。Teardown 时再提炼进 concepts/。必填: cwd, text。',
|
|
237
237
|
{
|
|
238
238
|
cwd: z.string().describe('项目根目录(.brain/ 所在处)'),
|
|
239
239
|
text: z.string().min(1).describe('经验/坑/技巧一句话'),
|
|
@@ -248,7 +248,7 @@ tool(
|
|
|
248
248
|
|
|
249
249
|
tool(
|
|
250
250
|
'abs_concept',
|
|
251
|
-
'建概念页骨架(给写入定结构:触发场景/表现/解法/验证)。只给结构不给内容 ——
|
|
251
|
+
'建概念页骨架(给写入定结构:触发场景/表现/解法/验证)。只给结构不给内容 —— 值不值得留、归哪页仍靠人判断。必填: cwd, slug。',
|
|
252
252
|
{
|
|
253
253
|
cwd: z.string().describe('项目根目录(.brain/ 所在处)'),
|
|
254
254
|
slug: z.string().min(1).describe('文件名/slug,如 "docker-prisma-429"(命名即链接)'),
|
package/hooks/abs.pi.ts
CHANGED
|
@@ -99,6 +99,31 @@ let pendingTodoTool = false
|
|
|
99
99
|
* 只记第一次写文件,不是每次都记:断点是「记到哪」不是流水账。 */
|
|
100
100
|
let autoTaskSeen = false
|
|
101
101
|
|
|
102
|
+
/** 本会话动过的文件(去重,最多记 FILES_SEEN_MAX 个)。
|
|
103
|
+
*
|
|
104
|
+
* ★ 定位(用户 2026-10-05 定,别改):这**是「摆事实」,不是「发现问题」**。
|
|
105
|
+
* 它只把我自己刚干过的事(改过哪些文件)与看板并列摆出来,
|
|
106
|
+
* **不判断这些改动算不算任务、不提醒、不追踪、不逼登记**。
|
|
107
|
+
* 是否意识到「我干的和看板对不上」、要不要补记 —— 全部由 LLM 自己反思。
|
|
108
|
+
*
|
|
109
|
+
* 为何不挂在「改文件/一轮结束」两个时机:那两个钩子改不了 system prompt
|
|
110
|
+
* (tool_call/agent_end 拿不到 systemPromptOptions,只有 before_agent_start 有);
|
|
111
|
+
* 而 sendUserMessage 插话已被否决(打断用户)。故记在这里、下轮带出。
|
|
112
|
+
*
|
|
113
|
+
* 今日最大的教训:**不要用技术手段替 LLM 发现 todo 问题。**
|
|
114
|
+
* 一侧(摆事实)有效,另一侧(替它发现)十二次全败。详见 concept remind-vs-gate。 */
|
|
115
|
+
const filesSeen = new Set<string>()
|
|
116
|
+
const FILES_SEEN_MAX = 8
|
|
117
|
+
|
|
118
|
+
/** 写类工具调用的文件路径:edit/write 直接取 input.path,bash 取命令里像路径的片段。 */
|
|
119
|
+
function touchedPath(name: string, input: any): string | null {
|
|
120
|
+
const p = input?.path ?? input?.file_path ?? input?.filename
|
|
121
|
+
if (typeof p === 'string' && p.trim()) return p.trim()
|
|
122
|
+
// bash 的写入靠命令字符串抻出路径(重定向/ sed -i / tee 等)
|
|
123
|
+
const cmd = String(input?.command ?? '')
|
|
124
|
+
const m = cmd.match(/(?:>>?|tee|sed -i[^ ]*)\s+([^\s|;&<>]+)/)
|
|
125
|
+
return m ? m[1] : null
|
|
126
|
+
}
|
|
102
127
|
|
|
103
128
|
/** 最近一次渲染拿到的 tui 引用。hidePanel 用它请求重绘 —— 面板消失时动画
|
|
104
129
|
* 定时器已停,没有别的重绘通道(见 hidePanel 的坑注解)。 */
|
|
@@ -149,8 +174,8 @@ const TODO_GUIDELINES = [
|
|
|
149
174
|
// ④ 反例(照 rpiv-todo):明确什么情况【不许】标完成 —— 只给正向要求时,
|
|
150
175
|
// 模型倾向于把"我以为做完了"当成完成,结语失真会污染下个会话的判断。
|
|
151
176
|
'Never mark a task "done" when tests are failing, the implementation is partial, or an error is unresolved — keep it in progress and add a task for the blocker instead.',
|
|
152
|
-
// ⑤ 唯一进行中(同 rpiv-todo):看板要能回答"现在在做什么"
|
|
153
|
-
'Keep exactly one task in "进行中" at a time; use action "state" with
|
|
177
|
+
// ⑤ 唯一进行中(同 rpiv-todo):看板要能回答"现在在做什么",多的应转走。
|
|
178
|
+
'Keep exactly one task in "进行中" at a time; when the user switches direction or drops a task, use action "state" with 搁置 (dropped/not doing it); 滞留中 for still-wanted-but-stuck; 讨论中 for still-open. If you finish one and another is ready, promote it explicitly.',
|
|
154
179
|
// ⑥ 结语要真实:三种结语各有含义,别一律写落地。
|
|
155
180
|
'When completing, pick the honest conclusion via the `as` field: 落地 (built and verified) / 否决 (decided against, or built then reverted) / 仅方案 (designed only). A wrong conclusion makes the next session treat "considered" as "completed".',
|
|
156
181
|
]
|
|
@@ -173,12 +198,83 @@ async function hasBrain(cwd: string): Promise<boolean> {
|
|
|
173
198
|
}
|
|
174
199
|
}
|
|
175
200
|
|
|
176
|
-
|
|
201
|
+
/** 看板快照在 guidelines 里的前缀 —— 用于每轮替换上轮快照(内容会变,不能用 includes 去重)。 */
|
|
202
|
+
const BOARD_MARK = '[看板] '
|
|
203
|
+
|
|
204
|
+
/** 把当前看板拼成 Guidelines 里的一段(对齐用)。
|
|
205
|
+
*
|
|
206
|
+
* 设计(用户 2026-10-05 定):**只对齐,不多说** —— 把「现在看板上有什么」摆在
|
|
207
|
+
* 面前让 LLM 自己对照,不写「记得登记」这类劝告(那些已证明会被忽略)。
|
|
208
|
+
*
|
|
209
|
+
* ★ 铁律(2026-10-05 用户定,别改):
|
|
210
|
+
* - **只用对齐,不用技术手段替 LLM 发现 todo 问题。**
|
|
211
|
+
* 今天最大的收获:技术只负责「摆事实」,不负责「发现问题、提醒、追踪、逼补记」。
|
|
212
|
+
* 让 LLM 自己看到、自己反思、自己决定 —— 这是唯一活下来的方向。
|
|
213
|
+
* - **反复对齐,不厌其烦**:不是一次性提示,是**每一轮都摆**。
|
|
214
|
+
* 用户原话:「对齐永远是反反复复的对齐,不厌其烦」。
|
|
215
|
+
* 别用节流/去重/「刚说过就不重复」去优化掉它 —— 反复就是它的工作方式。
|
|
216
|
+
* - **尺度:只说 todo.md 本身**。不夹带判断、不写劝告、不评价好坏。
|
|
217
|
+
* 只把「看板上现在有什么」摆出来。
|
|
218
|
+
* - **是否添加/完成/删除/转状态,全部由 LLM 自己判断** —— 不替它决定,
|
|
219
|
+
* 不自动登记、不自动完成、不写看板。
|
|
220
|
+
* - **不做重任务**:不起进程、不 spawn、不写盘;只读一个本地文件。
|
|
221
|
+
* 理由:前 12 次尝试都失败,根因是「让不掌握判据的一方启动动作」。
|
|
222
|
+
* 判据在 LLM 手里,这里只负责保证它**每轮都看得见**。
|
|
223
|
+
* 详见 concept remind-vs-gate(对齐必做 + 门禁兜底)。
|
|
224
|
+
*
|
|
225
|
+
* 与以前删掉的两种注入的本质区别:那不调 sendUserMessage(不插话、不抢 turn),
|
|
226
|
+
* 只改 system prompt 内容 —— 和静默的 TODO_GUIDELINES 走同一个通道。
|
|
227
|
+
*
|
|
228
|
+
* 看板空或全完成时不拼(没有可对齐的东西,拼个空列表纯占位)。 */
|
|
229
|
+
export function boardGuideline(md: string, who: string, touched: string[] = []): string | null {
|
|
230
|
+
const { total, rows } = parseOpenTasks(md, PANEL_MAX_ROWS, who)
|
|
231
|
+
// 看板空但动过文件:仍要输出(「改了文件却没任务」正是最该对齐的时刻)。
|
|
232
|
+
if ((!total || !rows.length) && !touched.length) return null
|
|
233
|
+
const lines = rows.map((r) => {
|
|
234
|
+
// desc 为空时不拖空破折号(MCP 登记只给 id 的常见情形)。
|
|
235
|
+
const head = ` ${r.state ? `[${r.state}] ` : ''}${r.id}${r.desc ? ` — ${r.desc}` : ''}`
|
|
236
|
+
// 断点必须带上:它就是「改到哪一步」,丢了等于对齐了也接不上。
|
|
237
|
+
return r.note ? `${head}\n ↳ ${r.note}` : head
|
|
238
|
+
})
|
|
239
|
+
const more = total > rows.length ? ` …另 ${total - rows.length} 条` : ''
|
|
240
|
+
// 本会话动过的文件 —— 与看板并列摆出,让 LLM 自己对照(不判断算不算任务)。
|
|
241
|
+
const touchLines = touched.length
|
|
242
|
+
? ['本会话改过:', ...touched.map((f) => ` ${f}`)]
|
|
243
|
+
: []
|
|
244
|
+
return [
|
|
245
|
+
'当前 .brain/todo.md 看板(开工前对齐:正在做的算哪条?有新任务要加吗?有该删的吗?):',
|
|
246
|
+
...(lines.length ? lines : [' (空)']),
|
|
247
|
+
...(more ? [more] : []),
|
|
248
|
+
...touchLines,
|
|
249
|
+
].join('\n')
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** 读看板拼成对齐段。失败/无图谱/看板空都返 null(静默降级,绝不阻断对话)。 */
|
|
253
|
+
async function readBoardForGuide(cwd: string): Promise<string | null> {
|
|
254
|
+
try {
|
|
255
|
+
const md = await readFile(join(cwd, '.brain', 'todo.md'), 'utf8')
|
|
256
|
+
return boardGuideline(md, await currentUser(), [...filesSeen])
|
|
257
|
+
} catch {
|
|
258
|
+
return null
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** 注入 todo 指引。静态 6 条(行为要求) + 实时看板(对齐用)—— 两者职责不同:
|
|
263
|
+
* 静态条说「该怎么做」,看板说「现在是什么」。 */
|
|
264
|
+
export function injectTodoGuidelines(options: any, board?: string | null): boolean {
|
|
177
265
|
if (String(process.env.ABS_TODO_GUIDE || '') === '0') return false
|
|
178
266
|
const list: string[] = options.promptGuidelines || (options.promptGuidelines = [])
|
|
179
267
|
for (const g of TODO_GUIDELINES) {
|
|
180
268
|
if (!list.includes(g)) list.push(g)
|
|
181
269
|
}
|
|
270
|
+
// 看板内容每轮会变(任务增减/状态转换),不能用 includes 去重 —— 先删上轮的旧快照再推。
|
|
271
|
+
// ★ 必须先删:board 为 null(看板空了/全完成)时同样要删 ——
|
|
272
|
+
// 曾只在 board 非空时删,结果看板清空后旧快照残留在 prompt 里,
|
|
273
|
+
// LLM 会继续看到已经不存在的任务(对齐撒谎)。
|
|
274
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
275
|
+
if (list[i].startsWith(BOARD_MARK)) list.splice(i, 1)
|
|
276
|
+
}
|
|
277
|
+
if (board) list.push(BOARD_MARK + board)
|
|
182
278
|
return true
|
|
183
279
|
}
|
|
184
280
|
|
|
@@ -518,7 +614,7 @@ export function renderPanelLines(
|
|
|
518
614
|
nickname = '',
|
|
519
615
|
): string[] {
|
|
520
616
|
if (data.total === 0) return []
|
|
521
|
-
const colorOf = (state: string): string => (state === '滞留中' ? 'muted' : state === '讨论中' ? 'dim' : 'accent')
|
|
617
|
+
const colorOf = (state: string): string => (state === '搁置' ? 'dim' : state === '滞留中' ? 'muted' : state === '讨论中' ? 'dim' : 'accent')
|
|
522
618
|
const lines: string[] = []
|
|
523
619
|
// 上描边:用 `thinkingOff` —— 跟 Pi 输入框描边**完全同一个色**。
|
|
524
620
|
// 排查过程(2026-10-03 用户两轮反馈):
|
|
@@ -620,6 +716,7 @@ export default function absPiHook(pi: ExtensionAPI): void {
|
|
|
620
716
|
pi.on("session_start", (event: any, ctx: any) => {
|
|
621
717
|
// 新会话 = 重新登记:不重置则同进程的第二个会话永远不再落断点
|
|
622
718
|
autoTaskSeen = false
|
|
719
|
+
filesSeen.clear() // 同理:新会话的文件痕迹不能带到下个会话
|
|
623
720
|
resetThrottle()
|
|
624
721
|
// 本会话的随机昵称 —— 在这里抽一次(不是每帧抽,否则面板会疯狂闪)。
|
|
625
722
|
// 每次 session_start(startup/reload/new/resume/fork)重抽 → “每次打开 pi 都是随机的”。
|
|
@@ -663,6 +760,12 @@ export default function absPiHook(pi: ExtensionAPI): void {
|
|
|
663
760
|
const sid = sessionIdFor(ctx)
|
|
664
761
|
logHook(`tool_call first_write name=${name} sid=${sid || '-'}`).catch(() => {})
|
|
665
762
|
}
|
|
763
|
+
// 记下动过的文件(去重)—— 下一轮对齐时摆给 LLM(见 filesSeen 注释)。
|
|
764
|
+
// 每轮都记(不限首次):一轮里改多个文件都要能看到。
|
|
765
|
+
if (isWriteCall(name, input)) {
|
|
766
|
+
const p = touchedPath(name, input)
|
|
767
|
+
if (p && filesSeen.size < FILES_SEEN_MAX) filesSeen.add(p)
|
|
768
|
+
}
|
|
666
769
|
})
|
|
667
770
|
|
|
668
771
|
pi.on("tool_execution_end", (_event: any, ctx: any) => {
|
|
@@ -690,7 +793,7 @@ export default function absPiHook(pi: ExtensionAPI): void {
|
|
|
690
793
|
logHook(`before_agent_start todo_guide=off reason=no_brain`).catch(() => {})
|
|
691
794
|
return
|
|
692
795
|
}
|
|
693
|
-
const ok = injectTodoGuidelines(event?.systemPromptOptions)
|
|
796
|
+
const ok = injectTodoGuidelines(event?.systemPromptOptions, await readBoardForGuide(cwd))
|
|
694
797
|
logHook(`before_agent_start todo_guide=${ok ? 'on' : 'off'} cwd=${cwd}`).catch(() => {})
|
|
695
798
|
|
|
696
799
|
// 自动登记已整体移除(2026-10-05 用户实报两点):
|
package/package.json
CHANGED
package/src/lint.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { promises as fs } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { requireBrain, brainPath, BRAIN_DIR } from './index.js';
|
|
6
|
-
import { readRules, idOfPage, statusOfPage, supersededByOf, clip } from './store.js';
|
|
6
|
+
import { readRules, idOfPage, statusOfPage, supersededByOf, clip, RULE_BODY_MAX } from './store.js';
|
|
7
7
|
import { doneKindOf, doneDateOf, checkFileShape, TODO_MAX_LINES, LOG_MAX_LINES, INDEX_MAX_LINES } from './todo.js';
|
|
8
8
|
|
|
9
9
|
// ---------- 图谱遍历 ----------
|
|
@@ -342,9 +342,11 @@ async function checkFiles(vault, pages, indexLinks) {
|
|
|
342
342
|
if (found && items.length > LINT_RULES_MAX) {
|
|
343
343
|
issues.push(`RULES-PILED-UP: Rules 区 ${items.length} 条 > ${LINT_RULES_MAX};把长条目提炼成概念页,这里只留一句话`);
|
|
344
344
|
}
|
|
345
|
-
|
|
345
|
+
// 单条长度:与 cmdRule 用同一个阈值(RULE_BODY_MAX)—— 写入侧和体检侧必须同标准,
|
|
346
|
+
// 否则「手工编辑绕过写入校验」的条目静默通过,而 lint 报 0 问题(假健康)。
|
|
347
|
+
const longOnes = items.filter((l) => l.trim().length > RULE_BODY_MAX);
|
|
346
348
|
if (longOnes.length) {
|
|
347
|
-
issues.push(`RULES-TOO-LONG: Rules 区 ${longOnes.length} 条超
|
|
349
|
+
issues.push(`RULES-TOO-LONG: Rules 区 ${longOnes.length} 条超 ${RULE_BODY_MAX} 字(如 "${clip(longOnes[0].trim(), 40)}",${longOnes[0].trim().length} 字);展开写进概念页,这里只留一句话`);
|
|
348
350
|
}
|
|
349
351
|
}
|
|
350
352
|
return issues;
|
package/src/store.js
CHANGED
|
@@ -523,6 +523,10 @@ async function sourcesCountSection(root) {
|
|
|
523
523
|
/** index.md 的 `## Rules` 区名与上限。 */
|
|
524
524
|
export const RULES_HEADING = '## Rules';
|
|
525
525
|
export const RULES_MAX = 30; // 超过就 lint 报:它属于“被读到才有价值”的区,不能无界增长
|
|
526
|
+
/** 单条上限(中文按字算)。2026-10-05 从 cmdRule 内联数字抽出:lint 也要查同一个值 ——
|
|
527
|
+
* 曾经只在「abs rule add」写入时校验,手工编辑 index.md 完全绕过,而 lint 只查条数不查长度
|
|
528
|
+
* → 89 字的条目静默通过,同一内容用 add 写反而被拒(两个口子标准不一致)。 */
|
|
529
|
+
export const RULE_BODY_MAX = 42;
|
|
526
530
|
|
|
527
531
|
|
|
528
532
|
/** 提取 index.md 里的 Rules 区条目(不含标题)。返回 { items:[行], body, found }。 */
|
|
@@ -575,8 +579,8 @@ export async function cmdRule({ dir, action, text }) {
|
|
|
575
579
|
// 门槛:**纪律不是记事本**。Rules 是最前面的项目铁律,每条必须一眼扫完。
|
|
576
580
|
// 2026-09-13 用户定:不带链接、不带解释、不写细节 —— 细节进概念页,Rules 只留结论。
|
|
577
581
|
// 宽严:中文一字信息量大,按 40 字算(≈ 英文 80 字符的量)。实测现有 12 条最长 56 字符。
|
|
578
|
-
if (clean.length >
|
|
579
|
-
return `✗ 太长(${clean.length} >
|
|
582
|
+
if (clean.length > RULE_BODY_MAX) {
|
|
583
|
+
return `✗ 太长(${clean.length} > ${RULE_BODY_MAX})—— Rules 是纪律不是记事本:\n`
|
|
580
584
|
+ ` 每条要一眼扫完。细节/出处/例子 → 写进 concepts/ 概念页,这里只留一句话结论。`;
|
|
581
585
|
}
|
|
582
586
|
if (/\[\[|\]\]|https?:\/\//.test(clean)) {
|
|
@@ -855,9 +859,15 @@ export async function cmdTask({ dir, action, id, section, note, as }) {
|
|
|
855
859
|
return SKIP;
|
|
856
860
|
});
|
|
857
861
|
if (others.length) {
|
|
862
|
+
// 提示要具体到「该转成哪个」:只报「已有 N 个进行中」实测不够 ——
|
|
863
|
+
// 用户改方向时,旧任务是「搁置」(不做了),与「滞留中」(还要做)不同,
|
|
864
|
+
// 选错会让下会话分不清该不该接着干。
|
|
858
865
|
warning = `\n ⚠ 已有 ${others.length} 个 [进行中]: ${others.join(', ')}\n`
|
|
859
866
|
+ ' 同时只有一个进行中才说得清"现在在做什么"。\n'
|
|
860
|
-
+
|
|
867
|
+
+ ` 旧任务怎么处理 ——\n`
|
|
868
|
+
+ ` • 用户改方向了/不做了 → abs todo state <id> --note 搁置\n`
|
|
869
|
+
+ ` • 还要做,只是卡住了 → abs todo state <id> --note 滞留中\n`
|
|
870
|
+
+ ` • 还想商量 → abs todo state <id> --note 讨论中`;
|
|
861
871
|
}
|
|
862
872
|
}
|
|
863
873
|
const r = await upsertTask(root, {
|
package/src/todo.js
CHANGED
|
@@ -340,8 +340,13 @@ function indexLinkSectionNeeds(section) {
|
|
|
340
340
|
}
|
|
341
341
|
|
|
342
342
|
/** 任务状态标记(行首,方括号)。替代原 Backlog/Today/Blocked 三区的区分作用。
|
|
343
|
-
* 放在 id **之前**,与 Done 结语的 `【落地】` 形态区分开(那是行尾、结语用)。
|
|
344
|
-
|
|
343
|
+
* 放在 id **之前**,与 Done 结语的 `【落地】` 形态区分开(那是行尾、结语用)。
|
|
344
|
+
*
|
|
345
|
+
* 「搁置」与「滞留中」的区别(2026-10-05 用户定,别合并):
|
|
346
|
+
* 滞留中 = 还要做,只是卡住了(等外部/等信息) → 下会话该捡起来
|
|
347
|
+
* 搁置 = 不做了 / 用户改方向了 → 下会话不该捡,除非用户又提
|
|
348
|
+
* 混在一起会让下会话分不清「该不该接着干」。 */
|
|
349
|
+
export const TASK_STATES = ['进行中', '讨论中', '滞留中', '搁置'];
|
|
345
350
|
|
|
346
351
|
/** 结构重建(B 档):以标准分区表为准重排整个文件。
|
|
347
352
|
*
|