@fanchao8609/agent_brain_sync 1.17.2 → 1.19.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/LICENSE +21 -0
- package/README.md +16 -7
- package/bin/abs.js +19 -4
- package/bin/mcp.js +8 -6
- package/hooks/abs.omp.ts +852 -0
- package/hooks/abs.pi.ts +55 -18
- package/package.json +2 -1
- package/src/hosts.js +10 -0
- package/src/install.js +53 -0
- package/src/lint.js +5 -3
- package/src/store.js +31 -5
- package/src/todo/archive.js +18 -8
- package/src/todo/common.js +61 -6
- package/src/todo/spec.js +2 -2
- package/src/todo/structure.js +16 -8
- package/src/todo/tasks.js +73 -17
- package/src/todo/validate.js +28 -4
- package/src/todo.js +3 -3
- package/src/wrapup.js +18 -5
package/src/todo/tasks.js
CHANGED
|
@@ -4,7 +4,7 @@ import { promises as fs } from 'node:fs';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { brainPath } from '../index.js';
|
|
6
6
|
import { editFile, SKIP } from '../lock.js';
|
|
7
|
-
import { today, localStamp, extractId, extractNote, extractAuthor, isLegacyAuthorTag, claimOf } from './common.js';
|
|
7
|
+
import { today, localStamp, extractId, extractNote, extractAuthor, isLegacyAuthorTag, claimOf, parseChild, renderChild } from './common.js';
|
|
8
8
|
import { todoTemplate, normalizeTodo, stateOfTaskLine, stripStateMark } from './structure.js';
|
|
9
9
|
import { TASK_STATES } from './spec.js';
|
|
10
10
|
import { collapseDone, groupDoneSection } from './archive.js';
|
|
@@ -45,7 +45,9 @@ export function setStateMark(text, id, state) {
|
|
|
45
45
|
const cur = lines[idx];
|
|
46
46
|
if (stateOfTaskLine(cur) === state) return { text, changed: [] };
|
|
47
47
|
const raw = stripStateMark(cur); // 先剥旧标记,再补新的
|
|
48
|
-
|
|
48
|
+
// 前缀两种形态都要能接回(`- [ ]` 与断点任务 `↳ [ ]`),并保留原缩进 ——
|
|
49
|
+
// 原正则写死 `^(- \[[ x]\] )`,断点任务上补不回标记(2026-10-16 实测:state 报成功但盘上没变)。
|
|
50
|
+
lines[idx] = raw.replace(/^(\s*(?:-\s*\[[ x]\]|↳\s*\[[ x]\])\s*)/, `$1[${state}] `);
|
|
49
51
|
return { text: lines.join('\n'), changed: [`${id} 状态 → ${state}`] };
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -67,9 +69,13 @@ function insertTask(orig, section, text) {
|
|
|
67
69
|
if (idx === -1) {
|
|
68
70
|
lines.push('', header, `- [ ] ${text}`);
|
|
69
71
|
} else {
|
|
70
|
-
//
|
|
72
|
+
// 在该分区标题后、下一个分区标题前插入。
|
|
73
|
+
// ★ 插入点要**跳过尾部空行**:分区末尾本来就有个排版空行(`## Done` 前的那个),
|
|
74
|
+
// 若插到它后面,新任务与前面的内容之间就隔了个空行 —— 每建一条多一个空行,
|
|
75
|
+
// 而闸门明写「条目之间不允许空行」(实测:建 4 条就有 3 个空行)。
|
|
71
76
|
let insertAt = idx + 1;
|
|
72
77
|
while (insertAt < lines.length && !lines[insertAt].startsWith('## ')) insertAt++;
|
|
78
|
+
while (insertAt > idx + 1 && !lines[insertAt - 1].trim()) insertAt--;
|
|
73
79
|
lines.splice(insertAt, 0, `- [ ] ${text}`);
|
|
74
80
|
}
|
|
75
81
|
return lines.join('\n');
|
|
@@ -115,17 +121,19 @@ export async function upsertTask(brainRoot, { section, text, session = '' }) {
|
|
|
115
121
|
: { file: p, text, updated: false };
|
|
116
122
|
}
|
|
117
123
|
|
|
118
|
-
/** 从任务文本提取幂等键(行首 id,如 TASK-1 / T-2 / fix-hook)。
|
|
124
|
+
/** 从任务文本提取幂等键(行首 id,如 TASK-1 / T-2 / fix-hook)。
|
|
125
|
+
* 两种前缀都认:`- [ ] `(普通任务)与 `↳ [ ] `(断点任务)。缩进不影响取 id。 */
|
|
119
126
|
|
|
120
127
|
export function idOfTaskLine(line) {
|
|
121
|
-
if (!line
|
|
128
|
+
if (!line) return null;
|
|
122
129
|
// 两区制下行首可带状态标记(`- [ ] [进行中] <id> …`)—— 必须先剥掉再取 id,
|
|
123
130
|
// 否则 id 会被读成 "[进行中]",findTaskLine 全等比对永远落空(2026-09-13 实测)。
|
|
124
|
-
|
|
131
|
+
// 前缀允许缩进(` - [ ]` / ` ↳ [ ]`)—— 断点任务天然带缩进。
|
|
132
|
+
const m = line.match(/^\s*(?:-\s*\[ \]|↳\s*\[ \])\s*(?:\[[^\]]+\]\s*)?(\S+)/);
|
|
125
133
|
return m ? m[1].replace(/\u200b/g, '') : null;
|
|
126
134
|
}
|
|
127
135
|
|
|
128
|
-
/** 定位 id
|
|
136
|
+
/** 定位 id 对应的未完成任务行下标;真正的附属断点行(`↳ 断点:`)不算任务行。
|
|
129
137
|
* 全等比对,不用 includes(否则 T1 会误命中 T11)。 */
|
|
130
138
|
|
|
131
139
|
export function findTaskLine(lines, id) {
|
|
@@ -134,25 +142,73 @@ export function findTaskLine(lines, id) {
|
|
|
134
142
|
return lines.findIndex((l) => idOfTaskLine(l) === want);
|
|
135
143
|
}
|
|
136
144
|
|
|
137
|
-
/**
|
|
145
|
+
/** 子项行解析:从 `idx+1` 起扫连续的缩进行(子项),返回它们的下标。
|
|
146
|
+
* 遇到非子项(下一个顶层任务 / 空行 / 分区标题)就停。 */
|
|
147
|
+
function childRange(lines, idx) {
|
|
148
|
+
const out = [];
|
|
149
|
+
for (let i = idx + 1; i < lines.length; i++) {
|
|
150
|
+
const line = lines[i];
|
|
151
|
+
if (!line.trim()) break; // 空行 = 子项块结束
|
|
152
|
+
const child = parseChild(line);
|
|
153
|
+
if (child && child.indent > 0) { out.push(i); continue; }
|
|
154
|
+
break; // 另一个顶层任务行 / 标题
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
138
158
|
|
|
159
|
+
/** 追加/更新一条子项(标准 md:缩进 2 空格 + `- [ ]`)。
|
|
160
|
+
*
|
|
161
|
+
* 语义(2026-10-16 用户定):子项就是 todo 的嵌套子任务 —— 可多条、可各自勾选。
|
|
162
|
+
* `abs todo note <id> "…"` **追加**一条(不再覆盖)—— 用户原话「不限,写多少算多少」。
|
|
163
|
+
* 仍保留幂等:与最后一条完全同文就不重复追加(防手滑重复调)。
|
|
164
|
+
* 旧的 `↳ 断点:` 存量行不动(parseChild 认得,继续显示);新写一律用标准 md。 */
|
|
139
165
|
export async function setBreakpoint(brainRoot, { id, text }) {
|
|
140
166
|
const p = await ensureTodo(brainRoot);
|
|
141
|
-
|
|
167
|
+
let added = false;
|
|
142
168
|
const res = await editFile(p, (orig) => {
|
|
143
169
|
const lines = orig.split('\n');
|
|
144
170
|
const idx = findTaskLine(lines, id);
|
|
145
171
|
if (idx === -1) return SKIP;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
172
|
+
const kids = childRange(lines, idx);
|
|
173
|
+
const last = kids.length ? parseChild(lines[kids[kids.length - 1]]) : null;
|
|
174
|
+
if (last && last.text === String(text).trim()) return SKIP; // 幂等:同文不重复追加
|
|
175
|
+
lines.splice(idx + 1 + kids.length, 0, renderChild(text));
|
|
176
|
+
added = true;
|
|
177
|
+
return { text: lines.join('\n'), ok: true };
|
|
178
|
+
});
|
|
179
|
+
if (res === SKIP) {
|
|
180
|
+
return added
|
|
181
|
+
? { ok: true, msg: `✓ 子项已追加 → ${id}` }
|
|
182
|
+
: { ok: false, msg: `[NO_MATCH] 未找到含 "${id}" 的未完成任务行(或子项已存在)` };
|
|
183
|
+
}
|
|
184
|
+
return { ok: true, msg: `✓ 子项已追加 → ${id}\n ${renderChild(text)}` };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** 勾/取消勾子项:`- [ ] …` ↔ `- [x] …`。
|
|
188
|
+
*
|
|
189
|
+
* 定位:子项没有自己的 id(身份 = 「某任务的第 N 条子项」),故按 n 定位(1-based)。
|
|
190
|
+
* n 缺省 = 勾最后一条(最常用的「刚写的这条做完了」)。
|
|
191
|
+
* 旧格式 `↳ 断点:` 也能勾 —— 勾一下就地升级成标准 md 形态。 */
|
|
192
|
+
export async function setBreakpointDone(brainRoot, { id, n, done = true }) {
|
|
193
|
+
const p = await ensureTodo(brainRoot);
|
|
194
|
+
let picked = null;
|
|
195
|
+
const res = await editFile(p, (orig) => {
|
|
196
|
+
const lines = orig.split('\n');
|
|
197
|
+
const idx = findTaskLine(lines, id);
|
|
198
|
+
if (idx === -1) return SKIP;
|
|
199
|
+
const kids = childRange(lines, idx);
|
|
200
|
+
if (!kids.length) return SKIP;
|
|
201
|
+
const target = n ? kids[n - 1] : kids[kids.length - 1];
|
|
202
|
+
if (target === undefined) return SKIP;
|
|
203
|
+
const child = parseChild(lines[target]);
|
|
204
|
+
picked = child;
|
|
205
|
+
lines[target] = renderChild(child.text, { done });
|
|
151
206
|
return { text: lines.join('\n'), ok: true };
|
|
152
207
|
});
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
208
|
+
if (res === SKIP || !picked) {
|
|
209
|
+
return { ok: false, msg: `[NO_MATCH] "${id}" 下没有第 ${n || '(最后)'} 条子项(或任务不存在)` };
|
|
210
|
+
}
|
|
211
|
+
return { ok: true, msg: `✓ 子项 ${done ? '已勾' : '已取消勾'} → ${id}\n ${renderChild(picked.text, { done })}` };
|
|
156
212
|
}
|
|
157
213
|
|
|
158
214
|
// ---------- 任务随手记(memo)----------
|
package/src/todo/validate.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/todo/validate.js — 条目级闸门:形状校验、长度限制、空行检查。
|
|
2
2
|
|
|
3
3
|
import { TODO_SECTIONS, isAllowedTodoSub, INDEX_SECTIONS, ENTRY_SHAPES, indexLinkSectionNeeds, BREAKPOINT_MAX, TASK_LINE_MAX } from './spec.js';
|
|
4
|
-
import { sigOfLine, REPORT_MARKERS, sentenceCount, isTaskLine, isAllowedTodoLine, todoSectionOf } from './common.js';
|
|
4
|
+
import { sigOfLine, REPORT_MARKERS, sentenceCount, isTaskLine, isAllowedTodoLine, todoSectionOf, parseChild } from './common.js';
|
|
5
5
|
|
|
6
6
|
export function checkFileShape(file, text, { lintMode = false } = {}) {
|
|
7
7
|
const issues = [];
|
|
@@ -162,6 +162,29 @@ export function assertTodoContent(text, prevSig = null) {
|
|
|
162
162
|
const l = lines[i];
|
|
163
163
|
const lineNo = i + 1;
|
|
164
164
|
if (l.startsWith('## ')) continue;
|
|
165
|
+
// 子项行要比任务行先判:标准 md 下子项(缩进的 `- [ ]`)**也是任务行**,
|
|
166
|
+
// 若让任务行分支先 continue,子项就只受 TASK_LINE_MAX(400) 管,
|
|
167
|
+
// BREAKPOINT_MAX(200) 的闸门永远走不到(实测:300 字子项静默写入)。
|
|
168
|
+
const kidEarly = parseChild(l);
|
|
169
|
+
if (kidEarly && kidEarly.indent > 0) {
|
|
170
|
+
if (isLegacy(l)) continue; // 存量:不阻写入(lint 报)
|
|
171
|
+
const kbody = kidEarly.text;
|
|
172
|
+
if (kbody.length > BREAKPOINT_MAX) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`✗ 子项太长(第 ${lineNo} 行 ${kbody.length} 字 > ${BREAKPOINT_MAX})—— 子项只写「改到哪个文件哪一步」。\n` +
|
|
175
|
+
` 写不下的内容请分流:实施报告/验证数据 → abs log;经验与坑 → abs note;长设计 → .brain/sources/ 页。\n` +
|
|
176
|
+
` 原文开头: ${kbody.slice(0, 60)}...`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (REPORT_MARKERS.some((re) => re.test(kbody)) || sentenceCount(kbody) > 2) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`✗ 子项里像是塞了报告/清单(第 ${lineNo} 行)—— 子项是一句话(最多两句),不是文档。\n` +
|
|
182
|
+
` todo 只能加 todo:要做的拆成任务,做完的写 abs log,经验写 abs note。\n` +
|
|
183
|
+
` 原文开头: ${kbody.slice(0, 60)}...`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
165
188
|
// 任务行
|
|
166
189
|
if (isTaskLine(l)) {
|
|
167
190
|
if (l.length > TASK_LINE_MAX && !isLegacy(l)) {
|
|
@@ -173,10 +196,11 @@ export function assertTodoContent(text, prevSig = null) {
|
|
|
173
196
|
}
|
|
174
197
|
continue;
|
|
175
198
|
}
|
|
176
|
-
//
|
|
177
|
-
|
|
199
|
+
// 断点附属行(新旧格式用同一个解析器 —— 格式知识只有 common.js 一个真源)
|
|
200
|
+
const bp = parseChild(l);
|
|
201
|
+
if (bp) {
|
|
178
202
|
if (isLegacy(l)) continue; // 存量超长断点:不阻写入(lint 报)
|
|
179
|
-
const body =
|
|
203
|
+
const body = bp.text;
|
|
180
204
|
if (body.length > BREAKPOINT_MAX) {
|
|
181
205
|
throw new Error(
|
|
182
206
|
`✗ 断点太长(第 ${lineNo} 行 ${body.length} 字 > ${BREAKPOINT_MAX})—— 断点只写「改到哪个文件哪一步」。\n` +
|
package/src/todo.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// 保留这一层是为了对外接口稳定:store.js / lint.js / note.js / wrapup.js / 测试的
|
|
3
3
|
// import 路径与名字都不用改。
|
|
4
4
|
|
|
5
|
-
export { TODO_MAX_LINES, LOG_MAX_LINES, INDEX_MAX_LINES, today, localStamp, SEC, extractId, extractNote, extractAuthor, isLegacyAuthorTag, claimOf, sessionOf } from './todo/common.js';
|
|
5
|
+
export { TODO_MAX_LINES, LOG_MAX_LINES, INDEX_MAX_LINES, today, localStamp, SEC, extractId, extractNote, extractAuthor, isLegacyAuthorTag, claimOf, sessionOf, parseChild, renderChild } from './todo/common.js';
|
|
6
6
|
export { LEGACY_SECTION_RENAMES, TODO_SECTIONS, TODO_SUBSECTIONS, isAllowedTodoSub, INDEX_SECTIONS, INDEX_LINK_SECTIONS, LOG_KINDS, H1_TO_FILE, ENTRY_SHAPES, TASK_STATES, RETIRED_SECTIONS, BREAKPOINT_MAX, TASK_LINE_MAX, DONE_KINDS, ARCHIVE_SECTION } from './todo/spec.js';
|
|
7
|
-
export { todoTemplate, rebuildStructure, dropRetiredSections, enforceBrainFormat, normalizeTodo, ensureStateMark, stateOfTaskLine, stripStateMark } from './todo/structure.js';
|
|
7
|
+
export { todoTemplate, rebuildStructure, dropRetiredSections, enforceBrainFormat, normalizeTodo, ensureStateMark, stateOfTaskLine, stripStateMark, checkTaskBox } from './todo/structure.js';
|
|
8
8
|
export { checkFileShape, violationSignatures, assertNoStrayBlank, assertTodoContent } from './todo/validate.js';
|
|
9
9
|
export { doneDateOf, doneKindOf, withDoneKind, collapseDone, archiveDoneInText, renderArchiveBody, upsertArchiveSection, groupDoneSection, insertDoneGrouped } from './todo/archive.js';
|
|
10
|
-
export { readTodo, ensureTodo, setStateMark, addTask, upsertTask, idOfTaskLine, findTaskLine, setBreakpoint, setMemo, readMemo, boardText } from './todo/tasks.js';
|
|
10
|
+
export { readTodo, ensureTodo, setStateMark, addTask, upsertTask, idOfTaskLine, findTaskLine, setBreakpoint, setBreakpointDone, setMemo, readMemo, boardText } from './todo/tasks.js';
|
package/src/wrapup.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { promises as fs } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { absLogDir } from './index.js';
|
|
11
|
-
import { readTodo, localStamp } from './todo.js';
|
|
11
|
+
import { readTodo, localStamp, parseChild } from './todo.js';
|
|
12
12
|
|
|
13
13
|
export function wrapupLogPath() {
|
|
14
14
|
return join(absLogDir(), 'wrapup.log');
|
|
@@ -62,7 +62,15 @@ export function extractOpenTasks(todoText) {
|
|
|
62
62
|
const bp = [];
|
|
63
63
|
while (i + 1 < lines.length && /^\s*↳/.test(lines[i + 1])) {
|
|
64
64
|
i++;
|
|
65
|
-
|
|
65
|
+
// 断点快照带上勾选标记(`[ ] 内容` / `[x] 内容`)—— 快照是给人看的,
|
|
66
|
+
// 不保留勾选就分不清「当时已完成」与「还没做」(2026-10-16 断点可勾后必需)。
|
|
67
|
+
const parsed = parseChild(lines[i]);
|
|
68
|
+
if (parsed) {
|
|
69
|
+
bp.push(`${parsed.done ? '[x]' : '[ ]'} ${parsed.text}`);
|
|
70
|
+
} else {
|
|
71
|
+
// 非断点的 ↳ 行(如 `↳ [ ] 子任务`)仍照旧剥前缀
|
|
72
|
+
bp.push(lines[i].trim().replace(/^↳\s*(断点|卡点)?[::]?\s*/, ''));
|
|
73
|
+
}
|
|
66
74
|
}
|
|
67
75
|
out.push({ body: taskBody, bp });
|
|
68
76
|
}
|
|
@@ -77,12 +85,13 @@ function isPlaceholderBody(body) {
|
|
|
77
85
|
return /^(待办任务|无|(无)|None|null)$/.test(body) || /^待办任务/.test(body);
|
|
78
86
|
}
|
|
79
87
|
|
|
80
|
-
/** 序列化一个快照块:首行时间戳+归属,后随未完成任务(主体+断点)。
|
|
88
|
+
/** 序列化一个快照块:首行时间戳+归属,后随未完成任务(主体+断点)。
|
|
89
|
+
* 断点 bp 元素已自带勾选前缀(`[ ] 内容` / `[x] 内容`,见 extractOpenTasks)。 */
|
|
81
90
|
function blockText(root, tasks) {
|
|
82
91
|
const head = `[${localStamp()}] wrapup proj=${root}`;
|
|
83
92
|
if (!tasks.length) return head; // 无滞留,仍记一行「干净结束」
|
|
84
93
|
const rows = tasks.map((t) => {
|
|
85
|
-
const bps = t.bp.map((b) => ` ↳ 断点: ${b}`);
|
|
94
|
+
const bps = t.bp.map((b) => (b.startsWith('[') ? ` ↳ ${b}` : ` ↳ 断点: ${b}`));
|
|
86
95
|
return ` - [ ] ${t.body}${bps.length ? '\n' + bps.join('\n') : ''}`;
|
|
87
96
|
});
|
|
88
97
|
return [head, ...rows].join('\n');
|
|
@@ -112,7 +121,11 @@ export function parseWrapup(text) {
|
|
|
112
121
|
}
|
|
113
122
|
const bm = line.match(/^\s*↳/);
|
|
114
123
|
if (bm && cur.tasks.length) {
|
|
115
|
-
|
|
124
|
+
// 回读时统一成 `[ ] 内容` / `[x] 内容` 形态(新写的就是这个;旧快照无勾 → 当未勾)
|
|
125
|
+
const parsed = parseChild(line);
|
|
126
|
+
cur.tasks[cur.tasks.length - 1].bp.push(parsed
|
|
127
|
+
? `${parsed.done ? '[x]' : '[ ]'} ${parsed.text}`
|
|
128
|
+
: line.trim().replace(/^↳\s*(断点|卡点)?[::]?\s*/, ''));
|
|
116
129
|
}
|
|
117
130
|
}
|
|
118
131
|
return byProj;
|