@fanchao8609/agent_brain_sync 1.19.6 → 1.20.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.
- package/README.md +8 -7
- package/bin/abs.js +7 -7
- package/bin/mcp.js +7 -7
- package/hooks/abs.omp.ts +15 -17
- package/hooks/abs.pi.ts +29 -34
- package/hooks/event.sh +1 -1
- package/package.json +1 -1
- package/skill/abs-agent-brain-sync/SKILL.md +104 -99
- package/src/lint.js +22 -5
- package/src/store.js +20 -13
- package/src/todo/archive.js +5 -2
- package/src/todo/common.js +40 -31
- package/src/todo/spec.js +3 -5
- package/src/todo/structure.js +44 -16
- package/src/todo/tasks.js +8 -12
- package/src/todo/validate.js +7 -8
- package/src/todo.js +1 -0
- package/src/wrapup.js +17 -11
package/src/todo/structure.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { TODO_SECTIONS, H1_TO_FILE, TASK_STATES, RETIRED_SECTIONS } from './spec.js';
|
|
4
4
|
import { checkFileShape, violationSignatures, assertNoStrayBlank, assertTodoContent } from './validate.js';
|
|
5
|
-
import { trimBlank, sigOfLine, isPlaceholder } from './common.js';
|
|
5
|
+
import { trimBlank, sigOfLine, isPlaceholder, migrateLegacyChild } from './common.js';
|
|
6
6
|
|
|
7
7
|
export function todoTemplate() {
|
|
8
8
|
// 由 rebuildStructure 生成,保证“模板”与“重排结果”逐字节一致
|
|
@@ -156,8 +156,24 @@ function stripStrayBlanks(body, fixed) {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
export function enforceBrainFormat(text, spec, prev) {
|
|
159
|
-
|
|
159
|
+
let src = String(text ?? '');
|
|
160
160
|
if (!src.trim()) return { text: src, fixed: [] };
|
|
161
|
+
const fixed = [];
|
|
162
|
+
// ★ 旧 `↳` 子项写法最先迁移(2026-10-20),必须跑在**一切校验之前**:
|
|
163
|
+
// 否则旧行会被 assertTodoContent 判成「裸文本」而拒写 —— 整个库变只读。
|
|
164
|
+
// (实测:真库形态的 ` ↳ 断点: …` 在 load 时就撞上了这条。)
|
|
165
|
+
if (spec?.h1 === '# 📋 Todo Board') {
|
|
166
|
+
const ls0 = src.split('\n');
|
|
167
|
+
let n0 = 0;
|
|
168
|
+
for (let i = 0; i < ls0.length; i++) {
|
|
169
|
+
const conv = migrateLegacyChild(ls0[i]);
|
|
170
|
+
if (conv !== null && conv !== ls0[i]) { ls0[i] = conv; n0++; }
|
|
171
|
+
}
|
|
172
|
+
if (n0) {
|
|
173
|
+
src = ls0.join('\n');
|
|
174
|
+
fixed.push(`旧 ↳ 子项写法 → 标准 md 缩进子项(${n0} 行)`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
161
177
|
// 内容闸门:形状归一之前先拒掉乱塞(超长断点/报告体),否则下面的 rebuild
|
|
162
178
|
// 会把它当「人自加的正文」好好保留下来 —— 越规整越难发现。
|
|
163
179
|
// ★ 存量宽容(2026-10-05 本机实测踩到):只拒「本次写入新引入」的违规;
|
|
@@ -203,7 +219,6 @@ export function enforceBrainFormat(text, spec, prev) {
|
|
|
203
219
|
);
|
|
204
220
|
}
|
|
205
221
|
};
|
|
206
|
-
const fixed = [];
|
|
207
222
|
// 旧标记 → 标准标记(H1 与分区/分组标题)。与 store.js 的 LEGACY_MARKS 同源,
|
|
208
223
|
// 由调用方通过 spec.renames 注入(todo.js 不反向依赖 store.js)。
|
|
209
224
|
|
|
@@ -233,13 +248,23 @@ export function enforceBrainFormat(text, spec, prev) {
|
|
|
233
248
|
// 底层 addTask/upsertTask 传的就是裸文本,它们是内部 API,不是 AI 入口)。
|
|
234
249
|
if (fname === 'todo.md') {
|
|
235
250
|
const ls = body.split('\n');
|
|
251
|
+
let migrated = 0;
|
|
236
252
|
for (let i = 0; i < ls.length; i++) {
|
|
253
|
+
// ★ 旧 `↳ 断点:` / `↳ [ ]` 写法 → 标准 md 缩进子项(2026-10-20)。
|
|
254
|
+
// 为何在闸门里做而不只靠 load:所有写入都过这里,迁移一次就永久生效。
|
|
255
|
+
// 保留这个转换器是「存量不能锁死」的代价 —— 它是全仓唯一还认得 ↳ 的地方。
|
|
256
|
+
const conv = migrateLegacyChild(ls[i]);
|
|
257
|
+
if (conv !== null) {
|
|
258
|
+
if (conv !== ls[i]) { ls[i] = conv; migrated++; }
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
237
261
|
// 裸任务行(`- [ ] id …` 无状态标记)→ 补默认「进行中」
|
|
238
262
|
if (/^- \[ \] /.test(ls[i]) && !/^- \[ \] \[[^\]]+\]/.test(ls[i])) {
|
|
239
263
|
const fixed2 = ensureStateMark(ls[i], '进行中');
|
|
240
264
|
if (fixed2 !== ls[i]) { ls[i] = fixed2; fixed.push('补回缺失的状态标记'); }
|
|
241
265
|
}
|
|
242
266
|
}
|
|
267
|
+
if (migrated) fixed.push(`旧 ↳ 子项写法 → 标准 md 缩进子项(${migrated} 行)`);
|
|
243
268
|
body = ls.join('\n');
|
|
244
269
|
}
|
|
245
270
|
|
|
@@ -277,13 +302,18 @@ function normalizeTodoIf(body, spec) {
|
|
|
277
302
|
}
|
|
278
303
|
|
|
279
304
|
export function normalizeTodo(text) {
|
|
280
|
-
|
|
305
|
+
// ★ 旧 `↳` 子项写法先就地迁移(2026-10-20)。
|
|
306
|
+
// 为何放在 normalizeTodo 而不只放闸门:读路径(readTodo)不过写入闸门,
|
|
307
|
+
// 不在这里迁,旧行就会以「看不见」的状态留在盘上(面板/看板都不认它)。
|
|
308
|
+
// 迁移后走到下面的「已是标准两区」早退分支也没关系 —— 内容已经是新的了。
|
|
309
|
+
const migrated = String(text).split('\n').map((l) => migrateLegacyChild(l) ?? l).join('\n');
|
|
310
|
+
const lines = migrated.split('\n');
|
|
281
311
|
const has = (name) => lines.some((l) => l.trim() === `## ${name}`);
|
|
282
312
|
// 已是标准两区制 = 有 Todo 且**没有**任何进行时老分区。
|
|
283
313
|
// 坑(2026-09-13):曾只判 has('Todo') 就 return —— 而最老格式里也可能有 `## Todo`
|
|
284
314
|
// (与 `## In Progress` 混用),于是旧文件永不迁移。
|
|
285
315
|
const legacy = ['Backlog', 'Today / In Progress', 'In Progress', 'Blocked'].some(has);
|
|
286
|
-
if (has('Todo') && !legacy) return
|
|
316
|
+
if (has('Todo') && !legacy) return migrated;
|
|
287
317
|
const out = ['# 📋 Todo Board'];
|
|
288
318
|
const grab = (name) => {
|
|
289
319
|
const items = [];
|
|
@@ -308,33 +338,31 @@ export function normalizeTodo(text) {
|
|
|
308
338
|
}
|
|
309
339
|
|
|
310
340
|
/** 给任务行补行首状态标记(已有则不覆盖)。`- [ ] id …` → `- [ ] [状态] id …`
|
|
311
|
-
*
|
|
341
|
+
* 缩进与 `- [ ]` 前缀都认(子项也要能补标记)。 */
|
|
312
342
|
export function ensureStateMark(line, state) {
|
|
313
|
-
const m = String(line).match(/^(\s
|
|
343
|
+
const m = String(line).match(/^(\s*-\s*\[[ x]\]\s*)(\[[^\]]+\]\s+)?(.*)$/);
|
|
314
344
|
if (!m) return line;
|
|
315
345
|
if (m[2] && TASK_STATES.includes(m[2].trim().replace(/[\x5B\x5D]/g, ''))) return line;
|
|
316
346
|
return `${m[1]}[${state}] ${m[3]}`;
|
|
317
347
|
}
|
|
318
348
|
|
|
319
|
-
/**
|
|
349
|
+
/** 读任务行的状态标记;缩进与 `- [ ]` 前缀都认。 */
|
|
320
350
|
export function stateOfTaskLine(line) {
|
|
321
|
-
const m = String(line).match(/^\s
|
|
351
|
+
const m = String(line).match(/^\s*-\s*\[[ x]\]\s*\[([^\]]+)\]/);
|
|
322
352
|
return m && TASK_STATES.includes(m[1]) ? m[1] : null;
|
|
323
353
|
}
|
|
324
354
|
|
|
325
355
|
/** 剥掉任务行的状态标记(`- [ ] [进行中] id …` → `- [ ] id …`),保留前缀与缩进。 */
|
|
326
356
|
|
|
327
357
|
export function stripStateMark(line) {
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
return String(line).replace(/^(\s*(?:-\s*\[[ x]\]|↳\s*\[[ x]\])\s*)\[[^\]]+\]\s*/, '$1');
|
|
358
|
+
// 前缀只剥 `- [ ] `(缩进与状态标记的位置保留)。
|
|
359
|
+
// 旧正则里带 `↳ [ ]` 分支已在 2026-10-20 随旧写法一并移除。
|
|
360
|
+
return String(line).replace(/^(\s*-\s*\[[ x]\]\s*)\[[^\]]+\]\s*/, '$1');
|
|
332
361
|
}
|
|
333
362
|
|
|
334
|
-
/** 把未完成任务行的复选框从 `[ ]` 翻到 `[x]`(保留原前缀与缩进)。
|
|
335
|
-
* 单独抽出:断点任务的 `↳ ` 前缀 + 缩进必须原样保留,用固定的 `- [ ]` 字面替换会漏。 */
|
|
363
|
+
/** 把未完成任务行的复选框从 `[ ]` 翻到 `[x]`(保留原前缀与缩进)。 */
|
|
336
364
|
export function checkTaskBox(line) {
|
|
337
|
-
return String(line).replace(/^(\s
|
|
365
|
+
return String(line).replace(/^(\s*-\s*)\[ \]/, '$1[x]');
|
|
338
366
|
}
|
|
339
367
|
|
|
340
368
|
/** 从已完成任务行提取 `(完成 YYYY-MM-DD)` 日期;无则返回 ''。兼容中英文括号。 */
|
package/src/todo/tasks.js
CHANGED
|
@@ -45,9 +45,8 @@ 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
|
-
//
|
|
49
|
-
|
|
50
|
-
lines[idx] = raw.replace(/^(\s*(?:-\s*\[[ x]\]|↳\s*\[[ x]\])\s*)/, `$1[${state}] `);
|
|
48
|
+
// 前缀统一为 `- [ ]`(缩进保留);旧 `↳` 写法已由 migrateLegacyChild 迁移掉。
|
|
49
|
+
lines[idx] = raw.replace(/^(\s*-\s*\[[ x]\]\s*)/, `$1[${state}] `);
|
|
51
50
|
return { text: lines.join('\n'), changed: [`${id} 状态 → ${state}`] };
|
|
52
51
|
}
|
|
53
52
|
|
|
@@ -121,19 +120,18 @@ export async function upsertTask(brainRoot, { section, text, session = '' }) {
|
|
|
121
120
|
: { file: p, text, updated: false };
|
|
122
121
|
}
|
|
123
122
|
|
|
124
|
-
/** 从任务文本提取幂等键(行首 id,如 TASK-1 / T-2 / fix-hook)。
|
|
125
|
-
* 两种前缀都认:`- [ ] `(普通任务)与 `↳ [ ] `(断点任务)。缩进不影响取 id。 */
|
|
123
|
+
/** 从任务文本提取幂等键(行首 id,如 TASK-1 / T-2 / fix-hook)。 */
|
|
126
124
|
|
|
127
125
|
export function idOfTaskLine(line) {
|
|
128
126
|
if (!line) return null;
|
|
129
127
|
// 两区制下行首可带状态标记(`- [ ] [进行中] <id> …`)—— 必须先剥掉再取 id,
|
|
130
128
|
// 否则 id 会被读成 "[进行中]",findTaskLine 全等比对永远落空(2026-09-13 实测)。
|
|
131
|
-
// 前缀允许缩进(` - [ ]
|
|
132
|
-
const m = line.match(/^\s
|
|
129
|
+
// 前缀允许缩进(` - [ ]`)—— 子任务天然带缩进。
|
|
130
|
+
const m = line.match(/^\s*-\s*\[ \]\s*(?:\[[^\]]+\]\s*)?(\S+)/);
|
|
133
131
|
return m ? m[1].replace(/\u200b/g, '') : null;
|
|
134
132
|
}
|
|
135
133
|
|
|
136
|
-
/** 定位 id
|
|
134
|
+
/** 定位 id 对应的未完成任务行下标;子项行不算独立任务。
|
|
137
135
|
* 全等比对,不用 includes(否则 T1 会误命中 T11)。 */
|
|
138
136
|
|
|
139
137
|
export function findTaskLine(lines, id) {
|
|
@@ -160,8 +158,7 @@ function childRange(lines, idx) {
|
|
|
160
158
|
*
|
|
161
159
|
* 语义(2026-10-16 用户定):子项就是 todo 的嵌套子任务 —— 可多条、可各自勾选。
|
|
162
160
|
* `abs todo note <id> "…"` **追加**一条(不再覆盖)—— 用户原话「不限,写多少算多少」。
|
|
163
|
-
* 仍保留幂等:与最后一条完全同文就不重复追加(防手滑重复调)。
|
|
164
|
-
* 旧的 `↳ 断点:` 存量行不动(parseChild 认得,继续显示);新写一律用标准 md。 */
|
|
161
|
+
* 仍保留幂等:与最后一条完全同文就不重复追加(防手滑重复调)。 */
|
|
165
162
|
export async function setBreakpoint(brainRoot, { id, text }) {
|
|
166
163
|
const p = await ensureTodo(brainRoot);
|
|
167
164
|
let added = false;
|
|
@@ -187,8 +184,7 @@ export async function setBreakpoint(brainRoot, { id, text }) {
|
|
|
187
184
|
/** 勾/取消勾子项:`- [ ] …` ↔ `- [x] …`。
|
|
188
185
|
*
|
|
189
186
|
* 定位:子项没有自己的 id(身份 = 「某任务的第 N 条子项」),故按 n 定位(1-based)。
|
|
190
|
-
* n 缺省 = 勾最后一条(最常用的「刚写的这条做完了」)。
|
|
191
|
-
* 旧格式 `↳ 断点:` 也能勾 —— 勾一下就地升级成标准 md 形态。 */
|
|
187
|
+
* n 缺省 = 勾最后一条(最常用的「刚写的这条做完了」)。 */
|
|
192
188
|
export async function setBreakpointDone(brainRoot, { id, n, done = true }) {
|
|
193
189
|
const p = await ensureTodo(brainRoot);
|
|
194
190
|
let picked = null;
|
package/src/todo/validate.js
CHANGED
|
@@ -91,12 +91,11 @@ export function checkFileShape(file, text, { lintMode = false } = {}) {
|
|
|
91
91
|
let p = i - 1;
|
|
92
92
|
while (p >= 0 && !lines[p].trim()) p--;
|
|
93
93
|
const prev = p >= 0 ? lines[p] : null;
|
|
94
|
-
// 父行可以是:顶层任务 `- [x]
|
|
95
|
-
//
|
|
94
|
+
// 父行可以是:顶层任务 `- [x]` 或子任务 ` - [x]`。
|
|
95
|
+
// (旧 `↳ [ ]` 形态已由 migrateLegacyChild 迁移,不再存在)
|
|
96
96
|
const ok = prev !== null && (
|
|
97
97
|
/^- \[[ x]\]/.test(prev) ||
|
|
98
|
-
/^\s+- \[[ x]\]/.test(prev)
|
|
99
|
-
/^\s*↳\s*\[[ x]\]/.test(prev)
|
|
98
|
+
/^\s+- \[[ x]\]/.test(prev)
|
|
100
99
|
);
|
|
101
100
|
if (!ok) {
|
|
102
101
|
issues.push({
|
|
@@ -233,28 +232,28 @@ export function assertTodoContent(text, prevSig = null) {
|
|
|
233
232
|
const body = bp.text;
|
|
234
233
|
if (body.length > BREAKPOINT_MAX) {
|
|
235
234
|
throw new Error(
|
|
236
|
-
`✗
|
|
235
|
+
`✗ 子项太长(第 ${lineNo} 行 ${body.length} 字 > ${BREAKPOINT_MAX})—— 子项只写「改到哪个文件哪一步」。\n` +
|
|
237
236
|
` 写不下的内容请分流:实施报告/验证数据 → abs log;经验与坑 → abs note;长设计 → .brain/sources/ 页。\n` +
|
|
238
237
|
` 原文开头: ${body.slice(0, 60)}...`,
|
|
239
238
|
);
|
|
240
239
|
}
|
|
241
240
|
if (REPORT_MARKERS.some((re) => re.test(body)) || sentenceCount(body) > 2) {
|
|
242
241
|
throw new Error(
|
|
243
|
-
`✗
|
|
242
|
+
`✗ 子项里像是塞了报告/清单(第 ${lineNo} 行)—— 子项是一句话(最多两句),不是文档。\n` +
|
|
244
243
|
` todo 只能加 todo:要做的拆成任务,做完的写 abs log,经验写 abs note。\n` +
|
|
245
244
|
` 原文开头: ${body.slice(0, 60)}...`,
|
|
246
245
|
);
|
|
247
246
|
}
|
|
248
247
|
continue;
|
|
249
248
|
}
|
|
250
|
-
// 裸文本行(2026-10-11):Todo
|
|
249
|
+
// 裸文本行(2026-10-11):Todo 区里既不是任务行也不是子项的行 = 总结/报告/清单。
|
|
251
250
|
// 前面的修法都挂在「行的形态」上(超长断点、多句断点),把总结写成裸文本行就全部绕过
|
|
252
251
|
// ——同一个坑第三次复发。改成白名单后,换形态也落回这里。
|
|
253
252
|
// 按区判:Todo 区才管;Done/Archived 区有日期分组与归档链接,判据不同。
|
|
254
253
|
// 存量行(prevSig)放行,交给 lint 报 —— 否则老库直接被锁死写不进去。
|
|
255
254
|
if (todoSectionOf(lines, i) === 'Todo' && !isAllowedTodoLine(l) && !isLegacy(l)) {
|
|
256
255
|
throw new Error(
|
|
257
|
-
`✗ todo 的 Todo 区不允许裸文本(第 ${lineNo} 行)—— 这里只放任务行(- [ ]
|
|
256
|
+
`✗ todo 的 Todo 区不允许裸文本(第 ${lineNo} 行)—— 这里只放任务行(- [ ] …)与子项( - [ ] …)。\n` +
|
|
258
257
|
` 「总结/实施报告/清单」请分流:流水与测试数据 → abs log;经验与坑 → abs note;长设计与方案 → .brain/sources/ 页。\n` +
|
|
259
258
|
` 原文开头: ${l.trim().slice(0, 60)}...`,
|
|
260
259
|
);
|
package/src/todo.js
CHANGED
|
@@ -8,3 +8,4 @@ export { todoTemplate, rebuildStructure, dropRetiredSections, enforceBrainFormat
|
|
|
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
10
|
export { readTodo, ensureTodo, setStateMark, addTask, upsertTask, idOfTaskLine, findTaskLine, setBreakpoint, setBreakpointDone, setMemo, readMemo, boardText } from './todo/tasks.js';
|
|
11
|
+
export { migrateLegacyChild, isTaskLine, isChildLine, CHILD_INDENT } from './todo/common.js';
|
package/src/wrapup.js
CHANGED
|
@@ -37,7 +37,7 @@ async function rotateIfNeeded(p) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/** 从 todo 文本/快照块提取任务清单。兼容顶层(todo.md `- [ ]`)与缩进(快照 ` - [ ]`)两种行。
|
|
40
|
-
* body = 去掉 `- [ ]` 前缀、`(认领|完成 date)` 标注后的核心文本。bp 去掉
|
|
40
|
+
* body = 去掉 `- [ ]` 前缀、`(认领|完成 date)` 标注后的核心文本。bp 去掉 `[ ]`/`[x]` 勾选前缀。
|
|
41
41
|
* Done 区(## Done 下)不采。返回 [{ body, bp }],bp 为附属断点数组(或空)。 */
|
|
42
42
|
export function extractOpenTasks(todoText) {
|
|
43
43
|
const lines = String(todoText || '').split('\n');
|
|
@@ -60,16 +60,18 @@ export function extractOpenTasks(todoText) {
|
|
|
60
60
|
.trim();
|
|
61
61
|
if (taskBody && !isPlaceholderBody(taskBody)) {
|
|
62
62
|
const bp = [];
|
|
63
|
-
|
|
63
|
+
// 子项:缩进 `- [ ]`(标准 md)。旧快照里的 `↳` 行也能读 —— 那是历史
|
|
64
|
+
// wrapup.log 的序列化格式,不能因为改了新格式就让旧快照的日志读不出来。
|
|
65
|
+
while (i + 1 < lines.length && (/^\s+-\s*\[[ x]\]/.test(lines[i + 1]) || /^\s*↳/.test(lines[i + 1]))) {
|
|
64
66
|
i++;
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
+
// 子项快照带上勾选标记(`[ ] 内容` / `[x] 内容`)—— 快照是给人看的,
|
|
68
|
+
// 不保留勾选就分不清「当时已完成」与「还没做」。
|
|
67
69
|
const parsed = parseChild(lines[i]);
|
|
68
70
|
if (parsed) {
|
|
69
71
|
bp.push(`${parsed.done ? '[x]' : '[ ]'} ${parsed.text}`);
|
|
70
72
|
} else {
|
|
71
|
-
//
|
|
72
|
-
bp.push(lines[i].trim().replace(/^↳\s*(
|
|
73
|
+
// 旧快照的非标准 ↳ 行:剥掉 ↳ 与可选标签,内容原样保留
|
|
74
|
+
bp.push(lines[i].trim().replace(/^↳\s*(断点|卡点|内容)?[::]?\s*/, ''));
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
out.push({ body: taskBody, bp });
|
|
@@ -85,13 +87,16 @@ function isPlaceholderBody(body) {
|
|
|
85
87
|
return /^(待办任务|无|(无)|None|null)$/.test(body) || /^待办任务/.test(body);
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
/**
|
|
89
|
-
*
|
|
90
|
+
/** 序列化一个快照块:首行时间戳+归属,后随未完成任务(主体+子项)。
|
|
91
|
+
* 子项 bp 元素已自带勾选前缀(`[ ] 内容` / `[x] 内容`,见 extractOpenTasks)。
|
|
92
|
+
* 新写入一律用标准 md 缩进子项(旧 `↳ 断点:` 形态已废弃)。 */
|
|
90
93
|
function blockText(root, tasks) {
|
|
91
94
|
const head = `[${localStamp()}] wrapup proj=${root}`;
|
|
92
95
|
if (!tasks.length) return head; // 无滞留,仍记一行「干净结束」
|
|
93
96
|
const rows = tasks.map((t) => {
|
|
94
|
-
const bps = t.bp.map((b) => (b.startsWith('[')
|
|
97
|
+
const bps = t.bp.map((b) => (b.startsWith('[')
|
|
98
|
+
? ` - ${b}`
|
|
99
|
+
: ` - [ ] ${b}`));
|
|
95
100
|
return ` - [ ] ${t.body}${bps.length ? '\n' + bps.join('\n') : ''}`;
|
|
96
101
|
});
|
|
97
102
|
return [head, ...rows].join('\n');
|
|
@@ -119,13 +124,14 @@ export function parseWrapup(text) {
|
|
|
119
124
|
}
|
|
120
125
|
continue;
|
|
121
126
|
}
|
|
122
|
-
|
|
127
|
+
// 子项:新快照写缩进 `- [ ]`;旧快照(历史 wrapup.log)写的是 `↳ …`,两者都要读
|
|
128
|
+
const bm = line.match(/^\s+-\s*\[[ x]\]/) || line.match(/^\s*↳/);
|
|
123
129
|
if (bm && cur.tasks.length) {
|
|
124
130
|
// 回读时统一成 `[ ] 内容` / `[x] 内容` 形态(新写的就是这个;旧快照无勾 → 当未勾)
|
|
125
131
|
const parsed = parseChild(line);
|
|
126
132
|
cur.tasks[cur.tasks.length - 1].bp.push(parsed
|
|
127
133
|
? `${parsed.done ? '[x]' : '[ ]'} ${parsed.text}`
|
|
128
|
-
: line.trim().replace(/^↳\s*(
|
|
134
|
+
: line.trim().replace(/^↳\s*(断点|卡点|内容)?[::]?\s*/, ''));
|
|
129
135
|
}
|
|
130
136
|
}
|
|
131
137
|
return byProj;
|