pi-anchor 0.5.0 → 0.6.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.
@@ -66,10 +66,10 @@ function extractProgressFromText(rawTitle) {
66
66
  progress: { done: 0, total: steps.length, steps }
67
67
  };
68
68
  }
69
- const ratioMatch = rawTitle.match(/(?:\[(\d{1,3})\/(\d{1,3})\]|\((\d{1,3})\/(\d{1,3})\)|(?:^|\s)(\d{1,3})\/(\d{1,3})(?=\s|$))/);
69
+ const ratioMatch = rawTitle.match(/(?:\[(\d{1,3})\/(\d{1,3})\]|\((\d{1,3})\/(\d{1,3})\))/);
70
70
  if (ratioMatch) {
71
- const done = parseInt(ratioMatch[1] ?? ratioMatch[3] ?? ratioMatch[5], 10);
72
- const total = parseInt(ratioMatch[2] ?? ratioMatch[4] ?? ratioMatch[6], 10);
71
+ const done = parseInt(ratioMatch[1] ?? ratioMatch[3], 10);
72
+ const total = parseInt(ratioMatch[2] ?? ratioMatch[4], 10);
73
73
  if (!isNaN(done) && !isNaN(total) && total >= 2 && total <= 500 && done >= 0 && done <= total) {
74
74
  const stripped = rawTitle.replace(ratioMatch[0], " ").replace(/\s+/g, " ").trim();
75
75
  return {
@@ -91,7 +91,8 @@ function isAnchorTargetingWorkspace(anchor, targetCwd) {
91
91
  }
92
92
  if (anchor.files && anchor.files.length > 0) {
93
93
  for (const f of anchor.files) {
94
- if (isSameOrSubPath(f, normTarget)) return true;
94
+ const resolvedFile = !path.isAbsolute(f) && !/^[a-zA-Z]:\//.test(f) && anchor.cwd ? `${anchor.cwd.replace(/\\/g, "/").replace(/\/+$/, "")}/${f.replace(/^\/+/, "")}` : f;
95
+ if (isSameOrSubPath(resolvedFile, normTarget)) return true;
95
96
  }
96
97
  }
97
98
  if (anchor.cwd && isSameOrSubPath(anchor.cwd, normTarget)) {
@@ -181,6 +182,41 @@ function makeSafeTaskAnnotation(filePath, anchor) {
181
182
  const num = anchor.id.replace(/^anc-/, "");
182
183
  return "\n\n" + fmt(`\u2316 Note #${num}: ${anchor.title}`);
183
184
  }
185
+ var COMMIT_STOP_WORDS = /* @__PURE__ */ new Set([
186
+ "fix",
187
+ "feat",
188
+ "chore",
189
+ "docs",
190
+ "test",
191
+ "tests",
192
+ "refactor",
193
+ "style",
194
+ "perf",
195
+ "build",
196
+ "ci",
197
+ "bug",
198
+ "issue",
199
+ "update",
200
+ "add",
201
+ "remove",
202
+ "todo",
203
+ "note",
204
+ "task",
205
+ "the",
206
+ "and",
207
+ "for",
208
+ "with",
209
+ "\u4FEE\u590D",
210
+ "\u4F18\u5316",
211
+ "\u66F4\u65B0",
212
+ "\u6DFB\u52A0",
213
+ "\u4FEE\u6539",
214
+ "\u5B8C\u6210",
215
+ "\u95EE\u9898",
216
+ "\u4EFB\u52A1",
217
+ "\u6A21\u5757",
218
+ "\u529F\u80FD"
219
+ ]);
184
220
  function matchesCommitMessage(anchor, commitMessages) {
185
221
  const idLower = anchor.id.toLowerCase();
186
222
  const numId = `#${anchor.id.replace(/^anc-/, "")}`;
@@ -189,21 +225,22 @@ function matchesCommitMessage(anchor, commitMessages) {
189
225
  if (typeof Intl !== "undefined" && Intl.Segmenter) {
190
226
  try {
191
227
  const seg = new Intl.Segmenter("und", { granularity: "word" });
192
- titleTokens = Array.from(seg.segment(title)).filter((s) => s.isWordLike).map((s) => s.segment.trim().toLowerCase()).filter((t) => t.length >= 2);
228
+ titleTokens = Array.from(seg.segment(title)).filter((s) => s.isWordLike).map((s) => s.segment.trim().toLowerCase()).filter((t) => t.length >= 2 && !COMMIT_STOP_WORDS.has(t));
193
229
  } catch {
194
- titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
230
+ titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2 && !COMMIT_STOP_WORDS.has(t));
195
231
  }
196
232
  } else {
197
- titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
233
+ titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2 && !COMMIT_STOP_WORDS.has(t));
198
234
  }
199
235
  for (const msg of commitMessages) {
200
236
  const msgLower = msg.toLowerCase();
201
- if (msgLower.includes(idLower) || msgLower.includes(numId)) {
237
+ if (msgLower.includes(idLower) || new RegExp(`(?:^|\\s|\\()${numId}(?:\\b|\\)|$)`).test(msgLower)) {
202
238
  return { matched: true, message: msg };
203
239
  }
204
240
  if (titleTokens.length > 0) {
205
241
  const hits = titleTokens.filter((tok) => msgLower.includes(tok)).length;
206
- if (hits >= Math.min(2, titleTokens.length)) {
242
+ const requiredHits = titleTokens.length === 1 ? 1 : 2;
243
+ if (hits >= requiredHits) {
207
244
  return { matched: true, message: msg };
208
245
  }
209
246
  }
@@ -221,10 +258,12 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
221
258
  if (activeNotes.length === 0) return "";
222
259
  const lines = activeNotes.map((a) => {
223
260
  const num = a.id.replace(/^anc-/, "");
261
+ const tag = a.source === "todo-sync" ? `[Todo Checkpoint \xB7 ${a.project}]` : `[${a.project}]`;
224
262
  const files = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(", ")})` : "";
225
- 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}]` : "";
226
265
  const desc = a.description ? ` \u2014 ${a.description}` : "";
227
- return `\u2022 #${num} [${a.project}] ${a.title}${prog}${files}${desc}`;
266
+ return `\u2022 #${num} ${tag} ${a.title}${prog}${files}${desc}`;
228
267
  });
229
268
  return [
230
269
  `<sticky-notes count="${activeNotes.length}">`,
@@ -416,6 +455,7 @@ var AnchorStore = class {
416
455
  description: input.description?.trim(),
417
456
  priority: input.priority || "p1",
418
457
  status: "active",
458
+ ...input.source ? { source: input.source } : {},
419
459
  project: projectName,
420
460
  cwd,
421
461
  createdAt: now,
@@ -504,9 +544,10 @@ var AnchorStore = class {
504
544
  if (idx === -1) throw new Error(`Note not found: ${id}`);
505
545
  const current = state.anchors[idx];
506
546
  const now = Date.now();
507
- const total = explicitTotal && explicitTotal >= 2 ? explicitTotal : current.progress?.total || 4;
547
+ const safeDelta = Number.isFinite(delta) ? delta : 1;
548
+ const total = explicitTotal && Number.isFinite(explicitTotal) && explicitTotal >= 1 ? explicitTotal : current.progress?.total || 4;
508
549
  const prevDone = current.progress?.done || 0;
509
- const nextDone = Math.max(0, Math.min(total, prevDone + delta));
550
+ const nextDone = Math.max(0, Math.min(total, prevDone + safeDelta));
510
551
  current.progress = {
511
552
  ...current.progress || {},
512
553
  done: nextDone,
@@ -522,6 +563,97 @@ var AnchorStore = class {
522
563
  return { anchor: current, completed: nextDone >= total };
523
564
  });
524
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
+ }
525
657
  /**
526
658
  * Tear off (settle) a completed sticky note and archive it to archive.jsonl
527
659
  */
@@ -740,7 +872,8 @@ function updateStartupBanner(ctx, store) {
740
872
  const a = sorted[0];
741
873
  const num = a.id.replace(/^anc-/, "");
742
874
  const bar = a.progress ? `${fg("accent", formatProgressBar(a.progress))} ` : "";
743
- 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}`) : "";
744
877
  const origin = formatOrigin(a);
745
878
  const sessionIcon = a.sessionFile ? " \u21AA" : "";
746
879
  const prefix = `${fg("accent", "\u2316")} ${fg("dim", `#${num}`)} ${bar}`;
@@ -749,15 +882,24 @@ function updateStartupBanner(ctx, store) {
749
882
  const titleClipped = truncateToWidth(a.title, availTitle);
750
883
  return [truncateToWidth(`${prefix}${titleClipped}${nextStep}${suffix}`, width)];
751
884
  }
752
- const countTag = fg("accent", `\u2316 ${sorted.length} notes: `);
753
- const segments = sorted.slice(0, 3).map((a) => {
885
+ const maxItems = width < 68 ? 1 : width < 100 ? 2 : 3;
886
+ const visibleSlice = sorted.slice(0, maxItems);
887
+ const countTag = fg("accent", `\u2316 ${sorted.length}: `);
888
+ const moreCount = sorted.length - visibleSlice.length;
889
+ const moreSuffix = moreCount > 0 ? fg("dim", ` +${moreCount}`) : "";
890
+ const perSlotWidth = Math.max(
891
+ 10,
892
+ Math.floor((width - getDisplayWidth(countTag) - getDisplayWidth(moreSuffix) - (visibleSlice.length - 1) * 5) / visibleSlice.length)
893
+ );
894
+ const segments = visibleSlice.map((a) => {
754
895
  const num = a.id.replace(/^anc-/, "");
755
896
  const bar = a.progress ? `${formatProgressBar(a.progress)} ` : "";
756
- const shortTitle = truncateToWidth(a.title, Math.max(14, Math.floor((width - 20) / Math.min(3, sorted.length)) - 12));
897
+ const prefix = `#${num} ${bar}`;
898
+ const titleBudget = Math.max(6, perSlotWidth - getDisplayWidth(prefix));
899
+ const shortTitle = truncateToWidth(a.title, titleBudget);
757
900
  return `${fg("dim", `#${num}`)} ${bar}${shortTitle}`;
758
901
  });
759
- const more = sorted.length > 3 ? fg("dim", ` +${sorted.length - 3}`) : "";
760
- return [truncateToWidth(`${countTag}${segments.join(fg("dim", " \xB7 "))}${more}`, width)];
902
+ return [truncateToWidth(`${countTag}${segments.join(fg("dim", " \xB7 "))}${moreSuffix}`, width)];
761
903
  },
762
904
  invalidate: () => {
763
905
  }
@@ -798,7 +940,7 @@ async function openAnchorDashboard(ctx, store) {
798
940
  if (!anchor) return;
799
941
  const currentSessionFile = ctx.sessionManager?.getSessionFile?.();
800
942
  const canJumpSession = Boolean(
801
- anchor.sessionFile && fs2.existsSync(anchor.sessionFile) && typeof ctx.switchSession === "function" && (!currentSessionFile || path2.resolve(anchor.sessionFile) !== path2.resolve(currentSessionFile))
943
+ anchor.sessionFile && fs2.existsSync(anchor.sessionFile) && typeof ctx.switchSession === "function" && (!currentSessionFile || !isSameOrSubPath(anchor.sessionFile, currentSessionFile))
802
944
  );
803
945
  const stepLabel = anchor.progress ? `\u25B6 Advance step (${anchor.progress.done}/${anchor.progress.total} \u2794 ${Math.min(anchor.progress.total, anchor.progress.done + 1)}/${anchor.progress.total})` : "\u25B6 Start progress bar (1/4)";
804
946
  const actions = [
@@ -887,11 +1029,14 @@ function parseAnchorCommandInput(raw) {
887
1029
  };
888
1030
  }
889
1031
  function parseQuickNoteInput(text) {
890
- const m = (text || "").trim().match(/^(?:pin|note|便签|备忘)\s*[::]\s*(.+)$/i);
891
- return m && m[1].trim() ? m[1].trim() : null;
1032
+ const m = (text || "").trim().match(/^(?:pin|note|便签|备忘)\s*[::]\s*(.*)$/i);
1033
+ if (!m) return null;
1034
+ return m[1].trim();
892
1035
  }
893
1036
  function resolveAnchorTarget(store, idOrIndex, cwd) {
894
1037
  const clean = idOrIndex.trim().replace(/^#/, "");
1038
+ const direct = store.get(clean);
1039
+ if (direct) return direct.id;
895
1040
  if (/^\d+$/.test(clean)) {
896
1041
  const seqIndex = parseInt(clean, 10);
897
1042
  const visible = store.list({ cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping");
@@ -900,7 +1045,7 @@ function resolveAnchorTarget(store, idOrIndex, cwd) {
900
1045
  return sorted[seqIndex - 1].id;
901
1046
  }
902
1047
  }
903
- return store.get(clean)?.id;
1048
+ return void 0;
904
1049
  }
905
1050
  function getSessionFileSafe(ctx) {
906
1051
  try {
@@ -949,7 +1094,13 @@ function extension_default(pi) {
949
1094
  pi.on("input", async (event, ctx) => {
950
1095
  if (event?.source === "extension") return { action: "continue" };
951
1096
  const quickNote = parseQuickNoteInput(String(event?.text || ""));
952
- if (!quickNote) return { action: "continue" };
1097
+ if (quickNote === null) return { action: "continue" };
1098
+ if (!quickNote) {
1099
+ if (hasActiveUI(ctx)) {
1100
+ await openAnchorDashboard(ctx, store);
1101
+ }
1102
+ return { action: "handled" };
1103
+ }
953
1104
  const anc = createStickyNoteFromUser(quickNote, ctx);
954
1105
  const bar = anc.progress ? ` ${formatProgressBar(anc.progress)}` : "";
955
1106
  if (hasActiveUI(ctx)) {
@@ -973,6 +1124,20 @@ ${snippet}` };
973
1124
  if (event.isError) return;
974
1125
  const lowerTool = String(event.toolName || "").toLowerCase();
975
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
+ }
976
1141
  if (lowerTool === "bash" || lowerTool === "powershell") {
977
1142
  const cmd = String(event.input?.command || "");
978
1143
  if (cmd.includes("git commit")) {
@@ -1201,7 +1366,7 @@ ${snippet}` };
1201
1366
  return;
1202
1367
  }
1203
1368
  const rawTrimmed = (args || "").trim();
1204
- if (/^(?:#?anc-\d+|#\d+)$/i.test(rawTrimmed)) {
1369
+ if (/^(?:#?anc-\d+|#?\d+)$/i.test(rawTrimmed)) {
1205
1370
  const resolvedId = resolveAnchorTarget(store, rawTrimmed, ctx.cwd);
1206
1371
  if (resolvedId) {
1207
1372
  const settled = store.settle(resolvedId, { settledBy: "manual" });
@@ -1268,6 +1433,35 @@ ${snippet}` };
1268
1433
  await openAnchorDashboard(ctx, store);
1269
1434
  }
1270
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
+ });
1271
1465
  }
1272
1466
 
1273
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
  */
@@ -197,9 +221,14 @@ declare function parseAnchorCommandInput(raw: string): {
197
221
  /**
198
222
  * Detect 0ms human-first quick note prefix in the input box:
199
223
  * e.g. `pin: 明天把Token改Cookie` or `便签: 检查网关重试 [1/4]`
224
+ * Returns `{ matched: true, note: '...' }` (or `note: ''` if user typed bare `pin:`)
200
225
  */
201
226
  declare function parseQuickNoteInput(text: string): string | null;
227
+ /**
228
+ * Resolve note target by exact ID first (`#2` -> `anc-2`), falling back to 1-based display row index
229
+ * only when no note with that ID exists. Prevents settling the wrong note when sort order differs from ID order!
230
+ */
202
231
  declare function resolveAnchorTarget(store: AnchorStore, idOrIndex: string, cwd?: string): string | undefined;
203
232
  declare function export_default(pi: ExtensionAPI): void;
204
233
 
205
- 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
  */
@@ -197,9 +221,14 @@ declare function parseAnchorCommandInput(raw: string): {
197
221
  /**
198
222
  * Detect 0ms human-first quick note prefix in the input box:
199
223
  * e.g. `pin: 明天把Token改Cookie` or `便签: 检查网关重试 [1/4]`
224
+ * Returns `{ matched: true, note: '...' }` (or `note: ''` if user typed bare `pin:`)
200
225
  */
201
226
  declare function parseQuickNoteInput(text: string): string | null;
227
+ /**
228
+ * Resolve note target by exact ID first (`#2` -> `anc-2`), falling back to 1-based display row index
229
+ * only when no note with that ID exists. Prevents settling the wrong note when sort order differs from ID order!
230
+ */
202
231
  declare function resolveAnchorTarget(store: AnchorStore, idOrIndex: string, cwd?: string): string | undefined;
203
232
  declare function export_default(pi: ExtensionAPI): void;
204
233
 
205
- 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 };