pi-anchor 0.5.1 → 0.6.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 CHANGED
@@ -1,12 +1,12 @@
1
1
  # ⌖ Anchor
2
2
 
3
- > **A digital sticky note docked below your Pi editor — 0ms instant stick, micro progress bars `[■■□□ 2/4]`, 1-click session teleport (`↪`), and automatic teardown on `git commit`.**
3
+ > **A digital sticky note & cross-session `todo` checkpoint docked below your Pi editor — automatically catches unfinished `todo` lists when you close the terminal, sticks quick notes in 0ms (`pin: ...`), and teleports back to the original session (`/back`).**
4
4
 
5
+ [![npm version](https://img.shields.io/npm/v/pi-anchor?color=blue)](https://www.npmjs.com/package/pi-anchor)
6
+ [![npm downloads](https://img.shields.io/npm/dm/pi-anchor?color=22c55e&label=downloads)](https://www.npmjs.com/package/pi-anchor)
7
+ [![GitHub stars](https://img.shields.io/github/stars/3ZEROS12/anchor?style=flat&color=f59e0b)](https://github.com/3ZEROS12/anchor/stargazers)
5
8
  [![CI Status](https://github.com/3ZEROS12/anchor/actions/workflows/ci.yml/badge.svg)](https://github.com/3ZEROS12/anchor/actions/workflows/ci.yml)
6
- [![Node v20+](https://img.shields.io/badge/Node-v20+-22c55e.svg)](package.json)
7
- [![TypeScript Strict](https://img.shields.io/badge/TypeScript-Strict-3b82f6.svg)](tsconfig.json)
8
- [![Tests: 10 Passed](https://img.shields.io/badge/Tests-10%20Passed-22c55e.svg)](tests/store.test.ts)
9
- [![Startup: < 45ms](https://img.shields.io/badge/Startup-%3C%2045ms-success.svg)](package.json)
9
+ [![Tests: 11 Passed](https://img.shields.io/badge/Tests-11%20Passed-22c55e.svg)](tests/store.test.ts)
10
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-f97316.svg)](LICENSE)
11
11
 
12
12
  **English** | [简体中文](./README_zh.md)
@@ -37,11 +37,11 @@ Late at night, mid-way through fixing a bug, you spot a second issue in `src/aut
37
37
  2. **`TODO.md` Pollutes Git & Burns Context Tokens**: Tracking personal reminders inside repository markdown files dirties `git status` and re-injects stale checklists into the system prompt on every turn.
38
38
  3. **Splash Banners Vanish When You Start Typing**: Traditional startup widgets disappear on the first message. For the rest of a two-hour session, your notes are hidden unless you manually run a slash command.
39
39
 
40
- **Anchor acts like a physical sticky note stuck to the bottom bezel of your Pi editor**:
41
- - **Persistent Below-Editor Strip (`belowEditor`)**: Docks a single-line note strip directly beneath your input box. It stays in view while you code, leaves `aboveEditor` untouched for extensions like `pi-lingual`, and collapses to 0 lines when empty.
40
+ **Anchor acts like a physical sticky note and auto-save checkpoint stuck to the bottom bezel of your Pi editor**:
41
+ - **Auto-Todo Bridge (`todo` ➔ Cross-Session Checkpoint)**: Standard `todo` tools (`@juicesharp/rpiv-todo` and built-in `todo`) live only inside a single conversation with zero disk writes — closing the terminal or running `/new` wipes unfinished tasks. Anchor automatically mirrors unfinished `todo` snapshots to disk (`[■■□□ 2/5]`) and auto-clears them the moment all steps reach `completed`.
42
+ - **Persistent Below-Editor Strip (`belowEditor`)**: Docks a single-line note strip directly beneath your input box. It stays in view while you code, leaves `aboveEditor` untouched for extensions like `pi-lingual` and `rpiv-todo`, and collapses to 0 lines when empty.
42
43
  - **0ms Human-First Capture (`pin: ...`)**: Type `pin: Check token expiry` (or `/pin ...`) straight into the editor. An input hook intercepts the text in `0.00ms` without calling the language model or spending tokens.
43
- - **Micro Progress Bar (`[■■□□ 2/4]`)**: Write `[2/4]` or numbered steps (`1. Extract interface 2. HttpOnly cookie 3. Unit tests`) in your note to get an inline 4-cell progress bar with next-step hints.
44
- - **Session Teleport (`↪ Jump to session`)**: Every note bookmarks the exact `.jsonl` session file where it was created or last touched. Select `↪ Jump to session` in `/anchor` to switch Pi straight back to that conversation.
44
+ - **Session Teleport (`/back` & `↪ Jump to session`)**: Every note and auto-checkpoint bookmarks the exact `.jsonl` session file where work happened. Run `/back` (or select `↪ Jump to session` in `/anchor`) to switch Pi straight back to that conversation.
45
45
 
46
46
  ---
47
47
 
@@ -49,9 +49,10 @@ Late at night, mid-way through fixing a bug, you spot a second issue in `src/aut
49
49
 
50
50
  | Mode / Action | What You Input | What the Engine Does | What You See | Best For |
51
51
  | :--- | :--- | :--- | :--- | :--- |
52
+ | **Auto-Todo Checkpoint** | Agent calls `todo` (`2/5` done, then you quit or `/new`) | Mirrors unfinished `todo` snapshot + modified files + `sessionFile`; auto-clears at `5/5` | Next session shows: `⌖ #1 [■■□□ 2/5] Switch token ▸ Add tests (↪)` | Never losing unfinished `todo` lists across terminal restarts |
52
53
  | **Stick a Note (0ms)** | Type `pin: Fix auth in src/auth.ts` or `/pin ...` | Intercepts via input hook in `0ms`, extracts file paths, and links current `sessionFile` | 1-line strip appears below editor: `⌖ #1 Fix auth (app · auth.ts ↪)` | Capturing a tangent without derailing the current prompt |
53
54
  | **Track Multi-Step Work** | Type `/pin Refactor: 1. API 2. Cookie 3. Tests` then `/step 1` | Parses step list and increments `done / total` counter in `~/.anchor/state.json` | Strip updates in place: `⌖ #1 [■■□□ 2/3] Refactor ▸ Tests` | Multi-session features and long-running study goals |
54
- | **Resume Context (`↪`)** | Run `/anchor` ➔ select `↪ Jump to session` (or `/anchor jump 1`) | Invokes Pi's native `ctx.switchSession(note.sessionFile)` | Terminal switches directly back to the original conversation transcript | Picking up a complex task days later with full history |
55
+ | **Resume Context (`/back`)** | Run `/back` (or `/anchor` ➔ `↪ Jump to session`) | Invokes Pi's native `ctx.switchSession(note.sessionFile)` | Terminal switches directly back to the original conversation transcript | Picking up a complex task days later with full history |
55
56
  | **Auto-Tear on Commit** | Commit code (`git commit -m "feat: fix auth"`) or run `/done 1` | Matches commit message against active notes (filtering generic stop-words) | Note archives to `archive.jsonl`; strip clears (`⌖ Auto-torn off #1`) | Keeping the sticky strip clean without manual bookkeeping |
56
57
 
57
58
  ---
@@ -90,7 +91,7 @@ All state lives in `~/.anchor/` outside your git repository:
90
91
  | **Pi Slash** | `/pin <note>` | Stick a note (supports `[1/4]` or `1. A 2. B 3. C` steps) |
91
92
  | **Pi Slash** | `/anchor` | Open interactive sticky note board (tear off, `+1` step, `↪` jump to session, or read details) |
92
93
  | **Pi Slash** | `/step 1` | Advance progress bar on note `#1` by `+1` |
93
- | **Pi Slash** | `/anchor jump 1` | Switch Pi back to the conversation session linked to note `#1` |
94
+ | **Pi Slash** | `/back` (or `/anchor jump 1`) | Teleport Pi back to the conversation session of the latest unfinished note/todo checkpoint |
94
95
  | **Pi Slash** | `/done 1` / `/anchor 1` | Tear off note `#1` and archive it |
95
96
  | **Pi Slash** | `/anchor undo` | Put back the most recently torn-off note |
96
97
  | **Terminal CLI** | `anchor` / `anchor "<note>"` | View or stick notes from any shell outside Pi |
@@ -111,12 +112,25 @@ In `v0.4` and `v0.5`, I threw away 60% of the codebase and rebuilt Anchor around
111
112
 
112
113
  ```bash
113
114
  npm run typecheck # Strict tsc --noEmit check (0 errors)
114
- npm test # Native Node test runner (10/10 passing)
115
+ npm test # Native Node test runner (11/11 passing)
115
116
  npm run build # Dual ESM/CJS compilation via tsup + declaration emit
116
117
  ```
117
118
 
118
119
  ---
119
120
 
121
+ ## Ecosystem: Pi Terminal Suite
122
+
123
+ Each extension occupies a distinct terminal slot (`aboveEditor`, `belowEditor`, `status-only` footer, and runtime tool hooks) so all four run together with zero widget overlap:
124
+
125
+ | Package | Terminal Slot & Mechanism | Install |
126
+ | :--- | :--- | :--- |
127
+ | **[`pi-lingual`](https://github.com/3ZEROS12/pi-lingual)** | `二 ⇄ two` `aboveEditor` HUD (≤9 lines, UAX #11 tree layout) & prompt-to-English transformer (0 chat history tokens) | `pi install npm:pi-lingual` |
128
+ | **[`toolflow`](https://github.com/3ZEROS12/toolflow)** | `⌬` Runtime tool hooks — stage-gated MCP mounting & >40-line log dehydration to disk (-95% log tokens) | `pi install npm:toolflow` |
129
+ | **[`pi-tui-status-beautifier`](https://github.com/3ZEROS12/pi-tui-status-beautifier)** | `status-only` footer hook — 6 badge presets, `<80`-col idle folding `(+N idle)`, 0 render-loop disk reads | `pi install npm:pi-tui-status-beautifier` |
130
+ | **[`pi-anchor`](https://github.com/3ZEROS12/anchor)** *(Current)* | `⌖` `belowEditor` 1-line sticky strip, 0ms `pin:` intercept, `[■■□□]` step bar & `↪` session jump | `pi install npm:pi-anchor` |
131
+
132
+ ---
133
+
120
134
  ## License
121
135
 
122
136
  MIT © [Jason Song](https://github.com/3ZEROS12)
package/README_zh.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # ⌖ Anchor
2
2
 
3
- > **贴在 Pi 输入框正下方的电子便签 —— `便签: ...` 0ms 瞬贴上屏、长线任务迷你进度条 `[■■□□ 2/4]`、一键切回原对话上下文 (`↪`),`git commit` 提交代码自动撕便签。**
3
+ > **贴在 Pi 输入框正下方的跨会话电子便签与 `todo` 自动存档点 —— 关终端自动接管未完 `todo` 进度 `[■■□□ 2/5]`,敲 `便签: ...` 0ms 瞬贴支线灵感,敲 `/back` 一键穿越回原对话,`git commit` 自动撕便签。**
4
4
 
5
+ [![npm version](https://img.shields.io/npm/v/pi-anchor?color=blue)](https://www.npmjs.com/package/pi-anchor)
6
+ [![npm downloads](https://img.shields.io/npm/dm/pi-anchor?color=22c55e&label=downloads)](https://www.npmjs.com/package/pi-anchor)
7
+ [![GitHub stars](https://img.shields.io/github/stars/3ZEROS12/anchor?style=flat&color=f59e0b)](https://github.com/3ZEROS12/anchor/stargazers)
5
8
  [![CI 状态](https://github.com/3ZEROS12/anchor/actions/workflows/ci.yml/badge.svg)](https://github.com/3ZEROS12/anchor/actions/workflows/ci.yml)
6
- [![Node v20+](https://img.shields.io/badge/Node-v20+-22c55e.svg)](package.json)
7
- [![TypeScript Strict](https://img.shields.io/badge/TypeScript-Strict-3b82f6.svg)](tsconfig.json)
8
- [![Tests: 10 Passed](https://img.shields.io/badge/Tests-10%20通过-22c55e.svg)](tests/store.test.ts)
9
- [![启动耗时: < 45ms](https://img.shields.io/badge/启动耗时-%3C%2045ms-success.svg)](package.json)
9
+ [![Tests: 11 Passed](https://img.shields.io/badge/Tests-11%20通过-22c55e.svg)](tests/store.test.ts)
10
10
  [![开源协议: MIT](https://img.shields.io/badge/开源协议-MIT-f97316.svg)](LICENSE)
11
11
 
12
12
  [English](./README.md) | **简体中文**
@@ -37,11 +37,11 @@ pi install npm:pi-anchor
37
37
  2. **`TODO.md` 弄脏 Git 且每轮白烧 Token**:在项目根目录维护待办文件,既制造杂乱的 Git 修改记录,又在每一轮对话里反复把旧清单塞进系统提示词。
38
38
  3. **开局闪现横幅一打字就消失**:普通插件只在打开终端时闪一下提示,你发完第一句话它就自我隐藏。接下来两个小时写代码,除非专门敲命令查询,否则根本看不见自己记了什么。
39
39
 
40
- **Anchor 的定位很单纯:贴在 Pi 输入框下边框的一张电子便签**:
41
- - **输入框正下方常驻 1 行 (`belowEditor`)**:便签条固定在输入框下沿,写代码时抬眼可见;输入框上方空间完全留给 `pi-lingual` 等插件;没有便签时自动收起为 0 行。
42
- - **自己随手敲,0ms 瞬贴 (`便签: ...`)**:直接在输入框敲 `便签: 检查网关超时重试`(或 `pin: ...`、`/pin ...`),输入钩子在 `0.00ms` 内直接拦截上屏,不调用大模型、不耗费 1 个 Token,并自动关联当前刚修改过的代码文件。
43
- - **长线任务迷你进度条 (`[■■□□ 2/4]`)**:在便签里写 `[2/4]` 或分步序号 `1. 抽出接口 2. 替换Cookie 3. 补单测`,便签条自动显示 4 格进度条与下一步提示。
44
- - **原对话一键穿越 (`↪ Jump to session`)**:每张便签自动记录创建或施工时的会话文件路径(`sessionFile`)。在 `/anchor` 面板中点击 `↪ Jump to session`,直接切回当时那场对话。
40
+ **Anchor 与单次对话内的 `todo` 形成了天然互补:它是贴在输入框下沿的「跨会话自动存档点 + 随手便签」**:
41
+ - **自动接管未完 `todo`(Auto-Todo Bridge)**:现有的 `todo` 工具(如 `@juicesharp/rpiv-todo` 与内置 `todo`)只存在于单次对话分支中、不写磁盘,一旦关掉终端或敲 `/new` 开启新会话,没跑完的 `todo` 清单就会全部清空。Anchor 会自动监听 `todo` 进度,把未走完的步骤(如 `[■■□□ 2/5]`)与当前修改的文件自动存为跨会话断点;当同一会话内 `todo` 全部完成(`5/5`)时又会自动清理,全程零手动操作。
42
+ - **输入框正下方常驻 1 行 (`belowEditor`)**:便签条固定在输入框下沿,写代码时抬眼可见;输入框上方空间完全留给 `pi-lingual` 与 `rpiv-todo`;没有便签时自动收起为 0 行。
43
+ - **自己随手敲,0ms 瞬贴 (`便签: ...`)**:直接在输入框敲 `便签: 检查网关超时重试`(或 `pin: ...`、`/pin ...`),输入钩子在 `0.00ms` 内直接拦截上屏,不调用大模型、不耗费 1 个 Token。
44
+ - **原对话一键穿越 (`/back` & `↪ Jump to session`)**:每张便签与自动断点都会记录当时的会话文件路径(`sessionFile`)。敲一句 `/back`(或在 `/anchor` 面板中点击 `↪ Jump to session`),直接切回当时那场对话。
45
45
 
46
46
  ---
47
47
 
@@ -49,9 +49,10 @@ pi install npm:pi-anchor
49
49
 
50
50
  | 操作场景 | 你的输入 | 后台执行逻辑 | 界面即时反馈 | 适用时刻 |
51
51
  | :--- | :--- | :--- | :--- | :--- |
52
+ | **未完 `todo` 自动存档** | AI 调用 `todo` 做到 `2/5` 时你关掉终端或敲 `/new` | 自动同步未完 `todo` 步骤、已改文件与 `sessionFile`;若做到 `5/5` 则自动销毁 | 下次打开终端直接显示:`⌖ #1 [■■□□ 2/5] 替换Cookie ▸ 补单测 (↪)` | 弥补单会话 `todo` 关窗即失忆的短板,实现零操作跨会话续传 |
52
53
  | **0ms 随手贴便签** | 输入框敲 `便签: 把 src/auth.ts 改用 Cookie` | `input` 钩子 0ms 拦截,提取文件路径并记录当前 `sessionFile` | 输入框下方立即浮现:`⌖ #1 把 src/auth.ts 改用 Cookie (app · auth.ts ↪)` | 改代码中途冒出支线念头,不想打断当前对话 |
53
54
  | **分步推进长任务** | 敲 `/pin 重构: 1. 接口 2. Cookie 3. 单测` 后敲 `/step 1` | 解析分步列表并在 `~/.anchor/state.json` 推进完成步数 | 下方便签条实时变为:`⌖ #1 [■■□□ 2/3] 重构 ▸ 单测` | 跨天重构、分步排障或刷课程章节 |
54
- | **切回原对话 (`↪`)** | 敲 `/anchor` 选中便签点 `↪ Jump to session`(或 `/anchor jump 1`) | 调用 Pi 原生 `ctx.switchSession(note.sessionFile)` | 终端直接切回贴这张便签时的完整历史对话上下文 | 隔了几天回来继续施工,需要找回当时的讨论细节 |
55
+ | **切回原对话 (`/back`)** | 敲 `/back`(或 `/anchor` 选中便签点 `↪ Jump to session`) | 调用 Pi 原生 `ctx.switchSession(note.sessionFile)` | 终端直接切回贴这张便签时的完整历史对话上下文 | 隔了几天回来继续施工,需要找回当时的讨论细节 |
55
56
  | **提交代码自动撕** | 执行 `git commit -m "feat: 登录改用 Cookie"` 或敲 `/done 1` | 过滤通用停用词后比对提交信息与便签关键词 | 便签自动归档至 `archive.jsonl`,下方便签条同步消失 | 写完代码顺手提交,免去手动打勾销账 |
56
57
 
57
58
  ---
@@ -90,7 +91,7 @@ Anchor 显式指定 `{ placement: "belowEditor" }` 挂载在输入框下方,
90
91
  | **Pi 斜杠命令** | `/pin <内容>` | 贴一张新便签(支持写 `[1/4]` 或 `1. A 2. B 3. C` 自动生成进度条) |
91
92
  | **Pi 斜杠命令** | `/anchor` | 打开交互面板(回车可撕便签、推进度 `+1`、`↪` 切回原对话 Session、看详情) |
92
93
  | **Pi 斜杠命令** | `/step 1` | 将 `#1` 便签的进度条往前推进 `+1` 步 |
93
- | **Pi 斜杠命令** | `/anchor jump 1` | 直接切换回 `#1` 便签绑定的那场历史对话 (`switchSession`) |
94
+ | **Pi 斜杠命令** | `/back`(或 `/anchor jump 1`) | 一键切换回最近一条未完成便签/`todo` 断点对应的历史对话 (`switchSession`) |
94
95
  | **Pi 斜杠命令** | `/done 1` / `/anchor 1` | 按编号 `#1` 直接撕下便签并归档 |
95
96
  | **Pi 斜杠命令** | `/anchor undo` | 把刚刚撕下的便签重新贴回原位 |
96
97
  | **系统命令行** | `anchor` / `anchor "<内容>"` | 不进 Pi 也能在外部终端查看或贴便签 |
@@ -111,12 +112,25 @@ Anchor 显式指定 `{ placement: "belowEditor" }` 挂载在输入框下方,
111
112
 
112
113
  ```bash
113
114
  npm run typecheck # TypeScript 严格类型检查 (0 错误)
114
- npm test # 原生 Node 测试运行器 (10/10 全绿通过)
115
+ npm test # 原生 Node 测试运行器 (11/11 全绿通过)
115
116
  npm run build # tsup 双格式打包与类型声明生成
116
117
  ```
117
118
 
118
119
  ---
119
120
 
121
+ ## 配套生态套件 (Pi Terminal Suite)
122
+
123
+ 四款扩展分别挂载于终端的不同物理槽位(`aboveEditor`、`belowEditor`、`status-only` 底栏与运行时工具钩子),独立运行或组合安装均不产生小部件重叠:
124
+
125
+ | 插件仓库 | 终端挂载槽位与核心机制 | 一键安装 |
126
+ | :--- | :--- | :--- |
127
+ | **[`pi-lingual`](https://github.com/3ZEROS12/pi-lingual)** | `二 ⇄ two` `aboveEditor` 双语伴学视窗(≤9 行 UAX #11 树状排版)与母语提示词转写(0 会话历史 Token 注入) | `pi install npm:pi-lingual` |
128
+ | **[`toolflow`](https://github.com/3ZEROS12/toolflow)** | `⌬` 运行时工具钩子 —— 按阶段动态挂载重型 MCP,>40 行终端长日志自动落盘脱水(削减 95% 日志 Token) | `pi install npm:toolflow` |
129
+ | **[`pi-tui-status-beautifier`](https://github.com/3ZEROS12/pi-tui-status-beautifier)** | `status-only` 底栏微钩子 —— 6 套徽章预设、`<80` 列窄屏折叠 `(+N idle)`、重绘循环 0 磁盘读取 | `pi install npm:pi-tui-status-beautifier` |
130
+ | **[`pi-anchor`](https://github.com/3ZEROS12/anchor)** *(当前)* | `⌖` `belowEditor` 1 行常驻便签条、0ms `pin:` 输入拦截、迷你进度条 `[■■□□]` 与原会话跳转 `↪` | `pi install npm:pi-anchor` |
131
+
132
+ ---
133
+
120
134
  ## License
121
135
 
122
136
  MIT © [Jason Song](https://github.com/3ZEROS12)
@@ -258,10 +258,12 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
258
258
  if (activeNotes.length === 0) return "";
259
259
  const lines = activeNotes.map((a) => {
260
260
  const num = a.id.replace(/^anc-/, "");
261
+ const tag = a.source === "todo-sync" ? `[Todo Checkpoint \xB7 ${a.project}]` : `[${a.project}]`;
261
262
  const files = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(", ")})` : "";
262
- const prog = a.progress ? ` [${a.progress.done}/${a.progress.total}${a.progress.steps && a.progress.steps[a.progress.done] ? ` \xB7 next: ${a.progress.steps[a.progress.done]}` : ""}]` : "";
263
+ const nextStep = a.progress?.steps && a.progress.steps[a.progress.done] && a.progress.steps[a.progress.done] !== a.title ? ` \xB7 current/next: ${a.progress.steps[a.progress.done]}` : "";
264
+ const prog = a.progress ? ` [${a.progress.done}/${a.progress.total}${nextStep}]` : "";
263
265
  const desc = a.description ? ` \u2014 ${a.description}` : "";
264
- return `\u2022 #${num} [${a.project}] ${a.title}${prog}${files}${desc}`;
266
+ return `\u2022 #${num} ${tag} ${a.title}${prog}${files}${desc}`;
265
267
  });
266
268
  return [
267
269
  `<sticky-notes count="${activeNotes.length}">`,
@@ -453,6 +455,7 @@ var AnchorStore = class {
453
455
  description: input.description?.trim(),
454
456
  priority: input.priority || "p1",
455
457
  status: "active",
458
+ ...input.source ? { source: input.source } : {},
456
459
  project: projectName,
457
460
  cwd,
458
461
  createdAt: now,
@@ -560,6 +563,97 @@ var AnchorStore = class {
560
563
  return { anchor: current, completed: nextDone >= total };
561
564
  });
562
565
  }
566
+ /**
567
+ * Clever Touch 4 (Todo-Bridge): Automatically mirror intra-session `todo` tool lists
568
+ * into a cross-session checkpoint!
569
+ * - While tasks are in_progress/pending: updates the live `[■■□□ done/total]` checkpoint on disk;
570
+ * - When all tasks in the todo list reach `completed`: automatically tears off the checkpoint!
571
+ */
572
+ syncFromTodoTool(tasks, options = {}) {
573
+ if (!Array.isArray(tasks)) return { status: "noop" };
574
+ const visible = tasks.filter((t) => t && typeof t.subject === "string" && t.status !== "deleted");
575
+ const normCwd = options.cwd ? normalizePath(options.cwd) : "";
576
+ const normSession = options.sessionFile ? normalizePath(options.sessionFile) : void 0;
577
+ return this.withLock(() => {
578
+ const state = this.loadState();
579
+ const existingIdx = state.anchors.findIndex(
580
+ (a) => a.source === "todo-sync" && (normSession && a.sessionFile === normSession || normCwd && isSameOrSubPath(a.cwd, normCwd))
581
+ );
582
+ const completedTasks = visible.filter((t) => t.status === "completed");
583
+ const unfinishedTasks = visible.filter((t) => t.status !== "completed");
584
+ if (visible.length === 0 || unfinishedTasks.length === 0) {
585
+ if (existingIdx !== -1) {
586
+ const [settled] = state.anchors.splice(existingIdx, 1);
587
+ settled.status = "settled";
588
+ settled.updatedAt = Date.now();
589
+ settled.evidence = {
590
+ settledAt: Date.now(),
591
+ settledBy: "todo-complete",
592
+ summary: `All ${completedTasks.length} session todo(s) completed`
593
+ };
594
+ this.ensureDirs();
595
+ fs.appendFileSync(this.archivePath, JSON.stringify(settled) + "\n", "utf-8");
596
+ this.saveState(state);
597
+ return { status: "completed", anchor: settled };
598
+ }
599
+ return { status: "noop" };
600
+ }
601
+ const inProgress = unfinishedTasks.find((t) => t.status === "in_progress") || unfinishedTasks[0];
602
+ const orderedSteps = [
603
+ ...completedTasks.map((t) => t.subject.trim()),
604
+ ...unfinishedTasks.filter((t) => t.status === "in_progress").map((t) => t.subject.trim()),
605
+ ...unfinishedTasks.filter((t) => t.status === "pending").map((t) => t.subject.trim())
606
+ ];
607
+ const progress = {
608
+ done: completedTasks.length,
609
+ total: visible.length,
610
+ steps: orderedSteps
611
+ };
612
+ const remainingSummary = unfinishedTasks.slice(0, 3).map((t) => t.subject.trim()).join(" \u2192 ");
613
+ const headline = inProgress.subject.trim();
614
+ const now = Date.now();
615
+ if (existingIdx !== -1) {
616
+ const current = state.anchors[existingIdx];
617
+ current.title = headline;
618
+ current.description = `Remaining (${unfinishedTasks.length}/${visible.length}): ${remainingSummary}`;
619
+ current.progress = progress;
620
+ current.status = "active";
621
+ current.updatedAt = now;
622
+ current.lastTouchedAt = now;
623
+ if (normSession) current.sessionFile = normSession;
624
+ if (options.recentFiles && options.recentFiles.length > 0) {
625
+ const merged = Array.from(/* @__PURE__ */ new Set([...options.recentFiles.map((f) => normalizePath(f)), ...current.files])).slice(0, 5);
626
+ current.files = merged;
627
+ }
628
+ state.anchors[existingIdx] = current;
629
+ this.saveState(state);
630
+ return { status: "synced", anchor: current };
631
+ }
632
+ const id = this.generateId(state);
633
+ const inferredFiles = options.recentFiles && options.recentFiles.length > 0 ? options.recentFiles.map((f) => normalizePath(f)).filter(Boolean) : extractPathsFromText(orderedSteps.join(" "));
634
+ const projectName = normCwd ? path.basename(normCwd) : "global";
635
+ const created = {
636
+ id,
637
+ title: headline,
638
+ description: `Remaining (${unfinishedTasks.length}/${visible.length}): ${remainingSummary}`,
639
+ priority: "p1",
640
+ status: "active",
641
+ source: "todo-sync",
642
+ project: projectName,
643
+ cwd: normCwd,
644
+ createdAt: now,
645
+ updatedAt: now,
646
+ lastTouchedAt: now,
647
+ files: inferredFiles,
648
+ tags: ["todo-sync"],
649
+ ...normSession ? { sessionFile: normSession } : {},
650
+ progress
651
+ };
652
+ state.anchors.push(created);
653
+ this.saveState(state);
654
+ return { status: "synced", anchor: created };
655
+ });
656
+ }
563
657
  /**
564
658
  * Tear off (settle) a completed sticky note and archive it to archive.jsonl
565
659
  */
@@ -778,7 +872,8 @@ function updateStartupBanner(ctx, store) {
778
872
  const a = sorted[0];
779
873
  const num = a.id.replace(/^anc-/, "");
780
874
  const bar = a.progress ? `${fg("accent", formatProgressBar(a.progress))} ` : "";
781
- const nextStep = a.progress?.steps && a.progress.steps[a.progress.done] ? fg("muted", ` \u25B8 ${a.progress.steps[a.progress.done]}`) : "";
875
+ const candidateStep = a.progress?.steps ? a.progress.steps[a.progress.done] !== a.title ? a.progress.steps[a.progress.done] : a.progress.steps[a.progress.done + 1] : void 0;
876
+ const nextStep = candidateStep ? fg("muted", ` \u25B8 ${candidateStep}`) : "";
782
877
  const origin = formatOrigin(a);
783
878
  const sessionIcon = a.sessionFile ? " \u21AA" : "";
784
879
  const prefix = `${fg("accent", "\u2316")} ${fg("dim", `#${num}`)} ${bar}`;
@@ -1029,6 +1124,20 @@ ${snippet}` };
1029
1124
  if (event.isError) return;
1030
1125
  const lowerTool = String(event.toolName || "").toLowerCase();
1031
1126
  const currentSessionFile = getSessionFileSafe(ctx);
1127
+ if (lowerTool === "todo") {
1128
+ const tasks = event?.details?.tasks;
1129
+ if (Array.isArray(tasks)) {
1130
+ const syncRes = store.syncFromTodoTool(tasks, {
1131
+ cwd: ctx.cwd,
1132
+ sessionFile: currentSessionFile,
1133
+ recentFiles: recentSessionFiles
1134
+ });
1135
+ if (syncRes.status !== "noop") {
1136
+ updateAnchorStatusBar(ctx, store);
1137
+ }
1138
+ }
1139
+ return;
1140
+ }
1032
1141
  if (lowerTool === "bash" || lowerTool === "powershell") {
1033
1142
  const cmd = String(event.input?.command || "");
1034
1143
  if (cmd.includes("git commit")) {
@@ -1324,6 +1433,35 @@ ${snippet}` };
1324
1433
  await openAnchorDashboard(ctx, store);
1325
1434
  }
1326
1435
  });
1436
+ pi.registerCommand("back", {
1437
+ description: "Jump back to the conversation session of the latest unfinished note or todo checkpoint (/back [id])",
1438
+ handler: async (args, ctx) => {
1439
+ const targetArg = (args || "").trim();
1440
+ const currentSession = getSessionFileSafe(ctx);
1441
+ if (targetArg) {
1442
+ const resolvedId = resolveAnchorTarget(store, targetArg, ctx.cwd);
1443
+ const note = resolvedId ? store.get(resolvedId) : void 0;
1444
+ if (!note || !note.sessionFile || !fs3.existsSync(note.sessionFile)) {
1445
+ ctx.ui.notify(`\u2316 No saved session found for #${targetArg}`, "error");
1446
+ return;
1447
+ }
1448
+ await ctx.switchSession(note.sessionFile);
1449
+ return;
1450
+ }
1451
+ const candidates = sortAnchorsForDisplay(
1452
+ store.list({ cwd: ctx.cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping"),
1453
+ ctx.cwd
1454
+ );
1455
+ const jumpTarget = candidates.find(
1456
+ (a) => a.sessionFile && fs3.existsSync(a.sessionFile) && (!currentSession || !isSameOrSubPath(a.sessionFile, currentSession))
1457
+ );
1458
+ if (!jumpTarget || !jumpTarget.sessionFile) {
1459
+ ctx.ui.notify("\u2316 No other linked conversation session found to jump back to.", "info");
1460
+ return;
1461
+ }
1462
+ await ctx.switchSession(jumpTarget.sessionFile);
1463
+ }
1464
+ });
1327
1465
  }
1328
1466
 
1329
1467
  export {
@@ -16,11 +16,18 @@ interface AnchorProgress {
16
16
  }
17
17
  interface AnchorEvidence {
18
18
  settledAt?: number;
19
- settledBy?: 'commit' | 'manual' | 'one-tap-settlement' | 'manual-command' | 'verification-test';
19
+ settledBy?: 'commit' | 'manual' | 'todo-complete' | 'one-tap-settlement' | 'manual-command' | 'verification-test';
20
20
  commitHash?: string;
21
21
  summary?: string;
22
22
  touchedFiles?: string[];
23
23
  }
24
+ interface TodoTaskItem {
25
+ id: number;
26
+ subject: string;
27
+ status: 'pending' | 'in_progress' | 'completed' | 'deleted';
28
+ activeForm?: string;
29
+ description?: string;
30
+ }
24
31
  interface Anchor {
25
32
  /** Sequential identifier, e.g. 'anc-1' */
26
33
  id: string;
@@ -30,6 +37,8 @@ interface Anchor {
30
37
  description?: string;
31
38
  /** Lifecycle status: 'active' (recent) or 'sleeping' (>7d untouched, 0 prompt tokens) */
32
39
  status: AnchorStatus;
40
+ /** Origin source: 'manual' (user/tool pinned) or 'todo-sync' (auto-bridged from unfinished session todos) */
41
+ source?: 'manual' | 'todo-sync';
33
42
  /** Short workspace label (e.g. 'anchor', 'College/优化', or 'global') */
34
43
  project: string;
35
44
  /** Workspace directory path where the note applies */
@@ -153,6 +162,7 @@ declare class AnchorStore {
153
162
  title: string;
154
163
  description?: string;
155
164
  priority?: AnchorPriority;
165
+ source?: 'manual' | 'todo-sync';
156
166
  cwd?: string;
157
167
  project?: string;
158
168
  files?: string[];
@@ -178,6 +188,20 @@ declare class AnchorStore {
178
188
  anchor: Anchor;
179
189
  completed: boolean;
180
190
  };
191
+ /**
192
+ * Clever Touch 4 (Todo-Bridge): Automatically mirror intra-session `todo` tool lists
193
+ * into a cross-session checkpoint!
194
+ * - While tasks are in_progress/pending: updates the live `[■■□□ done/total]` checkpoint on disk;
195
+ * - When all tasks in the todo list reach `completed`: automatically tears off the checkpoint!
196
+ */
197
+ syncFromTodoTool(tasks: TodoTaskItem[], options?: {
198
+ cwd?: string;
199
+ sessionFile?: string;
200
+ recentFiles?: string[];
201
+ }): {
202
+ status: 'synced' | 'completed' | 'noop';
203
+ anchor?: Anchor;
204
+ };
181
205
  /**
182
206
  * Tear off (settle) a completed sticky note and archive it to archive.jsonl
183
207
  */
@@ -207,4 +231,4 @@ declare function parseQuickNoteInput(text: string): string | null;
207
231
  declare function resolveAnchorTarget(store: AnchorStore, idOrIndex: string, cwd?: string): string | undefined;
208
232
  declare function export_default(pi: ExtensionAPI): void;
209
233
 
210
- export { type Anchor as A, COMMENT_FORMATS as C, MS_PER_DAY as M, QUIET_AFTER_DAYS as Q, type TouchMatchResult as T, type AnchorProgress as a, AnchorStore as b, type AnchorEvidence as c, type AnchorPriority as d, type AnchorStatus as e, type AnchorStoreState as f, acquireSyncLock as g, atomicRenameWithRetry as h, extractPathsFromText as i, extractProgressFromText as j, findMatchedAnchors as k, getDefaultStorageDir as l, globToRegExp as m, isAnchorTargetingWorkspace as n, isSameOrSubPath as o, makeSafeTaskAnnotation as p, matchAnchorAgainstTouchedFiles as q, matchesCommitMessage as r, normalizePath as s, parseAnchorCommandInput as t, parseQuickNoteInput as u, renderActiveAnchorsContext as v, renderColdStartAnchorsContext as w, resolveAnchorTarget as x, sweepQuietNotes as y, export_default as z };
234
+ export { type Anchor as A, export_default as B, COMMENT_FORMATS as C, MS_PER_DAY as M, QUIET_AFTER_DAYS as Q, type TodoTaskItem as T, type AnchorProgress as a, AnchorStore as b, type AnchorEvidence as c, type AnchorPriority as d, type AnchorStatus as e, type AnchorStoreState as f, type TouchMatchResult as g, acquireSyncLock as h, atomicRenameWithRetry as i, extractPathsFromText as j, extractProgressFromText as k, findMatchedAnchors as l, getDefaultStorageDir as m, globToRegExp as n, isAnchorTargetingWorkspace as o, isSameOrSubPath as p, makeSafeTaskAnnotation as q, matchAnchorAgainstTouchedFiles as r, matchesCommitMessage as s, normalizePath as t, parseAnchorCommandInput as u, parseQuickNoteInput as v, renderActiveAnchorsContext as w, renderColdStartAnchorsContext as x, resolveAnchorTarget as y, sweepQuietNotes as z };
@@ -16,11 +16,18 @@ interface AnchorProgress {
16
16
  }
17
17
  interface AnchorEvidence {
18
18
  settledAt?: number;
19
- settledBy?: 'commit' | 'manual' | 'one-tap-settlement' | 'manual-command' | 'verification-test';
19
+ settledBy?: 'commit' | 'manual' | 'todo-complete' | 'one-tap-settlement' | 'manual-command' | 'verification-test';
20
20
  commitHash?: string;
21
21
  summary?: string;
22
22
  touchedFiles?: string[];
23
23
  }
24
+ interface TodoTaskItem {
25
+ id: number;
26
+ subject: string;
27
+ status: 'pending' | 'in_progress' | 'completed' | 'deleted';
28
+ activeForm?: string;
29
+ description?: string;
30
+ }
24
31
  interface Anchor {
25
32
  /** Sequential identifier, e.g. 'anc-1' */
26
33
  id: string;
@@ -30,6 +37,8 @@ interface Anchor {
30
37
  description?: string;
31
38
  /** Lifecycle status: 'active' (recent) or 'sleeping' (>7d untouched, 0 prompt tokens) */
32
39
  status: AnchorStatus;
40
+ /** Origin source: 'manual' (user/tool pinned) or 'todo-sync' (auto-bridged from unfinished session todos) */
41
+ source?: 'manual' | 'todo-sync';
33
42
  /** Short workspace label (e.g. 'anchor', 'College/优化', or 'global') */
34
43
  project: string;
35
44
  /** Workspace directory path where the note applies */
@@ -153,6 +162,7 @@ declare class AnchorStore {
153
162
  title: string;
154
163
  description?: string;
155
164
  priority?: AnchorPriority;
165
+ source?: 'manual' | 'todo-sync';
156
166
  cwd?: string;
157
167
  project?: string;
158
168
  files?: string[];
@@ -178,6 +188,20 @@ declare class AnchorStore {
178
188
  anchor: Anchor;
179
189
  completed: boolean;
180
190
  };
191
+ /**
192
+ * Clever Touch 4 (Todo-Bridge): Automatically mirror intra-session `todo` tool lists
193
+ * into a cross-session checkpoint!
194
+ * - While tasks are in_progress/pending: updates the live `[■■□□ done/total]` checkpoint on disk;
195
+ * - When all tasks in the todo list reach `completed`: automatically tears off the checkpoint!
196
+ */
197
+ syncFromTodoTool(tasks: TodoTaskItem[], options?: {
198
+ cwd?: string;
199
+ sessionFile?: string;
200
+ recentFiles?: string[];
201
+ }): {
202
+ status: 'synced' | 'completed' | 'noop';
203
+ anchor?: Anchor;
204
+ };
181
205
  /**
182
206
  * Tear off (settle) a completed sticky note and archive it to archive.jsonl
183
207
  */
@@ -207,4 +231,4 @@ declare function parseQuickNoteInput(text: string): string | null;
207
231
  declare function resolveAnchorTarget(store: AnchorStore, idOrIndex: string, cwd?: string): string | undefined;
208
232
  declare function export_default(pi: ExtensionAPI): void;
209
233
 
210
- export { type Anchor as A, COMMENT_FORMATS as C, MS_PER_DAY as M, QUIET_AFTER_DAYS as Q, type TouchMatchResult as T, type AnchorProgress as a, AnchorStore as b, type AnchorEvidence as c, type AnchorPriority as d, type AnchorStatus as e, type AnchorStoreState as f, acquireSyncLock as g, atomicRenameWithRetry as h, extractPathsFromText as i, extractProgressFromText as j, findMatchedAnchors as k, getDefaultStorageDir as l, globToRegExp as m, isAnchorTargetingWorkspace as n, isSameOrSubPath as o, makeSafeTaskAnnotation as p, matchAnchorAgainstTouchedFiles as q, matchesCommitMessage as r, normalizePath as s, parseAnchorCommandInput as t, parseQuickNoteInput as u, renderActiveAnchorsContext as v, renderColdStartAnchorsContext as w, resolveAnchorTarget as x, sweepQuietNotes as y, export_default as z };
234
+ export { type Anchor as A, export_default as B, COMMENT_FORMATS as C, MS_PER_DAY as M, QUIET_AFTER_DAYS as Q, type TodoTaskItem as T, type AnchorProgress as a, AnchorStore as b, type AnchorEvidence as c, type AnchorPriority as d, type AnchorStatus as e, type AnchorStoreState as f, type TouchMatchResult as g, acquireSyncLock as h, atomicRenameWithRetry as i, extractPathsFromText as j, extractProgressFromText as k, findMatchedAnchors as l, getDefaultStorageDir as m, globToRegExp as n, isAnchorTargetingWorkspace as o, isSameOrSubPath as p, makeSafeTaskAnnotation as q, matchAnchorAgainstTouchedFiles as r, matchesCommitMessage as s, normalizePath as t, parseAnchorCommandInput as u, parseQuickNoteInput as v, renderActiveAnchorsContext as w, renderColdStartAnchorsContext as x, resolveAnchorTarget as y, sweepQuietNotes as z };
@@ -295,10 +295,12 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
295
295
  if (activeNotes.length === 0) return "";
296
296
  const lines = activeNotes.map((a) => {
297
297
  const num = a.id.replace(/^anc-/, "");
298
+ const tag = a.source === "todo-sync" ? `[Todo Checkpoint \xB7 ${a.project}]` : `[${a.project}]`;
298
299
  const files = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(", ")})` : "";
299
- const prog = a.progress ? ` [${a.progress.done}/${a.progress.total}${a.progress.steps && a.progress.steps[a.progress.done] ? ` \xB7 next: ${a.progress.steps[a.progress.done]}` : ""}]` : "";
300
+ const nextStep = a.progress?.steps && a.progress.steps[a.progress.done] && a.progress.steps[a.progress.done] !== a.title ? ` \xB7 current/next: ${a.progress.steps[a.progress.done]}` : "";
301
+ const prog = a.progress ? ` [${a.progress.done}/${a.progress.total}${nextStep}]` : "";
300
302
  const desc = a.description ? ` \u2014 ${a.description}` : "";
301
- return `\u2022 #${num} [${a.project}] ${a.title}${prog}${files}${desc}`;
303
+ return `\u2022 #${num} ${tag} ${a.title}${prog}${files}${desc}`;
302
304
  });
303
305
  return [
304
306
  `<sticky-notes count="${activeNotes.length}">`,
@@ -489,6 +491,7 @@ var AnchorStore = class {
489
491
  description: input.description?.trim(),
490
492
  priority: input.priority || "p1",
491
493
  status: "active",
494
+ ...input.source ? { source: input.source } : {},
492
495
  project: projectName,
493
496
  cwd,
494
497
  createdAt: now,
@@ -596,6 +599,97 @@ var AnchorStore = class {
596
599
  return { anchor: current, completed: nextDone >= total };
597
600
  });
598
601
  }
602
+ /**
603
+ * Clever Touch 4 (Todo-Bridge): Automatically mirror intra-session `todo` tool lists
604
+ * into a cross-session checkpoint!
605
+ * - While tasks are in_progress/pending: updates the live `[■■□□ done/total]` checkpoint on disk;
606
+ * - When all tasks in the todo list reach `completed`: automatically tears off the checkpoint!
607
+ */
608
+ syncFromTodoTool(tasks, options = {}) {
609
+ if (!Array.isArray(tasks)) return { status: "noop" };
610
+ const visible = tasks.filter((t) => t && typeof t.subject === "string" && t.status !== "deleted");
611
+ const normCwd = options.cwd ? normalizePath(options.cwd) : "";
612
+ const normSession = options.sessionFile ? normalizePath(options.sessionFile) : void 0;
613
+ return this.withLock(() => {
614
+ const state = this.loadState();
615
+ const existingIdx = state.anchors.findIndex(
616
+ (a) => a.source === "todo-sync" && (normSession && a.sessionFile === normSession || normCwd && isSameOrSubPath(a.cwd, normCwd))
617
+ );
618
+ const completedTasks = visible.filter((t) => t.status === "completed");
619
+ const unfinishedTasks = visible.filter((t) => t.status !== "completed");
620
+ if (visible.length === 0 || unfinishedTasks.length === 0) {
621
+ if (existingIdx !== -1) {
622
+ const [settled] = state.anchors.splice(existingIdx, 1);
623
+ settled.status = "settled";
624
+ settled.updatedAt = Date.now();
625
+ settled.evidence = {
626
+ settledAt: Date.now(),
627
+ settledBy: "todo-complete",
628
+ summary: `All ${completedTasks.length} session todo(s) completed`
629
+ };
630
+ this.ensureDirs();
631
+ import_node_fs.default.appendFileSync(this.archivePath, JSON.stringify(settled) + "\n", "utf-8");
632
+ this.saveState(state);
633
+ return { status: "completed", anchor: settled };
634
+ }
635
+ return { status: "noop" };
636
+ }
637
+ const inProgress = unfinishedTasks.find((t) => t.status === "in_progress") || unfinishedTasks[0];
638
+ const orderedSteps = [
639
+ ...completedTasks.map((t) => t.subject.trim()),
640
+ ...unfinishedTasks.filter((t) => t.status === "in_progress").map((t) => t.subject.trim()),
641
+ ...unfinishedTasks.filter((t) => t.status === "pending").map((t) => t.subject.trim())
642
+ ];
643
+ const progress = {
644
+ done: completedTasks.length,
645
+ total: visible.length,
646
+ steps: orderedSteps
647
+ };
648
+ const remainingSummary = unfinishedTasks.slice(0, 3).map((t) => t.subject.trim()).join(" \u2192 ");
649
+ const headline = inProgress.subject.trim();
650
+ const now = Date.now();
651
+ if (existingIdx !== -1) {
652
+ const current = state.anchors[existingIdx];
653
+ current.title = headline;
654
+ current.description = `Remaining (${unfinishedTasks.length}/${visible.length}): ${remainingSummary}`;
655
+ current.progress = progress;
656
+ current.status = "active";
657
+ current.updatedAt = now;
658
+ current.lastTouchedAt = now;
659
+ if (normSession) current.sessionFile = normSession;
660
+ if (options.recentFiles && options.recentFiles.length > 0) {
661
+ const merged = Array.from(/* @__PURE__ */ new Set([...options.recentFiles.map((f) => normalizePath(f)), ...current.files])).slice(0, 5);
662
+ current.files = merged;
663
+ }
664
+ state.anchors[existingIdx] = current;
665
+ this.saveState(state);
666
+ return { status: "synced", anchor: current };
667
+ }
668
+ const id = this.generateId(state);
669
+ const inferredFiles = options.recentFiles && options.recentFiles.length > 0 ? options.recentFiles.map((f) => normalizePath(f)).filter(Boolean) : extractPathsFromText(orderedSteps.join(" "));
670
+ const projectName = normCwd ? import_node_path.default.basename(normCwd) : "global";
671
+ const created = {
672
+ id,
673
+ title: headline,
674
+ description: `Remaining (${unfinishedTasks.length}/${visible.length}): ${remainingSummary}`,
675
+ priority: "p1",
676
+ status: "active",
677
+ source: "todo-sync",
678
+ project: projectName,
679
+ cwd: normCwd,
680
+ createdAt: now,
681
+ updatedAt: now,
682
+ lastTouchedAt: now,
683
+ files: inferredFiles,
684
+ tags: ["todo-sync"],
685
+ ...normSession ? { sessionFile: normSession } : {},
686
+ progress
687
+ };
688
+ state.anchors.push(created);
689
+ this.saveState(state);
690
+ return { status: "synced", anchor: created };
691
+ });
692
+ }
599
693
  /**
600
694
  * Tear off (settle) a completed sticky note and archive it to archive.jsonl
601
695
  */
@@ -814,7 +908,8 @@ function updateStartupBanner(ctx, store) {
814
908
  const a = sorted[0];
815
909
  const num = a.id.replace(/^anc-/, "");
816
910
  const bar = a.progress ? `${fg("accent", formatProgressBar(a.progress))} ` : "";
817
- const nextStep = a.progress?.steps && a.progress.steps[a.progress.done] ? fg("muted", ` \u25B8 ${a.progress.steps[a.progress.done]}`) : "";
911
+ const candidateStep = a.progress?.steps ? a.progress.steps[a.progress.done] !== a.title ? a.progress.steps[a.progress.done] : a.progress.steps[a.progress.done + 1] : void 0;
912
+ const nextStep = candidateStep ? fg("muted", ` \u25B8 ${candidateStep}`) : "";
818
913
  const origin = formatOrigin(a);
819
914
  const sessionIcon = a.sessionFile ? " \u21AA" : "";
820
915
  const prefix = `${fg("accent", "\u2316")} ${fg("dim", `#${num}`)} ${bar}`;
@@ -1065,6 +1160,20 @@ ${snippet}` };
1065
1160
  if (event.isError) return;
1066
1161
  const lowerTool = String(event.toolName || "").toLowerCase();
1067
1162
  const currentSessionFile = getSessionFileSafe(ctx);
1163
+ if (lowerTool === "todo") {
1164
+ const tasks = event?.details?.tasks;
1165
+ if (Array.isArray(tasks)) {
1166
+ const syncRes = store.syncFromTodoTool(tasks, {
1167
+ cwd: ctx.cwd,
1168
+ sessionFile: currentSessionFile,
1169
+ recentFiles: recentSessionFiles
1170
+ });
1171
+ if (syncRes.status !== "noop") {
1172
+ updateAnchorStatusBar(ctx, store);
1173
+ }
1174
+ }
1175
+ return;
1176
+ }
1068
1177
  if (lowerTool === "bash" || lowerTool === "powershell") {
1069
1178
  const cmd = String(event.input?.command || "");
1070
1179
  if (cmd.includes("git commit")) {
@@ -1360,6 +1469,35 @@ ${snippet}` };
1360
1469
  await openAnchorDashboard(ctx, store);
1361
1470
  }
1362
1471
  });
1472
+ pi.registerCommand("back", {
1473
+ description: "Jump back to the conversation session of the latest unfinished note or todo checkpoint (/back [id])",
1474
+ handler: async (args, ctx) => {
1475
+ const targetArg = (args || "").trim();
1476
+ const currentSession = getSessionFileSafe(ctx);
1477
+ if (targetArg) {
1478
+ const resolvedId = resolveAnchorTarget(store, targetArg, ctx.cwd);
1479
+ const note = resolvedId ? store.get(resolvedId) : void 0;
1480
+ if (!note || !note.sessionFile || !import_node_fs3.default.existsSync(note.sessionFile)) {
1481
+ ctx.ui.notify(`\u2316 No saved session found for #${targetArg}`, "error");
1482
+ return;
1483
+ }
1484
+ await ctx.switchSession(note.sessionFile);
1485
+ return;
1486
+ }
1487
+ const candidates = sortAnchorsForDisplay(
1488
+ store.list({ cwd: ctx.cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping"),
1489
+ ctx.cwd
1490
+ );
1491
+ const jumpTarget = candidates.find(
1492
+ (a) => a.sessionFile && import_node_fs3.default.existsSync(a.sessionFile) && (!currentSession || !isSameOrSubPath(a.sessionFile, currentSession))
1493
+ );
1494
+ if (!jumpTarget || !jumpTarget.sessionFile) {
1495
+ ctx.ui.notify("\u2316 No other linked conversation session found to jump back to.", "info");
1496
+ return;
1497
+ }
1498
+ await ctx.switchSession(jumpTarget.sessionFile);
1499
+ }
1500
+ });
1363
1501
  }
1364
1502
  // Annotate the CommonJS export names for ESM import in node:
1365
1503
  0 && (module.exports = {
@@ -1,2 +1,2 @@
1
1
  import '@earendil-works/pi-coding-agent';
2
- export { z as default, t as parseAnchorCommandInput, u as parseQuickNoteInput, x as resolveAnchorTarget } from './extension-CvTgSv1L.cjs';
2
+ export { B as default, u as parseAnchorCommandInput, v as parseQuickNoteInput, y as resolveAnchorTarget } from './extension-09AVvgl-.cjs';
@@ -1,2 +1,2 @@
1
1
  import '@earendil-works/pi-coding-agent';
2
- export { z as default, t as parseAnchorCommandInput, u as parseQuickNoteInput, x as resolveAnchorTarget } from './extension-CvTgSv1L.js';
2
+ export { B as default, u as parseAnchorCommandInput, v as parseQuickNoteInput, y as resolveAnchorTarget } from './extension-09AVvgl-.js';
package/dist/extension.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  parseAnchorCommandInput,
4
4
  parseQuickNoteInput,
5
5
  resolveAnchorTarget
6
- } from "./chunk-XL3YH2AF.js";
6
+ } from "./chunk-6NYXKR4F.js";
7
7
  export {
8
8
  extension_default as default,
9
9
  parseAnchorCommandInput,
package/dist/index.cjs CHANGED
@@ -322,10 +322,12 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
322
322
  if (activeNotes.length === 0) return "";
323
323
  const lines = activeNotes.map((a) => {
324
324
  const num = a.id.replace(/^anc-/, "");
325
+ const tag = a.source === "todo-sync" ? `[Todo Checkpoint \xB7 ${a.project}]` : `[${a.project}]`;
325
326
  const files = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(", ")})` : "";
326
- const prog = a.progress ? ` [${a.progress.done}/${a.progress.total}${a.progress.steps && a.progress.steps[a.progress.done] ? ` \xB7 next: ${a.progress.steps[a.progress.done]}` : ""}]` : "";
327
+ const nextStep = a.progress?.steps && a.progress.steps[a.progress.done] && a.progress.steps[a.progress.done] !== a.title ? ` \xB7 current/next: ${a.progress.steps[a.progress.done]}` : "";
328
+ const prog = a.progress ? ` [${a.progress.done}/${a.progress.total}${nextStep}]` : "";
327
329
  const desc = a.description ? ` \u2014 ${a.description}` : "";
328
- return `\u2022 #${num} [${a.project}] ${a.title}${prog}${files}${desc}`;
330
+ return `\u2022 #${num} ${tag} ${a.title}${prog}${files}${desc}`;
329
331
  });
330
332
  return [
331
333
  `<sticky-notes count="${activeNotes.length}">`,
@@ -517,6 +519,7 @@ var AnchorStore = class {
517
519
  description: input.description?.trim(),
518
520
  priority: input.priority || "p1",
519
521
  status: "active",
522
+ ...input.source ? { source: input.source } : {},
520
523
  project: projectName,
521
524
  cwd,
522
525
  createdAt: now,
@@ -624,6 +627,97 @@ var AnchorStore = class {
624
627
  return { anchor: current, completed: nextDone >= total };
625
628
  });
626
629
  }
630
+ /**
631
+ * Clever Touch 4 (Todo-Bridge): Automatically mirror intra-session `todo` tool lists
632
+ * into a cross-session checkpoint!
633
+ * - While tasks are in_progress/pending: updates the live `[■■□□ done/total]` checkpoint on disk;
634
+ * - When all tasks in the todo list reach `completed`: automatically tears off the checkpoint!
635
+ */
636
+ syncFromTodoTool(tasks, options = {}) {
637
+ if (!Array.isArray(tasks)) return { status: "noop" };
638
+ const visible = tasks.filter((t) => t && typeof t.subject === "string" && t.status !== "deleted");
639
+ const normCwd = options.cwd ? normalizePath(options.cwd) : "";
640
+ const normSession = options.sessionFile ? normalizePath(options.sessionFile) : void 0;
641
+ return this.withLock(() => {
642
+ const state = this.loadState();
643
+ const existingIdx = state.anchors.findIndex(
644
+ (a) => a.source === "todo-sync" && (normSession && a.sessionFile === normSession || normCwd && isSameOrSubPath(a.cwd, normCwd))
645
+ );
646
+ const completedTasks = visible.filter((t) => t.status === "completed");
647
+ const unfinishedTasks = visible.filter((t) => t.status !== "completed");
648
+ if (visible.length === 0 || unfinishedTasks.length === 0) {
649
+ if (existingIdx !== -1) {
650
+ const [settled] = state.anchors.splice(existingIdx, 1);
651
+ settled.status = "settled";
652
+ settled.updatedAt = Date.now();
653
+ settled.evidence = {
654
+ settledAt: Date.now(),
655
+ settledBy: "todo-complete",
656
+ summary: `All ${completedTasks.length} session todo(s) completed`
657
+ };
658
+ this.ensureDirs();
659
+ import_node_fs.default.appendFileSync(this.archivePath, JSON.stringify(settled) + "\n", "utf-8");
660
+ this.saveState(state);
661
+ return { status: "completed", anchor: settled };
662
+ }
663
+ return { status: "noop" };
664
+ }
665
+ const inProgress = unfinishedTasks.find((t) => t.status === "in_progress") || unfinishedTasks[0];
666
+ const orderedSteps = [
667
+ ...completedTasks.map((t) => t.subject.trim()),
668
+ ...unfinishedTasks.filter((t) => t.status === "in_progress").map((t) => t.subject.trim()),
669
+ ...unfinishedTasks.filter((t) => t.status === "pending").map((t) => t.subject.trim())
670
+ ];
671
+ const progress = {
672
+ done: completedTasks.length,
673
+ total: visible.length,
674
+ steps: orderedSteps
675
+ };
676
+ const remainingSummary = unfinishedTasks.slice(0, 3).map((t) => t.subject.trim()).join(" \u2192 ");
677
+ const headline = inProgress.subject.trim();
678
+ const now = Date.now();
679
+ if (existingIdx !== -1) {
680
+ const current = state.anchors[existingIdx];
681
+ current.title = headline;
682
+ current.description = `Remaining (${unfinishedTasks.length}/${visible.length}): ${remainingSummary}`;
683
+ current.progress = progress;
684
+ current.status = "active";
685
+ current.updatedAt = now;
686
+ current.lastTouchedAt = now;
687
+ if (normSession) current.sessionFile = normSession;
688
+ if (options.recentFiles && options.recentFiles.length > 0) {
689
+ const merged = Array.from(/* @__PURE__ */ new Set([...options.recentFiles.map((f) => normalizePath(f)), ...current.files])).slice(0, 5);
690
+ current.files = merged;
691
+ }
692
+ state.anchors[existingIdx] = current;
693
+ this.saveState(state);
694
+ return { status: "synced", anchor: current };
695
+ }
696
+ const id = this.generateId(state);
697
+ const inferredFiles = options.recentFiles && options.recentFiles.length > 0 ? options.recentFiles.map((f) => normalizePath(f)).filter(Boolean) : extractPathsFromText(orderedSteps.join(" "));
698
+ const projectName = normCwd ? import_node_path.default.basename(normCwd) : "global";
699
+ const created = {
700
+ id,
701
+ title: headline,
702
+ description: `Remaining (${unfinishedTasks.length}/${visible.length}): ${remainingSummary}`,
703
+ priority: "p1",
704
+ status: "active",
705
+ source: "todo-sync",
706
+ project: projectName,
707
+ cwd: normCwd,
708
+ createdAt: now,
709
+ updatedAt: now,
710
+ lastTouchedAt: now,
711
+ files: inferredFiles,
712
+ tags: ["todo-sync"],
713
+ ...normSession ? { sessionFile: normSession } : {},
714
+ progress
715
+ };
716
+ state.anchors.push(created);
717
+ this.saveState(state);
718
+ return { status: "synced", anchor: created };
719
+ });
720
+ }
627
721
  /**
628
722
  * Tear off (settle) a completed sticky note and archive it to archive.jsonl
629
723
  */
@@ -842,7 +936,8 @@ function updateStartupBanner(ctx, store) {
842
936
  const a = sorted[0];
843
937
  const num = a.id.replace(/^anc-/, "");
844
938
  const bar = a.progress ? `${fg("accent", formatProgressBar(a.progress))} ` : "";
845
- const nextStep = a.progress?.steps && a.progress.steps[a.progress.done] ? fg("muted", ` \u25B8 ${a.progress.steps[a.progress.done]}`) : "";
939
+ const candidateStep = a.progress?.steps ? a.progress.steps[a.progress.done] !== a.title ? a.progress.steps[a.progress.done] : a.progress.steps[a.progress.done + 1] : void 0;
940
+ const nextStep = candidateStep ? fg("muted", ` \u25B8 ${candidateStep}`) : "";
846
941
  const origin = formatOrigin(a);
847
942
  const sessionIcon = a.sessionFile ? " \u21AA" : "";
848
943
  const prefix = `${fg("accent", "\u2316")} ${fg("dim", `#${num}`)} ${bar}`;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { A as Anchor, a as AnchorProgress, b as AnchorStore } from './extension-CvTgSv1L.cjs';
2
- export { c as AnchorEvidence, d as AnchorPriority, e as AnchorStatus, f as AnchorStoreState, C as COMMENT_FORMATS, M as MS_PER_DAY, Q as QUIET_AFTER_DAYS, T as TouchMatchResult, g as acquireSyncLock, h as atomicRenameWithRetry, i as extractPathsFromText, j as extractProgressFromText, k as findMatchedAnchors, l as getDefaultStorageDir, m as globToRegExp, n as isAnchorTargetingWorkspace, o as isSameOrSubPath, p as makeSafeTaskAnnotation, q as matchAnchorAgainstTouchedFiles, r as matchesCommitMessage, s as normalizePath, t as parseAnchorCommandInput, u as parseQuickNoteInput, v as renderActiveAnchorsContext, w as renderColdStartAnchorsContext, x as resolveAnchorTarget, y as sweepQuietNotes } from './extension-CvTgSv1L.cjs';
1
+ import { A as Anchor, a as AnchorProgress, b as AnchorStore } from './extension-09AVvgl-.cjs';
2
+ export { c as AnchorEvidence, d as AnchorPriority, e as AnchorStatus, f as AnchorStoreState, C as COMMENT_FORMATS, M as MS_PER_DAY, Q as QUIET_AFTER_DAYS, T as TodoTaskItem, g as TouchMatchResult, h as acquireSyncLock, i as atomicRenameWithRetry, j as extractPathsFromText, k as extractProgressFromText, l as findMatchedAnchors, m as getDefaultStorageDir, n as globToRegExp, o as isAnchorTargetingWorkspace, p as isSameOrSubPath, q as makeSafeTaskAnnotation, r as matchAnchorAgainstTouchedFiles, s as matchesCommitMessage, t as normalizePath, u as parseAnchorCommandInput, v as parseQuickNoteInput, w as renderActiveAnchorsContext, x as renderColdStartAnchorsContext, y as resolveAnchorTarget, z as sweepQuietNotes } from './extension-09AVvgl-.cjs';
3
3
  import { ExtensionContext, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
4
4
 
5
5
  declare function stripAnsi(str: string): string;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { A as Anchor, a as AnchorProgress, b as AnchorStore } from './extension-CvTgSv1L.js';
2
- export { c as AnchorEvidence, d as AnchorPriority, e as AnchorStatus, f as AnchorStoreState, C as COMMENT_FORMATS, M as MS_PER_DAY, Q as QUIET_AFTER_DAYS, T as TouchMatchResult, g as acquireSyncLock, h as atomicRenameWithRetry, i as extractPathsFromText, j as extractProgressFromText, k as findMatchedAnchors, l as getDefaultStorageDir, m as globToRegExp, n as isAnchorTargetingWorkspace, o as isSameOrSubPath, p as makeSafeTaskAnnotation, q as matchAnchorAgainstTouchedFiles, r as matchesCommitMessage, s as normalizePath, t as parseAnchorCommandInput, u as parseQuickNoteInput, v as renderActiveAnchorsContext, w as renderColdStartAnchorsContext, x as resolveAnchorTarget, y as sweepQuietNotes } from './extension-CvTgSv1L.js';
1
+ import { A as Anchor, a as AnchorProgress, b as AnchorStore } from './extension-09AVvgl-.js';
2
+ export { c as AnchorEvidence, d as AnchorPriority, e as AnchorStatus, f as AnchorStoreState, C as COMMENT_FORMATS, M as MS_PER_DAY, Q as QUIET_AFTER_DAYS, T as TodoTaskItem, g as TouchMatchResult, h as acquireSyncLock, i as atomicRenameWithRetry, j as extractPathsFromText, k as extractProgressFromText, l as findMatchedAnchors, m as getDefaultStorageDir, n as globToRegExp, o as isAnchorTargetingWorkspace, p as isSameOrSubPath, q as makeSafeTaskAnnotation, r as matchAnchorAgainstTouchedFiles, s as matchesCommitMessage, t as normalizePath, u as parseAnchorCommandInput, v as parseQuickNoteInput, w as renderActiveAnchorsContext, x as renderColdStartAnchorsContext, y as resolveAnchorTarget, z as sweepQuietNotes } from './extension-09AVvgl-.js';
3
3
  import { ExtensionContext, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
4
4
 
5
5
  declare function stripAnsi(str: string): string;
package/dist/index.js CHANGED
@@ -34,7 +34,7 @@ import {
34
34
  truncateToWidth,
35
35
  updateAnchorStatusBar,
36
36
  updateStartupBanner
37
- } from "./chunk-XL3YH2AF.js";
37
+ } from "./chunk-6NYXKR4F.js";
38
38
  export {
39
39
  AnchorStore,
40
40
  COMMENT_FORMATS,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-anchor",
3
- "version": "0.5.1",
4
- "description": "A digital sticky note docked below your Pi editor — stick notes in 0ms (pin: ...), track long tasks with micro progress bars [■■□□], jump back to the original session (↪), and auto-tear off on git commit.",
3
+ "version": "0.6.1",
4
+ "description": "Cross-session sticky note & auto-todo checkpoint for Pi — automatically saves unfinished todos across terminal restarts, docks a 1-line progress bar [■■□□] below your editor, jumps back to the original session (/back), and auto-clears on git commit.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",
@@ -12,6 +12,7 @@
12
12
  "bin",
13
13
  "assets",
14
14
  "README.md",
15
+ "README_ja.md",
15
16
  "README_zh.md",
16
17
  "LICENSE"
17
18
  ],
package/src/extension.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  AnchorStore,
6
6
  findMatchedAnchors,
7
7
  isAnchorTargetingWorkspace,
8
+ isSameOrSubPath,
8
9
  makeSafeTaskAnnotation,
9
10
  matchesCommitMessage,
10
11
  normalizePath,
@@ -193,15 +194,33 @@ export default function (pi: ExtensionAPI) {
193
194
  });
194
195
 
195
196
  // 4. Clever Touches on Tool Execution:
196
- // (a) Auto-tear off sticky notes on matching `git commit`
197
- // (b) Slip a 1-line comment whisper when reading/editing a matching file & bind current sessionFile
197
+ // (a) Auto-Bridge for `todo` tool (`@juicesharp/rpiv-todo` / built-in `todo`):
198
+ // Mirrors unfinished session todos into a persistent cross-session checkpoint, and auto-clears when 100% done!
199
+ // (b) Auto-tear off sticky notes on matching `git commit`
200
+ // (c) Slip a 1-line comment whisper when reading/editing a matching file & bind current sessionFile
198
201
  pi.on('tool_result', async (event: any, ctx: ExtensionContext) => {
199
202
  if (event.isError) return;
200
203
 
201
204
  const lowerTool = String(event.toolName || '').toLowerCase();
202
205
  const currentSessionFile = getSessionFileSafe(ctx);
203
206
 
204
- // (a) Real-time auto-tear on `git commit -m "..."`
207
+ // (a) Auto-Bridge for intra-session `todo` tool
208
+ if (lowerTool === 'todo') {
209
+ const tasks = event?.details?.tasks;
210
+ if (Array.isArray(tasks)) {
211
+ const syncRes = store.syncFromTodoTool(tasks, {
212
+ cwd: ctx.cwd,
213
+ sessionFile: currentSessionFile,
214
+ recentFiles: recentSessionFiles
215
+ });
216
+ if (syncRes.status !== 'noop') {
217
+ updateAnchorStatusBar(ctx, store);
218
+ }
219
+ }
220
+ return;
221
+ }
222
+
223
+ // (b) Real-time auto-tear on `git commit -m "..."`
205
224
  if (lowerTool === 'bash' || lowerTool === 'powershell') {
206
225
  const cmd = String((event.input as any)?.command || '');
207
226
  if (cmd.includes('git commit')) {
@@ -523,4 +542,41 @@ export default function (pi: ExtensionAPI) {
523
542
  await openAnchorDashboard(ctx, store);
524
543
  }
525
544
  });
545
+
546
+ // 1-click Session Time-Machine: `/back` (or `/back 1`) jumps straight to the saved session of an unfinished note/todo checkpoint
547
+ pi.registerCommand('back', {
548
+ description: 'Jump back to the conversation session of the latest unfinished note or todo checkpoint (/back [id])',
549
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
550
+ const targetArg = (args || '').trim();
551
+ const currentSession = getSessionFileSafe(ctx);
552
+
553
+ if (targetArg) {
554
+ const resolvedId = resolveAnchorTarget(store, targetArg, ctx.cwd);
555
+ const note = resolvedId ? store.get(resolvedId) : undefined;
556
+ if (!note || !note.sessionFile || !fs.existsSync(note.sessionFile)) {
557
+ ctx.ui.notify(`⌖ No saved session found for #${targetArg}`, 'error');
558
+ return;
559
+ }
560
+ await ctx.switchSession(note.sessionFile);
561
+ return;
562
+ }
563
+
564
+ const candidates = sortAnchorsForDisplay(
565
+ store.list({ cwd: ctx.cwd, all: true }).filter(a => a.status === 'active' || a.status === 'sleeping'),
566
+ ctx.cwd
567
+ );
568
+ const jumpTarget = candidates.find(a =>
569
+ a.sessionFile &&
570
+ fs.existsSync(a.sessionFile) &&
571
+ (!currentSession || !isSameOrSubPath(a.sessionFile, currentSession))
572
+ );
573
+
574
+ if (!jumpTarget || !jumpTarget.sessionFile) {
575
+ ctx.ui.notify('⌖ No other linked conversation session found to jump back to.', 'info');
576
+ return;
577
+ }
578
+
579
+ await ctx.switchSession(jumpTarget.sessionFile);
580
+ }
581
+ });
526
582
  }
package/src/store.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  type AnchorProgress,
9
9
  type AnchorStatus,
10
10
  type AnchorStoreState,
11
+ type TodoTaskItem,
11
12
  type TouchMatchResult,
12
13
  QUIET_AFTER_DAYS,
13
14
  MS_PER_DAY
@@ -291,12 +292,16 @@ export function renderColdStartAnchorsContext(
291
292
 
292
293
  const lines = activeNotes.map(a => {
293
294
  const num = a.id.replace(/^anc-/, '');
295
+ const tag = a.source === 'todo-sync' ? `[Todo Checkpoint · ${a.project}]` : `[${a.project}]`;
294
296
  const files = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(', ')})` : '';
297
+ const nextStep = a.progress?.steps && a.progress.steps[a.progress.done] && a.progress.steps[a.progress.done] !== a.title
298
+ ? ` · current/next: ${a.progress.steps[a.progress.done]}`
299
+ : '';
295
300
  const prog = a.progress
296
- ? ` [${a.progress.done}/${a.progress.total}${a.progress.steps && a.progress.steps[a.progress.done] ? ` · next: ${a.progress.steps[a.progress.done]}` : ''}]`
301
+ ? ` [${a.progress.done}/${a.progress.total}${nextStep}]`
297
302
  : '';
298
303
  const desc = a.description ? ` — ${a.description}` : '';
299
- return `• #${num} [${a.project}] ${a.title}${prog}${files}${desc}`;
304
+ return `• #${num} ${tag} ${a.title}${prog}${files}${desc}`;
300
305
  });
301
306
 
302
307
  return [
@@ -484,6 +489,7 @@ export class AnchorStore {
484
489
  title: string;
485
490
  description?: string;
486
491
  priority?: AnchorPriority;
492
+ source?: 'manual' | 'todo-sync';
487
493
  cwd?: string;
488
494
  project?: string;
489
495
  files?: string[];
@@ -520,6 +526,7 @@ export class AnchorStore {
520
526
  description: input.description?.trim(),
521
527
  priority: input.priority || 'p1',
522
528
  status: 'active',
529
+ ...(input.source ? { source: input.source } : {}),
523
530
  project: projectName,
524
531
  cwd,
525
532
  createdAt: now,
@@ -649,6 +656,120 @@ export class AnchorStore {
649
656
  });
650
657
  }
651
658
 
659
+ /**
660
+ * Clever Touch 4 (Todo-Bridge): Automatically mirror intra-session `todo` tool lists
661
+ * into a cross-session checkpoint!
662
+ * - While tasks are in_progress/pending: updates the live `[■■□□ done/total]` checkpoint on disk;
663
+ * - When all tasks in the todo list reach `completed`: automatically tears off the checkpoint!
664
+ */
665
+ public syncFromTodoTool(
666
+ tasks: TodoTaskItem[],
667
+ options: { cwd?: string; sessionFile?: string; recentFiles?: string[] } = {}
668
+ ): { status: 'synced' | 'completed' | 'noop'; anchor?: Anchor } {
669
+ if (!Array.isArray(tasks)) return { status: 'noop' };
670
+
671
+ const visible = tasks.filter(t => t && typeof t.subject === 'string' && t.status !== 'deleted');
672
+ const normCwd = options.cwd ? normalizePath(options.cwd) : '';
673
+ const normSession = options.sessionFile ? normalizePath(options.sessionFile) : undefined;
674
+
675
+ return this.withLock(() => {
676
+ const state = this.loadState();
677
+ const existingIdx = state.anchors.findIndex(a =>
678
+ a.source === 'todo-sync' &&
679
+ ((normSession && a.sessionFile === normSession) || (normCwd && isSameOrSubPath(a.cwd, normCwd)))
680
+ );
681
+
682
+ // 1. If todo list was cleared or 100% completed, auto-tear off the synced checkpoint
683
+ const completedTasks = visible.filter(t => t.status === 'completed');
684
+ const unfinishedTasks = visible.filter(t => t.status !== 'completed');
685
+
686
+ if (visible.length === 0 || unfinishedTasks.length === 0) {
687
+ if (existingIdx !== -1) {
688
+ const [settled] = state.anchors.splice(existingIdx, 1);
689
+ settled.status = 'settled';
690
+ settled.updatedAt = Date.now();
691
+ settled.evidence = {
692
+ settledAt: Date.now(),
693
+ settledBy: 'todo-complete',
694
+ summary: `All ${completedTasks.length} session todo(s) completed`
695
+ };
696
+ this.ensureDirs();
697
+ fs.appendFileSync(this.archivePath, JSON.stringify(settled) + '\n', 'utf-8');
698
+ this.saveState(state);
699
+ return { status: 'completed', anchor: settled };
700
+ }
701
+ return { status: 'noop' };
702
+ }
703
+
704
+ // 2. Active unfinished todos exist: build live progress & next-step snapshot
705
+ const inProgress = unfinishedTasks.find(t => t.status === 'in_progress') || unfinishedTasks[0];
706
+ const orderedSteps = [
707
+ ...completedTasks.map(t => t.subject.trim()),
708
+ ...unfinishedTasks.filter(t => t.status === 'in_progress').map(t => t.subject.trim()),
709
+ ...unfinishedTasks.filter(t => t.status === 'pending').map(t => t.subject.trim())
710
+ ];
711
+ const progress: AnchorProgress = {
712
+ done: completedTasks.length,
713
+ total: visible.length,
714
+ steps: orderedSteps
715
+ };
716
+
717
+ const remainingSummary = unfinishedTasks
718
+ .slice(0, 3)
719
+ .map(t => t.subject.trim())
720
+ .join(' → ');
721
+ const headline = inProgress.subject.trim();
722
+ const now = Date.now();
723
+
724
+ if (existingIdx !== -1) {
725
+ const current = state.anchors[existingIdx];
726
+ current.title = headline;
727
+ current.description = `Remaining (${unfinishedTasks.length}/${visible.length}): ${remainingSummary}`;
728
+ current.progress = progress;
729
+ current.status = 'active';
730
+ current.updatedAt = now;
731
+ current.lastTouchedAt = now;
732
+ if (normSession) current.sessionFile = normSession;
733
+ if (options.recentFiles && options.recentFiles.length > 0) {
734
+ const merged = Array.from(new Set([...options.recentFiles.map(f => normalizePath(f)), ...current.files])).slice(0, 5);
735
+ current.files = merged;
736
+ }
737
+ state.anchors[existingIdx] = current;
738
+ this.saveState(state);
739
+ return { status: 'synced', anchor: current };
740
+ }
741
+
742
+ // Create new todo-synced checkpoint
743
+ const id = this.generateId(state);
744
+ const inferredFiles = (options.recentFiles && options.recentFiles.length > 0)
745
+ ? options.recentFiles.map(f => normalizePath(f)).filter(Boolean)
746
+ : extractPathsFromText(orderedSteps.join(' '));
747
+ const projectName = normCwd ? path.basename(normCwd) : 'global';
748
+
749
+ const created: Anchor = {
750
+ id,
751
+ title: headline,
752
+ description: `Remaining (${unfinishedTasks.length}/${visible.length}): ${remainingSummary}`,
753
+ priority: 'p1',
754
+ status: 'active',
755
+ source: 'todo-sync',
756
+ project: projectName,
757
+ cwd: normCwd,
758
+ createdAt: now,
759
+ updatedAt: now,
760
+ lastTouchedAt: now,
761
+ files: inferredFiles,
762
+ tags: ['todo-sync'],
763
+ ...(normSession ? { sessionFile: normSession } : {}),
764
+ progress
765
+ };
766
+
767
+ state.anchors.push(created);
768
+ this.saveState(state);
769
+ return { status: 'synced', anchor: created };
770
+ });
771
+ }
772
+
652
773
  /**
653
774
  * Tear off (settle) a completed sticky note and archive it to archive.jsonl
654
775
  */
package/src/tui.ts CHANGED
@@ -226,14 +226,17 @@ export function updateStartupBanner(ctx: ExtensionContext, store: AnchorStore):
226
226
  const a = sorted[0];
227
227
  const num = a.id.replace(/^anc-/, '');
228
228
  const bar = a.progress ? `${fg('accent', formatProgressBar(a.progress))} ` : '';
229
- const nextStep = a.progress?.steps && a.progress.steps[a.progress.done]
230
- ? fg('muted', ` ▸ ${a.progress.steps[a.progress.done]}`)
231
- : '';
229
+ const candidateStep = a.progress?.steps
230
+ ? (a.progress.steps[a.progress.done] !== a.title
231
+ ? a.progress.steps[a.progress.done]
232
+ : a.progress.steps[a.progress.done + 1])
233
+ : undefined;
234
+ const nextStep = candidateStep ? fg('muted', ` ▸ ${candidateStep}`) : '';
232
235
  const origin = formatOrigin(a);
233
236
  const sessionIcon = a.sessionFile ? ' ↪' : '';
234
237
  const prefix = `${fg('accent', '⌖')} ${fg('dim', `#${num}`)} ${bar}`;
235
238
  const suffix = ` ${fg('muted', `(${origin}${sessionIcon})`)}`;
236
- const availTitle = Math.max(12, width - getDisplayWidth(prefix) - getDisplayWidth( nextStep) - getDisplayWidth(suffix) - 2);
239
+ const availTitle = Math.max(12, width - getDisplayWidth(prefix) - getDisplayWidth(nextStep) - getDisplayWidth(suffix) - 2);
237
240
  const titleClipped = truncateToWidth(a.title, availTitle);
238
241
  return [truncateToWidth(`${prefix}${titleClipped}${nextStep}${suffix}`, width)];
239
242
  }
package/src/types.ts CHANGED
@@ -17,12 +17,20 @@ export interface AnchorProgress {
17
17
 
18
18
  export interface AnchorEvidence {
19
19
  settledAt?: number;
20
- settledBy?: 'commit' | 'manual' | 'one-tap-settlement' | 'manual-command' | 'verification-test';
20
+ settledBy?: 'commit' | 'manual' | 'todo-complete' | 'one-tap-settlement' | 'manual-command' | 'verification-test';
21
21
  commitHash?: string;
22
22
  summary?: string;
23
23
  touchedFiles?: string[];
24
24
  }
25
25
 
26
+ export interface TodoTaskItem {
27
+ id: number;
28
+ subject: string;
29
+ status: 'pending' | 'in_progress' | 'completed' | 'deleted';
30
+ activeForm?: string;
31
+ description?: string;
32
+ }
33
+
26
34
  export interface Anchor {
27
35
  /** Sequential identifier, e.g. 'anc-1' */
28
36
  id: string;
@@ -32,6 +40,8 @@ export interface Anchor {
32
40
  description?: string;
33
41
  /** Lifecycle status: 'active' (recent) or 'sleeping' (>7d untouched, 0 prompt tokens) */
34
42
  status: AnchorStatus;
43
+ /** Origin source: 'manual' (user/tool pinned) or 'todo-sync' (auto-bridged from unfinished session todos) */
44
+ source?: 'manual' | 'todo-sync';
35
45
  /** Short workspace label (e.g. 'anchor', 'College/优化', or 'global') */
36
46
  project: string;
37
47
  /** Workspace directory path where the note applies */