@fanchao8609/agent_brain_sync 1.7.0 → 1.7.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/bin/abs.js +33 -7
- package/package.json +1 -1
- package/skill/SKILL.md +8 -0
- package/src/store.js +52 -7
- package/src/todo.js +50 -2
package/bin/abs.js
CHANGED
|
@@ -76,12 +76,16 @@ const FLAG_SPEC = {
|
|
|
76
76
|
'keep-days': { type: 'string' },
|
|
77
77
|
'help': { type: 'boolean' },
|
|
78
78
|
'dry-run': { type: 'boolean' },
|
|
79
|
+
'full': { type: 'boolean' },
|
|
79
80
|
'yes': { type: 'boolean' },
|
|
80
81
|
'repair': { type: 'boolean' },
|
|
81
82
|
'no-mcp': { type: 'boolean' },
|
|
82
83
|
'no-skill': { type: 'boolean' },
|
|
83
84
|
};
|
|
84
85
|
|
|
86
|
+
/** 已被"规范键"接管的 raw flag:不再原样漏出(见 parseArgv 返回处的白名单注释)。 */
|
|
87
|
+
const KNOWN_RAW = new Set(['keep-days', 'dry-run', 'no-mcp', 'no-skill']);
|
|
88
|
+
|
|
85
89
|
function parseArgv(args) {
|
|
86
90
|
// 坑: parseArgs 会把**任何** `-` 开头的 token 当选项,连正文一起吃:
|
|
87
91
|
// `abs note "-X 是个坑"` → values={X:true,' ':true,是:true,…},正文全丢(旧手写版只认 `--` 长选项)。
|
|
@@ -124,17 +128,39 @@ function parseArgv(args) {
|
|
|
124
128
|
if (missing.length) {
|
|
125
129
|
throw new Error(`✗ ${missing.join('、')} 缺少值\n 用法: --<flag> <值>(如 --dir /path/to/project)`);
|
|
126
130
|
}
|
|
131
|
+
// 只输出**读者实际用的**规范形状:camelCase 键 + mcp/skill 正向布尔。
|
|
132
|
+
// 坑(OPTS-DOUBLE-KEYS): 曾 `...values` 之后再叠加 camelCase → opts 同时带两份 key
|
|
133
|
+
// (keep-days 与 keepDays、no-mcp 与 mcp),读者得自己猜哪份算数。raw 键无人读,
|
|
134
|
+
// 属纯噪音(grep 验证:opts['keep-days'] 零引用)。
|
|
135
|
+
// 用显式白名单而非 ...values:新增 flag 必须在此声明一次,否则"声明了却没接线"
|
|
136
|
+
// 会静默变成"读不到",而不是意外漏出一个无名键。
|
|
127
137
|
const o = {
|
|
128
|
-
...values,
|
|
129
138
|
_: positionals,
|
|
139
|
+
dir: values.dir,
|
|
140
|
+
agent: values.agent,
|
|
141
|
+
id: values.id,
|
|
142
|
+
section: values.section,
|
|
143
|
+
note: values.note,
|
|
144
|
+
as: values.as,
|
|
145
|
+
payload: values.payload,
|
|
146
|
+
tags: values.tags,
|
|
147
|
+
help: !!values.help,
|
|
148
|
+
yes: !!values.yes,
|
|
149
|
+
repair: !!values.repair,
|
|
150
|
+
full: !!values.full,
|
|
130
151
|
keepDays: values['keep-days'],
|
|
131
|
-
dryRun: values['dry-run'],
|
|
152
|
+
dryRun: !!values['dry-run'],
|
|
153
|
+
mcp: !values['no-mcp'],
|
|
154
|
+
skill: !values['no-skill'],
|
|
132
155
|
};
|
|
133
|
-
|
|
134
|
-
|
|
156
|
+
// 未声明的 `--unknown` 静默收下(旧行为:不进位置参数,也不报错)。
|
|
157
|
+
// 这条保留是因为 rejectExtra 只看位置参数 —— 把未知 flag 放进去会改成报错,
|
|
158
|
+
// 那是另一个行为变更,不在本次范围内。
|
|
159
|
+
for (const [k, v] of Object.entries(values)) {
|
|
160
|
+
if (!(k in o) && !KNOWN_RAW.has(k)) o[k] = v;
|
|
161
|
+
}
|
|
135
162
|
return o;
|
|
136
163
|
}
|
|
137
|
-
|
|
138
164
|
const usage = `abs — agent-brain-sync 记忆工具
|
|
139
165
|
|
|
140
166
|
读:
|
|
@@ -313,8 +339,8 @@ async function main() {
|
|
|
313
339
|
case 'todo': {
|
|
314
340
|
const [sub, id, ...rest2] = opts._;
|
|
315
341
|
if (!sub) {
|
|
316
|
-
rejectExtra([id, ...rest2].filter(Boolean), 'abs todo');
|
|
317
|
-
console.log(await cmdShow({ dir: opts.dir, view: 'todo' }));
|
|
342
|
+
rejectExtra([id, ...rest2].filter(Boolean), 'abs todo [--full]');
|
|
343
|
+
console.log(await cmdShow({ dir: opts.dir, view: 'todo', full: opts.full }));
|
|
318
344
|
break;
|
|
319
345
|
}
|
|
320
346
|
const action = TODO_ACTIONS[sub];
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -145,6 +145,14 @@ monorepo 若多个子包各自独立交付,可各建一份 `.brain/`。`abs st
|
|
|
145
145
|
图谱已存在;收到第一个核心开发指令**之前**走这条链载入上下文:
|
|
146
146
|
|
|
147
147
|
1. **读状态**:`abs load`(或 MCP `abs_load`)读 index 路线 + todo 看板 + 最近 log。
|
|
148
|
+
> **load 输出是折叠过的,不是全量。** 两个无上限增长的区块在读取侧收口:
|
|
149
|
+
> - **Done 区** → 按日期计数(曾占 load 输出 68.8%,长历史项目上单次 load 吃掉 40% 上下文)
|
|
150
|
+
> - **index 页面清单** → 各分区只给页数(concept 清单占 load 输出 64%,隨图谱线性增长)
|
|
151
|
+
> - 「最近动作」每条按语义边界收口到 220 字符
|
|
152
|
+
>
|
|
153
|
+
> **路线(Roadmap)区原样保留** —— 那是 load 要传达的状态本身。
|
|
154
|
+
> 要全量明细:`abs todo --full` / `abs index`,或直接读 `.brain/` 文件、
|
|
155
|
+
> `.brain/sessions/<日期>-todo归档.md`。
|
|
148
156
|
2. **对账滞留(强制,别跳过)**:若 `abs load` 顶部出现 `⏳ 上会话滞留`,说明上会话有任务做完/做到一半就断了。**先收尾再开工**:
|
|
149
157
|
- 快照里的任务现在真做完了 → `abs todo done <id>`(done 后下次 load 滞留自动消失);
|
|
150
158
|
- 还没做完 → `abs todo note <id> --note "接到哪/改到哪个文件"` 补断点(别空手续接)。
|
package/src/store.js
CHANGED
|
@@ -4,7 +4,7 @@ import { promises as fs } from 'node:fs';
|
|
|
4
4
|
import { join, resolve, dirname } from 'node:path';
|
|
5
5
|
import { requireBrain, brainPath, absLogDir, BRAIN_DIR } from './index.js';
|
|
6
6
|
import { requireUser, atTag, getUser } from './userconfig.js';
|
|
7
|
-
import { addTask, upsertTask, boardText, readTodo, ensureTodo, todoTemplate, today, localStamp, setBreakpoint, moveBlocked, insertDoneGrouped, idOfTaskLine, archiveDoneInText, renderArchivePage, renderArchiveBody, DONE_KINDS, withDoneKind, doneKindOf } from './todo.js';
|
|
7
|
+
import { addTask, upsertTask, boardText, readTodo, ensureTodo, todoTemplate, today, localStamp, setBreakpoint, moveBlocked, insertDoneGrouped, idOfTaskLine, archiveDoneInText, renderArchivePage, renderArchiveBody, DONE_KINDS, withDoneKind, doneKindOf, collapseDone } from './todo.js';
|
|
8
8
|
import { editFile, SKIP } from './lock.js';
|
|
9
9
|
import { appendWrapup, strandedFor } from './wrapup.js';
|
|
10
10
|
|
|
@@ -128,9 +128,9 @@ export function logTemplate() {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
// ---------- board: 看板 ----------
|
|
131
|
-
export async function cmdBoard({ dir }) {
|
|
131
|
+
export async function cmdBoard({ dir, full }) {
|
|
132
132
|
const root = await requireBrain(dir || process.cwd());
|
|
133
|
-
return boardText(root);
|
|
133
|
+
return boardText(root, undefined, { full: !!full });
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
// ---------- status: 定位报告 + 图谱概要 ----------
|
|
@@ -179,10 +179,11 @@ export async function cmdLoad({ dir }) {
|
|
|
179
179
|
}
|
|
180
180
|
sections.push(
|
|
181
181
|
'--- 当前路线 (index.md) ---',
|
|
182
|
-
index || '(index.md 为空)',
|
|
182
|
+
collapseIndex(index) || '(index.md 为空)',
|
|
183
183
|
'',
|
|
184
184
|
'--- Todo 看板 (todo.md) ---',
|
|
185
|
-
todo.
|
|
185
|
+
collapseDone(todo).text || '(todo.md 为空)',
|
|
186
|
+
'\n(Done 已按日期折叠计数;明细: abs todo --full)',
|
|
186
187
|
'',
|
|
187
188
|
'--- 最近动作 (log.md, 最新 5 条) ---',
|
|
188
189
|
recentLogLines(log, 5) || '(log.md 为空)',
|
|
@@ -204,6 +205,47 @@ export async function cmdLoad({ dir }) {
|
|
|
204
205
|
return sections.join('\n');
|
|
205
206
|
}
|
|
206
207
|
|
|
208
|
+
/** index.md 在 `abs load` 里的折叠形态:保留「当前路线」(那是 load 要传达的状态,
|
|
209
|
+
* 且有界),把页面清单各分区折成计数。
|
|
210
|
+
*
|
|
211
|
+
* 坑: index 的 concept 清单带每一页的一句话描述,**隨图谱线性增长** —— 本仓库 17 条
|
|
212
|
+
* 占 load 输出 2286/3571 tok(64%),另一台 40 条的项目约 2.3 倍。它刚成了 Done 之后
|
|
213
|
+
* 最大的单体膨胀源(Done 已折叠,见 collapseDone)。
|
|
214
|
+
*
|
|
215
|
+
* 续接真正需要的只是「有哪些分区、各多少页」(据此知道去哪找),不需要每页写了什么 ——
|
|
216
|
+
* 要那个用 `abs index`(或直接读 index.md / 按词 `abs query`)。
|
|
217
|
+
* `## 当前路线` 不折(它是路线内容本身,非清单)。 */
|
|
218
|
+
export function collapseIndex(text) {
|
|
219
|
+
const s = String(text || '').trim();
|
|
220
|
+
if (!s) return '';
|
|
221
|
+
const out = [];
|
|
222
|
+
let mode = null; // null=逐行透传(路线区/文件头);字符串=当前在计数的分区名
|
|
223
|
+
let n = 0; // mode 非 null 时的清单行计数
|
|
224
|
+
const flush = () => {
|
|
225
|
+
if (mode !== null) out.push(n ? `## ${mode}(${n} 页)` : `## ${mode}`);
|
|
226
|
+
mode = null;
|
|
227
|
+
n = 0;
|
|
228
|
+
};
|
|
229
|
+
for (const l of s.split('\n')) {
|
|
230
|
+
const m = l.match(/^##\s+(.+?)\s*$/);
|
|
231
|
+
if (m) {
|
|
232
|
+
flush();
|
|
233
|
+
const name = m[1].trim();
|
|
234
|
+
if (/路线|Roadmap/i.test(name)) {
|
|
235
|
+
out.push(l); // 「当前路线」是内容不是清单:原样保留
|
|
236
|
+
mode = null;
|
|
237
|
+
} else {
|
|
238
|
+
mode = name; // 页面清单分区:只计数
|
|
239
|
+
}
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (mode === null) out.push(l); // 透传区(含文件头 H1、路线区正文)
|
|
243
|
+
else if (l.trim().startsWith('-')) n++;
|
|
244
|
+
}
|
|
245
|
+
flush();
|
|
246
|
+
return out.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
247
|
+
}
|
|
248
|
+
|
|
207
249
|
// ---------- wrapup: 滞留快照(B)/ load 内展示由 cmdLoad 完成(A) ----------
|
|
208
250
|
export async function cmdWrapup({ dir }) {
|
|
209
251
|
const root = await requireBrain(dir || process.cwd());
|
|
@@ -366,6 +408,9 @@ function recentLogLines(text, n) {
|
|
|
366
408
|
.split('\n')
|
|
367
409
|
.filter((l) => /^##\s*\[/.test(l))
|
|
368
410
|
.slice(0, n)
|
|
411
|
+
// 每条按语义边界收口:load 是开机读状态,只求"上会话做到哪"的线索,不须全文。
|
|
412
|
+
// 坑: 不收口时 5 条能占 2.9KB(单条 800B),又是一处随日志变长而膨胀的上下文。
|
|
413
|
+
.map((l) => clip(l, 220))
|
|
369
414
|
.join('\n');
|
|
370
415
|
}
|
|
371
416
|
|
|
@@ -493,7 +538,7 @@ async function markDone(file, id, kind = '落地') {
|
|
|
493
538
|
}
|
|
494
539
|
|
|
495
540
|
// ---------- show: 查看 index/todo/log(只读面) ----------
|
|
496
|
-
export async function cmdShow({ dir, view }) {
|
|
541
|
+
export async function cmdShow({ dir, view, full }) {
|
|
497
542
|
const v = String(view || '').toLowerCase();
|
|
498
543
|
if (!['todo', 'index', 'log'].includes(v)) {
|
|
499
544
|
return '用法: abs <todo|index|log> — todo=看板(原 board), index=图谱索引, log=操作流水';
|
|
@@ -510,7 +555,7 @@ export async function cmdShow({ dir, view }) {
|
|
|
510
555
|
return `${v}.md 不存在于 ${brainPath(root)}。初始化/补齐: abs init --repair`;
|
|
511
556
|
}
|
|
512
557
|
if (!text) return `(${v}.md 为空)`;
|
|
513
|
-
return v === 'todo' ? boardText(root, text) : text;
|
|
558
|
+
return v === 'todo' ? boardText(root, text, { full: !!full }) : text;
|
|
514
559
|
}
|
|
515
560
|
// ---------- query: 检索知识图谱(多词 OR,扫全 .md 页) ----------
|
|
516
561
|
const KNOWN_SLUG_HINT = /模板残留|\[\[slug\]\]/;
|
package/src/todo.js
CHANGED
|
@@ -160,6 +160,51 @@ function renderDoneGroups(units) {
|
|
|
160
160
|
return out;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/** 把 Done 区从「全量行」折叠为「按日期计数」,供 `abs load` / `abs todo` 这类
|
|
164
|
+
* 给人(和 AI 上下文)看的视图用。
|
|
165
|
+
*
|
|
166
|
+
* 坑: 这两个命令此前直接 `todo.trim()` 全量打印,而 Done 区**无上限增长** ——
|
|
167
|
+
* 本仓库一处就占 load 输出的 68.8%(19.6KB/28.4KB)。长历史机器上直接把
|
|
168
|
+
* 上下文塞满(实报:「另一台机器 abs load 塞了 40%」)。
|
|
169
|
+
* 这与 hooks.log / wrapup.log 同类问题(那两处有轮转),故这里只给计数,
|
|
170
|
+
* 要看明细用 `abs todo` 加 `--full`,或直接看 todo.md / 归档页。
|
|
171
|
+
*
|
|
172
|
+
* `### 归档` 区(历史的「归档 N 条」标记行)体积小且是长期引用,原样保留。
|
|
173
|
+
* 返回 { text, doneCount };text 不含 `## Done` 标题行。 */
|
|
174
|
+
export function collapseDone(text) {
|
|
175
|
+
const lines = String(text).split('\n');
|
|
176
|
+
const di = lines.findIndex((l) => l.startsWith('## Done'));
|
|
177
|
+
if (di === -1) return { text: String(text).trim(), doneCount: 0 };
|
|
178
|
+
const head = lines.slice(0, di);
|
|
179
|
+
const rest = lines.slice(di + 1);
|
|
180
|
+
const { groupLines, archiveLines } = splitDoneBody(rest);
|
|
181
|
+
const units = parseDoneUnits(groupLines);
|
|
182
|
+
// 按日期归组计数(新日期在前),未标日期的归尾
|
|
183
|
+
const byDate = new Map();
|
|
184
|
+
let undated = 0;
|
|
185
|
+
for (const u of units) {
|
|
186
|
+
if (!u.date) undated++;
|
|
187
|
+
else byDate.set(u.date, (byDate.get(u.date) || 0) + 1);
|
|
188
|
+
}
|
|
189
|
+
const counts = [...byDate.entries()]
|
|
190
|
+
.sort((a, b) => (a[0] < b[0] ? 1 : -1))
|
|
191
|
+
.map(([d, n]) => `${d} ${n} 条`);
|
|
192
|
+
if (undated) counts.push(`未标日期 ${undated} 条`);
|
|
193
|
+
// 只列最近 MAX_DONE_DATE_LINES 个日期组:日期组本身也随历史增长(实测 300 条跨 28 天
|
|
194
|
+
// 会把 load 输出从 660B 拉到 1203B)。载入视图只需"最近做了多少",早期历史看归档页。
|
|
195
|
+
const MAX_DONE_DATE_LINES = 7;
|
|
196
|
+
const shown = counts.slice(0, MAX_DONE_DATE_LINES);
|
|
197
|
+
if (counts.length > MAX_DONE_DATE_LINES) {
|
|
198
|
+
const rest = counts.length - MAX_DONE_DATE_LINES;
|
|
199
|
+
shown.push(`… 另有 ${rest} 个更早日期组(abs todo --full 看全量)`);
|
|
200
|
+
}
|
|
201
|
+
const doneLines = units.length
|
|
202
|
+
? [`## Done(${units.length} 条,按日期折叠)`, ...shown.map((c) => `- ${c}`)]
|
|
203
|
+
: ['## Done(0 条)'];
|
|
204
|
+
const out = [...head, ...doneLines, '', ...archiveLines].join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
205
|
+
return { text: out, doneCount: units.length };
|
|
206
|
+
}
|
|
207
|
+
|
|
163
208
|
/** 归档标记区标题。它在 Done 区内部、日期分组之后,形如:
|
|
164
209
|
* ### 归档
|
|
165
210
|
* - [[2026-09-10-todo归档]] 完成任务 10 条
|
|
@@ -513,9 +558,12 @@ export async function moveBlocked(brainRoot, { id, reason }) {
|
|
|
513
558
|
}
|
|
514
559
|
|
|
515
560
|
// ---------- 看板输出 ----------
|
|
516
|
-
export async function boardText(brainRoot, textOverride) {
|
|
561
|
+
export async function boardText(brainRoot, textOverride, { full = false } = {}) {
|
|
517
562
|
const text = textOverride !== undefined ? textOverride : await readTodo(brainRoot);
|
|
518
563
|
const head = `📂 abs → 项目: ${brainRoot}`;
|
|
519
564
|
if (!text.trim()) return `${head}\n\n(todo.md 为空,先 abs todo add 登记任务)`;
|
|
520
|
-
|
|
565
|
+
// Done 区折叠:它无上限增长,全量打印会把上下文塞满(见 collapseDone 注释)
|
|
566
|
+
const body = full ? text.trim() : collapseDone(text).text;
|
|
567
|
+
const hint = full ? '' : '\n\n(Done 只给计数;看明细: abs todo --full)';
|
|
568
|
+
return `${head}\n\n${body}${hint}`;
|
|
521
569
|
}
|