pi-anchor 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -30,89 +30,52 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- AnchorProtocol: () => AnchorProtocol,
34
33
  AnchorStore: () => AnchorStore,
35
34
  COMMENT_FORMATS: () => COMMENT_FORMATS,
36
- DEFAULT_DECAY_POLICY: () => DEFAULT_DECAY_POLICY,
37
- DURABLE_DECAY_POLICY: () => DURABLE_DECAY_POLICY,
38
- EPHEMERAL_DECAY_POLICY: () => EPHEMERAL_DECAY_POLICY,
39
- SessionTouchObserver: () => SessionTouchObserver,
35
+ MS_PER_DAY: () => MS_PER_DAY,
36
+ QUIET_AFTER_DAYS: () => QUIET_AFTER_DAYS,
40
37
  acquireSyncLock: () => acquireSyncLock,
41
38
  atomicRenameWithRetry: () => atomicRenameWithRetry,
42
- classifyAnchor: () => classifyAnchor,
43
- detectDurability: () => detectDurability,
44
- detectRecurrence: () => detectRecurrence,
45
- detectTargetDate: () => detectTargetDate,
46
- evaluateAnchorDecay: () => evaluateAnchorDecay,
47
39
  extractPathsFromText: () => extractPathsFromText,
40
+ extractProgressFromText: () => extractProgressFromText,
48
41
  findMatchedAnchors: () => findMatchedAnchors,
49
- formatCreationTime: () => formatCreationTime,
50
42
  formatOrigin: () => formatOrigin,
51
- formatRelativeTime: () => formatRelativeTime,
52
- formatRemainingTtl: () => formatRemainingTtl,
53
- formatSettlementCard: () => formatSettlementCard,
54
- formatTargetDate: () => formatTargetDate,
55
- generateSettlementProposals: () => generateSettlementProposals,
43
+ formatProgressBar: () => formatProgressBar,
44
+ formatRelativeAge: () => formatRelativeAge,
56
45
  getDefaultStorageDir: () => getDefaultStorageDir,
57
46
  getDisplayWidth: () => getDisplayWidth,
58
- getEphemeralDecayPolicy: () => getEphemeralDecayPolicy,
59
- getTodayDateString: () => getTodayDateString,
60
47
  globToRegExp: () => globToRegExp,
61
- groupAnchorsByQuadrant: () => groupAnchorsByQuadrant,
62
48
  hasActiveUI: () => hasActiveUI,
63
49
  isAnchorTargetingWorkspace: () => isAnchorTargetingWorkspace,
64
50
  isSameOrSubPath: () => isSameOrSubPath,
65
51
  makeSafeTaskAnnotation: () => makeSafeTaskAnnotation,
66
52
  matchAnchorAgainstTouchedFiles: () => matchAnchorAgainstTouchedFiles,
53
+ matchesCommitMessage: () => matchesCommitMessage,
67
54
  normalizePath: () => normalizePath,
68
55
  openAnchorDashboard: () => openAnchorDashboard,
69
56
  padToWidth: () => padToWidth,
57
+ parseAnchorCommandInput: () => parseAnchorCommandInput,
58
+ parseQuickNoteInput: () => parseQuickNoteInput,
70
59
  renderActiveAnchorsContext: () => renderActiveAnchorsContext,
71
60
  renderColdStartAnchorsContext: () => renderColdStartAnchorsContext,
72
- runPhysicalVerification: () => runPhysicalVerification,
61
+ resolveAnchorTarget: () => resolveAnchorTarget,
73
62
  sortAnchorsForDisplay: () => sortAnchorsForDisplay,
74
63
  stripAnsi: () => stripAnsi,
75
- sweepStore: () => sweepStore,
64
+ sweepQuietNotes: () => sweepQuietNotes,
76
65
  truncateToWidth: () => truncateToWidth,
77
- tryRealtimeAutoSettle: () => tryRealtimeAutoSettle,
78
66
  updateAnchorStatusBar: () => updateAnchorStatusBar,
79
67
  updateStartupBanner: () => updateStartupBanner
80
68
  });
81
69
  module.exports = __toCommonJS(index_exports);
82
70
 
83
71
  // src/types.ts
84
- var EPHEMERAL_DECAY_POLICY = {
85
- activeDays: 7,
86
- // 7 days active before quiet sleep
87
- sleepDays: 30,
88
- graveyardDays: 9999
89
- // Never silently auto-delete unfinished tasks
90
- };
91
- var DURABLE_DECAY_POLICY = {
92
- activeDays: 7,
93
- // 7 days active before quiet sleep
94
- sleepDays: 30,
95
- graveyardDays: 9999
96
- // Never permanently auto-dropped; preserved indefinitely
97
- };
98
- var DEFAULT_DECAY_POLICY = {
99
- activeDays: 7,
100
- sleepDays: 30,
101
- graveyardDays: 9999
102
- };
72
+ var QUIET_AFTER_DAYS = 7;
73
+ var MS_PER_DAY = 864e5;
103
74
 
104
75
  // src/store.ts
105
76
  var import_node_fs = __toESM(require("fs"), 1);
106
- var import_node_path2 = __toESM(require("path"), 1);
107
- var import_node_os = __toESM(require("os"), 1);
108
-
109
- // src/matcher.ts
110
77
  var import_node_path = __toESM(require("path"), 1);
111
- function globToRegExp(pattern) {
112
- const collapsed = pattern.slice(0, 512).replace(/\/+(\*\*\/+)+/g, "/**/").replace(/\*{3,}/g, "**");
113
- const escaped = collapsed.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\/\*\*\//g, "___GLOB_DIR_SLASH___").replace(/\*\*/g, "___GLOB_STAR_STAR___").replace(/\*/g, "[^/]*").replace(/___GLOB_DIR_SLASH___/g, "(?:/|/(?:[^/]+/)+)").replace(/___GLOB_STAR_STAR___/g, ".*");
114
- return new RegExp(`^${escaped}$`);
115
- }
78
+ var import_node_os = __toESM(require("os"), 1);
116
79
  function normalizePath(filePath, baseCwd) {
117
80
  if (typeof filePath !== "string") return "";
118
81
  let norm = filePath.trim().replace(/\\/g, "/").replace(/^\.\//, "");
@@ -128,14 +91,19 @@ function normalizePath(filePath, baseCwd) {
128
91
  }
129
92
  return norm;
130
93
  }
131
- function isSameOrSubPath(anchorCwd, targetCwd) {
132
- if (!anchorCwd || !targetCwd) return false;
133
- const normA = anchorCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
134
- const normT = targetCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
135
- const isWinStyle = process.platform === "win32" || /^[a-zA-Z]:\//.test(normA) || /^[a-zA-Z]:\//.test(normT);
136
- const cmpA = isWinStyle ? normA.toLowerCase() : normA;
137
- const cmpT = isWinStyle ? normT.toLowerCase() : normT;
138
- return cmpA === cmpT || cmpT.startsWith(cmpA + "/") || cmpA.startsWith(cmpT + "/");
94
+ function isSameOrSubPath(dirA, dirB) {
95
+ if (!dirA || !dirB) return false;
96
+ const normA = dirA.trim().replace(/\\/g, "/").replace(/\/+$/, "");
97
+ const normB = dirB.trim().replace(/\\/g, "/").replace(/\/+$/, "");
98
+ const isWin = process.platform === "win32" || /^[a-zA-Z]:\//.test(normA) || /^[a-zA-Z]:\//.test(normB);
99
+ const a = isWin ? normA.toLowerCase() : normA;
100
+ const b = isWin ? normB.toLowerCase() : normB;
101
+ return a === b || b.startsWith(a + "/") || a.startsWith(b + "/");
102
+ }
103
+ function globToRegExp(pattern) {
104
+ const collapsed = pattern.slice(0, 512).replace(/\/+(\*\*\/+)+/g, "/**/").replace(/\*{3,}/g, "**");
105
+ const escaped = collapsed.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\/\*\*\//g, "___GLOB_DIR_SLASH___").replace(/\*\*/g, "___GLOB_STAR_STAR___").replace(/\*/g, "[^/]*").replace(/___GLOB_DIR_SLASH___/g, "(?:/|/(?:[^/]+/)+)").replace(/___GLOB_STAR_STAR___/g, ".*");
106
+ return new RegExp(`^${escaped}$`);
139
107
  }
140
108
  function extractPathsFromText(text) {
141
109
  if (!text) return [];
@@ -150,6 +118,32 @@ function extractPathsFromText(text) {
150
118
  }
151
119
  return Array.from(results);
152
120
  }
121
+ function extractProgressFromText(rawTitle) {
122
+ if (!rawTitle) return { cleanTitle: "" };
123
+ const stepParts = Array.from(rawTitle.matchAll(/(?:^|\s)(\d+)\.\s*([^\d]+?)(?=\s+\d+\.\s|$)/g));
124
+ if (stepParts.length >= 2) {
125
+ const steps = stepParts.map((m) => m[2].trim().replace(/[;;,,]+$/, "")).filter(Boolean);
126
+ const firstMatchIdx = rawTitle.indexOf(stepParts[0][0]);
127
+ const prefix = rawTitle.slice(0, firstMatchIdx).replace(/[::\-—\s]+$/, "").trim();
128
+ return {
129
+ cleanTitle: prefix || steps.join(" \u2192 "),
130
+ progress: { done: 0, total: steps.length, steps }
131
+ };
132
+ }
133
+ const ratioMatch = rawTitle.match(/(?:\[(\d{1,3})\/(\d{1,3})\]|\((\d{1,3})\/(\d{1,3})\)|(?:^|\s)(\d{1,3})\/(\d{1,3})(?=\s|$))/);
134
+ if (ratioMatch) {
135
+ const done = parseInt(ratioMatch[1] ?? ratioMatch[3] ?? ratioMatch[5], 10);
136
+ const total = parseInt(ratioMatch[2] ?? ratioMatch[4] ?? ratioMatch[6], 10);
137
+ if (!isNaN(done) && !isNaN(total) && total >= 2 && total <= 500 && done >= 0 && done <= total) {
138
+ const stripped = rawTitle.replace(ratioMatch[0], " ").replace(/\s+/g, " ").trim();
139
+ return {
140
+ cleanTitle: stripped || rawTitle.trim(),
141
+ progress: { done, total }
142
+ };
143
+ }
144
+ }
145
+ return { cleanTitle: rawTitle.trim() };
146
+ }
153
147
  function isAnchorTargetingWorkspace(anchor, targetCwd) {
154
148
  if (!targetCwd) return true;
155
149
  const normTarget = targetCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
@@ -157,8 +151,7 @@ function isAnchorTargetingWorkspace(anchor, targetCwd) {
157
151
  if (anchor.project && anchor.project !== "global") {
158
152
  const normProj = anchor.project.trim().replace(/\\/g, "/").replace(/\/+$/, "");
159
153
  if (isSameOrSubPath(normProj, normTarget)) return true;
160
- const projBase = import_node_path.default.basename(normProj).toLowerCase();
161
- if (projBase && projBase === targetBase) return true;
154
+ if (import_node_path.default.basename(normProj).toLowerCase() === targetBase) return true;
162
155
  }
163
156
  if (anchor.files && anchor.files.length > 0) {
164
157
  for (const f of anchor.files) {
@@ -183,12 +176,9 @@ function matchAnchorAgainstTouchedFiles(anchor, touchedFiles) {
183
176
  matchedExact.push(touched);
184
177
  break;
185
178
  }
186
- if (normPattern.includes("*")) {
187
- const regex = globToRegExp(normPattern);
188
- if (regex.test(touched)) {
189
- matchedExact.push(touched);
190
- break;
191
- }
179
+ if (normPattern.includes("*") && globToRegExp(normPattern).test(touched)) {
180
+ matchedExact.push(touched);
181
+ break;
192
182
  }
193
183
  const dirPrefix = normPattern.endsWith("/") ? normPattern : normPattern + "/";
194
184
  if (touched.startsWith(dirPrefix)) {
@@ -198,193 +188,138 @@ function matchAnchorAgainstTouchedFiles(anchor, touchedFiles) {
198
188
  }
199
189
  }
200
190
  if (matchedExact.length > 0) {
201
- return {
202
- anchor,
203
- score: 1,
204
- matchedFiles: Array.from(new Set(matchedExact)),
205
- reason: "exact-file"
206
- };
191
+ return { anchor, score: 1, matchedFiles: Array.from(new Set(matchedExact)), reason: "exact-file" };
207
192
  }
208
193
  if (matchedPrefix.length > 0) {
209
- return {
210
- anchor,
211
- score: 0.85,
212
- matchedFiles: Array.from(new Set(matchedPrefix)),
213
- reason: "dir-prefix"
214
- };
194
+ return { anchor, score: 0.85, matchedFiles: Array.from(new Set(matchedPrefix)), reason: "dir-prefix" };
215
195
  }
216
196
  return null;
217
197
  }
218
198
  function findMatchedAnchors(anchors, touchedFiles) {
219
- const priorityWeight = { p0: 3, p1: 2, p2: 1 };
220
199
  const results = [];
221
200
  for (const anchor of anchors) {
222
- const match = matchAnchorAgainstTouchedFiles(anchor, touchedFiles);
223
- if (match) {
224
- results.push(match);
225
- }
201
+ const m = matchAnchorAgainstTouchedFiles(anchor, touchedFiles);
202
+ if (m) results.push(m);
226
203
  }
227
- return results.sort((a, b) => {
228
- if (b.score !== a.score) {
229
- return b.score - a.score;
230
- }
231
- const weightA = priorityWeight[a.anchor.priority] || 0;
232
- const weightB = priorityWeight[b.anchor.priority] || 0;
233
- if (weightB !== weightA) {
234
- return weightB - weightA;
235
- }
236
- return b.anchor.lastTouchedAt - a.anchor.lastTouchedAt;
237
- });
204
+ return results.sort((a, b) => b.score - a.score);
238
205
  }
239
-
240
- // src/store.ts
241
- function detectDurability(title) {
242
- const temporalKeywords = [
243
- "\u4ECA\u5929",
244
- "\u4ECA\u65E5",
245
- "\u665A\u4E0A",
246
- "\u4ECA\u665A",
247
- "\u660E\u5929",
248
- "\u540E\u5929",
249
- "\u5927\u540E\u5929",
250
- "\u7A0D\u540E",
251
- "\u5F85\u4F1A",
252
- "\u4E0B\u5348",
253
- "\u4E0A\u5348",
254
- "\u4E34\u65F6",
255
- "\u4E00\u4F1A\u513F",
256
- "\u7B49\u7B49",
257
- "today",
258
- "tonight",
259
- "tomorrow",
260
- "later",
261
- "temp",
262
- "soon"
263
- ];
264
- const lower = title.toLowerCase();
265
- for (const kw of temporalKeywords) {
266
- if (lower.includes(kw)) {
267
- return "ephemeral";
206
+ var COMMENT_FORMATS = {
207
+ ".ts": (m) => `// ${m}`,
208
+ ".tsx": (m) => `// ${m}`,
209
+ ".js": (m) => `// ${m}`,
210
+ ".jsx": (m) => `// ${m}`,
211
+ ".mjs": (m) => `// ${m}`,
212
+ ".cjs": (m) => `// ${m}`,
213
+ ".go": (m) => `// ${m}`,
214
+ ".rs": (m) => `// ${m}`,
215
+ ".c": (m) => `// ${m}`,
216
+ ".cpp": (m) => `// ${m}`,
217
+ ".h": (m) => `// ${m}`,
218
+ ".cs": (m) => `// ${m}`,
219
+ ".java": (m) => `// ${m}`,
220
+ ".kt": (m) => `// ${m}`,
221
+ ".swift": (m) => `// ${m}`,
222
+ ".dart": (m) => `// ${m}`,
223
+ ".py": (m) => `# ${m}`,
224
+ ".rb": (m) => `# ${m}`,
225
+ ".sh": (m) => `# ${m}`,
226
+ ".bash": (m) => `# ${m}`,
227
+ ".yaml": (m) => `# ${m}`,
228
+ ".yml": (m) => `# ${m}`,
229
+ ".toml": (m) => `# ${m}`,
230
+ ".ps1": (m) => `# ${m}`,
231
+ ".html": (m) => `<!-- ${m} -->`,
232
+ ".xml": (m) => `<!-- ${m} -->`,
233
+ ".svg": (m) => `<!-- ${m} -->`,
234
+ ".vue": (m) => `<!-- ${m} -->`,
235
+ ".svelte": (m) => `<!-- ${m} -->`,
236
+ ".css": (m) => `/* ${m} */`,
237
+ ".scss": (m) => `/* ${m} */`,
238
+ ".sql": (m) => `-- ${m}`,
239
+ ".lua": (m) => `-- ${m}`
240
+ };
241
+ function makeSafeTaskAnnotation(filePath, anchor) {
242
+ const ext = import_node_path.default.extname(filePath).toLowerCase();
243
+ const fmt = COMMENT_FORMATS[ext];
244
+ if (!fmt) return null;
245
+ const num = anchor.id.replace(/^anc-/, "");
246
+ return "\n\n" + fmt(`\u2316 Note #${num}: ${anchor.title}`);
247
+ }
248
+ function matchesCommitMessage(anchor, commitMessages) {
249
+ const idLower = anchor.id.toLowerCase();
250
+ const numId = `#${anchor.id.replace(/^anc-/, "")}`;
251
+ const title = anchor.title.toLowerCase();
252
+ let titleTokens = [];
253
+ if (typeof Intl !== "undefined" && Intl.Segmenter) {
254
+ try {
255
+ const seg = new Intl.Segmenter("und", { granularity: "word" });
256
+ titleTokens = Array.from(seg.segment(title)).filter((s) => s.isWordLike).map((s) => s.segment.trim().toLowerCase()).filter((t) => t.length >= 2);
257
+ } catch {
258
+ titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
268
259
  }
260
+ } else {
261
+ titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
269
262
  }
270
- return "durable";
271
- }
272
- function getEphemeralDecayPolicy(title) {
273
- const lower = title.toLowerCase();
274
- if (lower.includes("\u5927\u540E\u5929")) {
275
- return { activeDays: 7, sleepDays: 30, graveyardDays: 9999 };
276
- }
277
- if (lower.includes("\u540E\u5929")) {
278
- return { activeDays: 7, sleepDays: 30, graveyardDays: 9999 };
279
- }
280
- return { ...EPHEMERAL_DECAY_POLICY };
281
- }
282
- function detectTargetDate(titleOrDate, now = Date.now()) {
283
- const lower = titleOrDate.toLowerCase().trim();
284
- const MS_DAY = 24 * 60 * 60 * 1e3;
285
- if (lower.includes("\u4ECA\u5929") || lower.includes("\u4ECA\u65E5") || lower.includes("\u4ECA\u665A") || lower === "today" || lower === "tonight") {
286
- return getTodayDateString(now);
287
- }
288
- if (lower.includes("\u660E\u5929") || lower === "tomorrow") {
289
- return getTodayDateString(now + MS_DAY);
290
- }
291
- if (lower.includes("\u540E\u5929")) {
292
- return getTodayDateString(now + 2 * MS_DAY);
293
- }
294
- if (lower.includes("\u5927\u540E\u5929")) {
295
- return getTodayDateString(now + 3 * MS_DAY);
296
- }
297
- const weekdayMap = {
298
- "\u5468\u65E5": 0,
299
- "\u661F\u671F\u65E5": 0,
300
- "sunday": 0,
301
- "sun": 0,
302
- "\u5468\u4E00": 1,
303
- "\u661F\u671F\u4E00": 1,
304
- "monday": 1,
305
- "mon": 1,
306
- "\u5468\u4E8C": 2,
307
- "\u661F\u671F\u4E8C": 2,
308
- "tuesday": 2,
309
- "tue": 2,
310
- "\u5468\u4E09": 3,
311
- "\u661F\u671F\u4E09": 3,
312
- "wednesday": 3,
313
- "wed": 3,
314
- "\u5468\u56DB": 4,
315
- "\u661F\u671F\u56DB": 4,
316
- "thursday": 4,
317
- "thu": 4,
318
- "\u5468\u4E94": 5,
319
- "\u661F\u671F\u4E94": 5,
320
- "friday": 5,
321
- "fri": 5,
322
- "\u5468\u516D": 6,
323
- "\u661F\u671F\u516D": 6,
324
- "saturday": 6,
325
- "sat": 6
326
- };
327
- const isNextWeek = lower.includes("\u4E0B\u5468") || lower.includes("next");
328
- for (const [name, targetDay] of Object.entries(weekdayMap)) {
329
- if (lower.includes(name)) {
330
- const curDay = new Date(now).getDay();
331
- let diff = targetDay - curDay;
332
- if (diff <= 0 || isNextWeek) {
333
- diff += 7;
263
+ for (const msg of commitMessages) {
264
+ const msgLower = msg.toLowerCase();
265
+ if (msgLower.includes(idLower) || msgLower.includes(numId)) {
266
+ return { matched: true, message: msg };
267
+ }
268
+ if (titleTokens.length > 0) {
269
+ const hits = titleTokens.filter((tok) => msgLower.includes(tok)).length;
270
+ if (hits >= Math.min(2, titleTokens.length)) {
271
+ return { matched: true, message: msg };
334
272
  }
335
- return getTodayDateString(now + diff * MS_DAY);
336
273
  }
337
274
  }
338
- const fullDateMatch = titleOrDate.match(/\b(20\d\d)-(\d{1,2})-(\d{1,2})\b/);
339
- if (fullDateMatch) {
340
- const y = fullDateMatch[1];
341
- const m = fullDateMatch[2].padStart(2, "0");
342
- const d = fullDateMatch[3].padStart(2, "0");
343
- return `${y}-${m}-${d}`;
344
- }
345
- const mmddMatch = titleOrDate.match(/\b(\d{1,2})-(\d{1,2})\b/);
346
- if (mmddMatch) {
347
- const curYear = new Date(now).getFullYear();
348
- const m = mmddMatch[1].padStart(2, "0");
349
- const d = mmddMatch[2].padStart(2, "0");
350
- return `${curYear}-${m}-${d}`;
351
- }
352
- return void 0;
353
- }
354
- function detectRecurrence(title) {
355
- const lower = title.toLowerCase();
356
- if (lower.includes("\u6BCF\u5929") || lower.includes("\u6BCF\u65E5") || lower.includes("daily") || lower.includes("every day")) {
357
- return "daily";
358
- }
359
- return void 0;
275
+ return { matched: false };
360
276
  }
361
- function getTodayDateString(timestamp = Date.now()) {
362
- const d = new Date(timestamp);
363
- const year = d.getFullYear();
364
- const month = String(d.getMonth() + 1).padStart(2, "0");
365
- const day = String(d.getDate()).padStart(2, "0");
366
- return `${year}-${month}-${day}`;
277
+ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
278
+ let cwd;
279
+ let now = Date.now();
280
+ if (typeof cwdOrNow === "number") now = cwdOrNow;
281
+ else if (typeof cwdOrNow === "string") cwd = cwdOrNow;
282
+ if (typeof nowArg === "number") now = nowArg;
283
+ sweepQuietNotes(store, now);
284
+ const activeNotes = store.list({ status: "active", cwd });
285
+ if (activeNotes.length === 0) return "";
286
+ const lines = activeNotes.map((a) => {
287
+ const num = a.id.replace(/^anc-/, "");
288
+ const files = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(", ")})` : "";
289
+ 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]}` : ""}]` : "";
290
+ const desc = a.description ? ` \u2014 ${a.description}` : "";
291
+ return `\u2022 #${num} [${a.project}] ${a.title}${prog}${files}${desc}`;
292
+ });
293
+ return [
294
+ `<sticky-notes count="${activeNotes.length}">`,
295
+ `Pinned notes from previous sessions:`,
296
+ lines.join("\n"),
297
+ `When a note is completed, call anchor(action="settle", id="...") to tear it off.`,
298
+ `</sticky-notes>`
299
+ ].join("\n");
367
300
  }
368
- function getDefaultStorageDir() {
369
- if (process.env.ANCHOR_DIR) {
370
- return import_node_path2.default.resolve(process.env.ANCHOR_DIR);
371
- }
372
- const primaryDir = import_node_path2.default.join(import_node_os.default.homedir(), ".anchor");
373
- const legacyPiDir = import_node_path2.default.join(import_node_os.default.homedir(), ".pi", "agent", "anchors");
374
- if (!import_node_fs.default.existsSync(primaryDir) && import_node_fs.default.existsSync(legacyPiDir)) {
375
- try {
376
- import_node_fs.default.mkdirSync(primaryDir, { recursive: true });
377
- for (const file of ["state.json", "archive.jsonl", "graveyard.jsonl"]) {
378
- const src = import_node_path2.default.join(legacyPiDir, file);
379
- const dest = import_node_path2.default.join(primaryDir, file);
380
- if (import_node_fs.default.existsSync(src) && !import_node_fs.default.existsSync(dest)) {
381
- import_node_fs.default.copyFileSync(src, dest);
382
- }
301
+ var renderActiveAnchorsContext = renderColdStartAnchorsContext;
302
+ function sweepQuietNotes(store, now = Date.now()) {
303
+ return store.withLock(() => {
304
+ const state = store.loadState();
305
+ const quieted = [];
306
+ for (const a of state.anchors) {
307
+ if (a.status === "active" && now - a.lastTouchedAt > QUIET_AFTER_DAYS * MS_PER_DAY) {
308
+ a.status = "sleeping";
309
+ a.updatedAt = now;
310
+ quieted.push(a.id);
383
311
  }
384
- } catch {
385
312
  }
386
- }
387
- return primaryDir;
313
+ if (quieted.length > 0) {
314
+ state.lastSweepAt = now;
315
+ store.saveState(state);
316
+ }
317
+ return { quieted };
318
+ });
319
+ }
320
+ function getDefaultStorageDir() {
321
+ if (process.env.ANCHOR_DIR) return import_node_path.default.resolve(process.env.ANCHOR_DIR);
322
+ return import_node_path.default.join(import_node_os.default.homedir(), ".anchor");
388
323
  }
389
324
  function sleepSync(ms) {
390
325
  const end = Date.now() + ms;
@@ -406,22 +341,18 @@ function acquireSyncLock(lockPath, maxWaitMs = 1500, staleTimeoutMs = 5e3) {
406
341
  attempt++;
407
342
  try {
408
343
  const fd = import_node_fs.default.openSync(lockPath, "wx");
409
- const meta = { pid: process.pid, createdAt: Date.now() };
410
- import_node_fs.default.writeFileSync(fd, JSON.stringify(meta), "utf-8");
344
+ import_node_fs.default.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: Date.now() }), "utf-8");
411
345
  import_node_fs.default.closeSync(fd);
412
346
  return () => {
413
347
  try {
414
- if (import_node_fs.default.existsSync(lockPath)) {
415
- import_node_fs.default.unlinkSync(lockPath);
416
- }
348
+ if (import_node_fs.default.existsSync(lockPath)) import_node_fs.default.unlinkSync(lockPath);
417
349
  } catch {
418
350
  }
419
351
  };
420
352
  } catch (err) {
421
353
  if (err.code === "EEXIST") {
422
354
  try {
423
- const content = import_node_fs.default.readFileSync(lockPath, "utf-8");
424
- const meta = JSON.parse(content);
355
+ const meta = JSON.parse(import_node_fs.default.readFileSync(lockPath, "utf-8"));
425
356
  const isStale = Date.now() - meta.createdAt > staleTimeoutMs;
426
357
  const isDead = meta.pid && !isProcessAlive(meta.pid);
427
358
  if (isStale || isDead) {
@@ -433,8 +364,7 @@ function acquireSyncLock(lockPath, maxWaitMs = 1500, staleTimeoutMs = 5e3) {
433
364
  }
434
365
  } catch {
435
366
  }
436
- const delay = Math.min(40, Math.floor(attempt * 4 + Math.random() * 8));
437
- sleepSync(delay);
367
+ sleepSync(Math.min(40, Math.floor(attempt * 4 + Math.random() * 8)));
438
368
  continue;
439
369
  }
440
370
  break;
@@ -443,50 +373,46 @@ function acquireSyncLock(lockPath, maxWaitMs = 1500, staleTimeoutMs = 5e3) {
443
373
  return () => {
444
374
  };
445
375
  }
446
- function atomicRenameWithRetry(tempPath, targetPath, maxAttempts = 6) {
447
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
376
+ function atomicRenameWithRetry(tempPath, targetPath, maxRetries = 5) {
377
+ let lastErr;
378
+ for (let i = 0; i < maxRetries; i++) {
448
379
  try {
449
380
  import_node_fs.default.renameSync(tempPath, targetPath);
450
381
  return;
451
382
  } catch (err) {
452
- const code = err?.code;
453
- if ((code === "EBUSY" || code === "EPERM" || code === "EACCES") && attempt < maxAttempts) {
454
- const delay = Math.floor(attempt * 20 + Math.random() * 15);
455
- sleepSync(delay);
383
+ lastErr = err;
384
+ if (err.code === "EPERM" || err.code === "EACCES" || err.code === "EBUSY") {
385
+ sleepSync(15 * (i + 1));
456
386
  continue;
457
387
  }
458
- const crashDump = `${tempPath}.failed`;
459
- try {
460
- if (import_node_fs.default.existsSync(tempPath)) import_node_fs.default.renameSync(tempPath, crashDump);
461
- } catch {
462
- }
463
- throw err;
388
+ break;
464
389
  }
465
390
  }
391
+ try {
392
+ import_node_fs.default.copyFileSync(tempPath, targetPath);
393
+ import_node_fs.default.unlinkSync(tempPath);
394
+ } catch {
395
+ throw lastErr;
396
+ }
466
397
  }
467
398
  var AnchorStore = class {
468
399
  storageDir;
469
400
  statePath;
401
+ lockPath;
470
402
  archivePath;
471
- graveyardPath;
472
- constructor(customStorageDir) {
473
- if (customStorageDir) {
474
- this.storageDir = import_node_path2.default.resolve(customStorageDir);
475
- } else {
476
- this.storageDir = getDefaultStorageDir();
477
- }
478
- this.statePath = import_node_path2.default.join(this.storageDir, "state.json");
479
- this.archivePath = import_node_path2.default.join(this.storageDir, "archive.jsonl");
480
- this.graveyardPath = import_node_path2.default.join(this.storageDir, "graveyard.jsonl");
481
- this.ensureDirs();
403
+ constructor(storageDir) {
404
+ this.storageDir = storageDir ? import_node_path.default.resolve(storageDir) : getDefaultStorageDir();
405
+ this.statePath = import_node_path.default.join(this.storageDir, "state.json");
406
+ this.lockPath = import_node_path.default.join(this.storageDir, "state.lock");
407
+ this.archivePath = import_node_path.default.join(this.storageDir, "archive.jsonl");
482
408
  }
483
- get lockPath() {
484
- return import_node_path2.default.join(this.storageDir, "state.lock");
409
+ ensureDirs() {
410
+ if (!import_node_fs.default.existsSync(this.storageDir)) {
411
+ import_node_fs.default.mkdirSync(this.storageDir, { recursive: true });
412
+ }
485
413
  }
486
- /**
487
- * Execute mutation within a cross-process exclusive lock
488
- */
489
414
  withLock(fn) {
415
+ this.ensureDirs();
490
416
  const release = acquireSyncLock(this.lockPath);
491
417
  try {
492
418
  return fn();
@@ -494,84 +420,35 @@ var AnchorStore = class {
494
420
  release();
495
421
  }
496
422
  }
497
- ensureDirs() {
498
- if (!import_node_fs.default.existsSync(this.storageDir)) {
499
- try {
500
- import_node_fs.default.mkdirSync(this.storageDir, { recursive: true });
501
- } catch {
502
- }
503
- }
504
- }
505
- /**
506
- * Load store state. If corrupt or missing, returns safe default.
507
- */
508
423
  loadState() {
509
424
  this.ensureDirs();
510
425
  if (!import_node_fs.default.existsSync(this.statePath)) {
511
- return {
512
- version: 1,
513
- anchors: [],
514
- lastSweepAt: Date.now()
515
- };
516
- }
517
- let raw = null;
518
- for (let attempt = 1; attempt <= 3; attempt++) {
519
- try {
520
- raw = import_node_fs.default.readFileSync(this.statePath, "utf-8");
521
- break;
522
- } catch (err) {
523
- if ((err?.code === "EBUSY" || err?.code === "EPERM") && attempt < 3) {
524
- sleepSync(15);
525
- continue;
526
- }
527
- }
528
- }
529
- if (raw === null) {
530
- return { version: 1, anchors: [], lastSweepAt: Date.now() };
426
+ return { version: 1, anchors: [], lastSweepAt: Date.now(), nextId: 1 };
531
427
  }
532
428
  try {
533
- const data = JSON.parse(raw);
534
- if (!data || typeof data !== "object" || !Array.isArray(data.anchors)) {
535
- throw new Error("Invalid state: anchors must be array");
429
+ const parsed = JSON.parse(import_node_fs.default.readFileSync(this.statePath, "utf-8"));
430
+ if (!parsed || !Array.isArray(parsed.anchors)) {
431
+ return { version: 1, anchors: [], lastSweepAt: Date.now(), nextId: 1 };
536
432
  }
537
- data.anchors = data.anchors.filter((a) => a && typeof a === "object" && typeof a.id === "string" && typeof a.title === "string").map((a) => ({
538
- ...a,
539
- files: Array.isArray(a.files) ? a.files.filter((f) => typeof f === "string") : [],
540
- tags: Array.isArray(a.tags) ? a.tags.filter((t) => typeof t === "string") : [],
541
- decay: {
542
- activeDays: Number.isFinite(a.decay?.activeDays) && a.decay.activeDays > 0 ? a.decay.activeDays : DURABLE_DECAY_POLICY.activeDays,
543
- sleepDays: Number.isFinite(a.decay?.sleepDays) && a.decay.sleepDays > 0 ? a.decay.sleepDays : DURABLE_DECAY_POLICY.sleepDays,
544
- graveyardDays: Number.isFinite(a.decay?.graveyardDays) && a.decay.graveyardDays > 0 ? a.decay.graveyardDays : DURABLE_DECAY_POLICY.graveyardDays
545
- }
546
- }));
547
- return data;
433
+ return parsed;
548
434
  } catch {
549
- const backupPath = import_node_path2.default.join(this.storageDir, `state.corrupt.${Date.now()}.json`);
550
435
  try {
551
- import_node_fs.default.renameSync(this.statePath, backupPath);
436
+ import_node_fs.default.copyFileSync(this.statePath, import_node_path.default.join(this.storageDir, `state.corrupt.${Date.now()}.json`));
552
437
  } catch {
553
438
  }
554
- return {
555
- version: 1,
556
- anchors: [],
557
- lastSweepAt: Date.now()
558
- };
439
+ return { version: 1, anchors: [], lastSweepAt: Date.now(), nextId: 1 };
559
440
  }
560
441
  }
561
- /**
562
- * Atomically save store state via temp file + atomic rename with Windows NTFS spin-retry
563
- */
564
442
  saveState(state) {
565
443
  this.ensureDirs();
566
- const rand = Math.random().toString(36).slice(2, 7);
567
- const tempPath = import_node_path2.default.join(this.storageDir, `state.tmp.${process.pid}.${Date.now()}.${rand}`);
444
+ const tempPath = `${this.statePath}.${process.pid}.${Date.now()}.tmp`;
568
445
  import_node_fs.default.writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf-8");
569
446
  atomicRenameWithRetry(tempPath, this.statePath);
570
447
  }
571
448
  generateId(state) {
572
449
  const numbers = state.anchors.map((a) => {
573
- const match = a.id.match(/^anc-(\d+)$/);
574
- return match ? parseInt(match[1], 10) : 0;
450
+ const m = a.id.match(/^anc-(\d+)$/);
451
+ return m ? parseInt(m[1], 10) : 0;
575
452
  }).filter((n) => n > 0);
576
453
  const maxExisting = numbers.length > 0 ? Math.max(...numbers) : 0;
577
454
  const nextNum = Math.max(maxExisting + 1, state.nextId || 1);
@@ -579,38 +456,30 @@ var AnchorStore = class {
579
456
  return `anc-${nextNum}`;
580
457
  }
581
458
  /**
582
- * Create a new Anchor with automatic project and cwd tagging
459
+ * Pin a new sticky note
583
460
  */
584
461
  create(input) {
585
462
  return this.withLock(() => {
586
463
  const state = this.loadState();
587
464
  const now = Date.now();
588
465
  const id = this.generateId(state);
466
+ const { cleanTitle, progress: parsedProgress } = extractProgressFromText(input.title);
589
467
  const explicitFiles = (input.files || []).map((f) => normalizePath(f)).filter(Boolean);
590
- const inferredFiles = explicitFiles.length > 0 ? explicitFiles : extractPathsFromText(`${input.title} ${input.description || ""}`);
468
+ const inferredFiles = explicitFiles.length > 0 ? explicitFiles : extractPathsFromText(`${cleanTitle} ${input.description || ""}`);
591
469
  let cwd = input.cwd ? normalizePath(input.cwd) : "";
592
470
  if (input.project && (/^[a-zA-Z]:[\\/]/.test(input.project.trim()) || input.project.trim().startsWith("/"))) {
593
471
  cwd = normalizePath(input.project.trim());
594
472
  } else if (inferredFiles.length > 0 && (/^[a-zA-Z]:\//.test(inferredFiles[0]) || inferredFiles[0].startsWith("/"))) {
595
- const firstDir = import_node_path2.default.dirname(inferredFiles[0]);
596
- if (firstDir && firstDir !== ".") {
597
- cwd = normalizePath(firstDir);
598
- }
473
+ const firstDir = import_node_path.default.dirname(inferredFiles[0]);
474
+ if (firstDir && firstDir !== ".") cwd = normalizePath(firstDir);
599
475
  }
600
- const projectName = input.project ? input.project.trim() : cwd ? import_node_path2.default.basename(cwd) : "global";
601
- const recurrence = input.recurrence || detectRecurrence(input.title);
602
- const targetDate = input.targetDate || detectTargetDate(input.title, now);
603
- const durability = recurrence ? "durable" : input.durability || detectDurability(input.title);
604
- const decayPolicy = durability === "ephemeral" ? getEphemeralDecayPolicy(input.title) : DURABLE_DECAY_POLICY;
476
+ const projectName = input.project ? input.project.trim() : cwd ? import_node_path.default.basename(cwd) : "global";
605
477
  const anchor = {
606
478
  id,
607
- title: input.title.trim(),
479
+ title: cleanTitle,
608
480
  description: input.description?.trim(),
609
481
  priority: input.priority || "p1",
610
482
  status: "active",
611
- durability,
612
- recurrence,
613
- targetDate,
614
483
  project: projectName,
615
484
  cwd,
616
485
  createdAt: now,
@@ -618,8 +487,8 @@ var AnchorStore = class {
618
487
  lastTouchedAt: now,
619
488
  files: inferredFiles,
620
489
  tags: (input.tags || []).map((t) => t.trim()).filter(Boolean),
621
- verifyCommand: input.verifyCommand?.trim() || void 0,
622
- decay: input.decay ? { ...input.decay } : { ...decayPolicy }
490
+ ...input.sessionFile ? { sessionFile: normalizePath(input.sessionFile) } : {},
491
+ ...input.progress || parsedProgress ? { progress: input.progress || parsedProgress } : {}
623
492
  };
624
493
  state.anchors.push(anchor);
625
494
  this.saveState(state);
@@ -629,31 +498,19 @@ var AnchorStore = class {
629
498
  findAnchorIndex(anchors, id) {
630
499
  const clean = id.trim().replace(/^#/, "");
631
500
  return anchors.findIndex(
632
- (a) => a.id === clean || a.id === `anc-${clean}` || (clean.match(/^\d+$/) ? a.id === `anc-${parseInt(clean, 10)}` : false)
501
+ (a) => a.id === clean || a.id === `anc-${clean}` || (/^\d+$/.test(clean) ? a.id === `anc-${parseInt(clean, 10)}` : false)
633
502
  );
634
503
  }
635
- /**
636
- * Get an anchor by ID (supports 'anc-1', '#anc-1', '1', '01')
637
- */
638
504
  get(id) {
639
505
  const state = this.loadState();
640
506
  const idx = this.findAnchorIndex(state.anchors, id);
641
507
  return idx === -1 ? void 0 : state.anchors[idx];
642
508
  }
643
- /**
644
- * List anchors. By default, if cwd is provided, only returns anchors
645
- * belonging to this cwd or marked as global (''). Pass all: true for global view.
646
- */
647
509
  list(filter) {
648
510
  const state = this.loadState();
649
511
  const targetCwd = filter?.cwd ? normalizePath(filter.cwd) : null;
650
- const today = getTodayDateString(filter?.now);
651
512
  return state.anchors.filter((a) => {
652
- if (!filter?.all && filter?.status === "active" && a.recurrence === "daily" && a.lastCompletedDate === today) {
653
- return false;
654
- }
655
513
  if (filter?.status && a.status !== filter.status) return false;
656
- if (filter?.priority && a.priority !== filter.priority) return false;
657
514
  if (!filter?.all && targetCwd) {
658
515
  if (a.files.length > 0 && (a.cwd || a.project) && !isAnchorTargetingWorkspace(a, targetCwd)) {
659
516
  return false;
@@ -662,19 +519,13 @@ var AnchorStore = class {
662
519
  return true;
663
520
  });
664
521
  }
665
- /**
666
- * Update an existing anchor
667
- */
668
522
  update(id, patch) {
669
523
  return this.withLock(() => {
670
524
  const state = this.loadState();
671
525
  const idx = this.findAnchorIndex(state.anchors, id);
672
- if (idx === -1) {
673
- throw new Error(`Anchor not found: ${id}`);
674
- }
675
- const current = state.anchors[idx];
526
+ if (idx === -1) throw new Error(`Note not found: ${id}`);
676
527
  const updated = {
677
- ...current,
528
+ ...state.anchors[idx],
678
529
  ...patch,
679
530
  updatedAt: Date.now()
680
531
  };
@@ -686,60 +537,70 @@ var AnchorStore = class {
686
537
  return updated;
687
538
  });
688
539
  }
689
- /**
690
- * Touch an anchor to refresh its decay window and reactivate sleeping state
691
- */
692
- touch(id, timestamp = Date.now()) {
540
+ touch(id, timestamp = Date.now(), sessionFile) {
693
541
  return this.withLock(() => {
694
542
  const state = this.loadState();
695
543
  const idx = this.findAnchorIndex(state.anchors, id);
696
- if (idx === -1) {
697
- throw new Error(`Anchor not found: ${id}`);
698
- }
544
+ if (idx === -1) throw new Error(`Note not found: ${id}`);
699
545
  const current = state.anchors[idx];
700
546
  current.lastTouchedAt = timestamp;
701
547
  current.updatedAt = timestamp;
702
548
  if (current.status === "sleeping") {
703
549
  current.status = "active";
704
550
  }
551
+ if (sessionFile) {
552
+ current.sessionFile = normalizePath(sessionFile);
553
+ }
705
554
  state.anchors[idx] = current;
706
555
  this.saveState(state);
707
556
  return current;
708
557
  });
709
558
  }
710
559
  /**
711
- * Settle an anchor (recurring daily habits complete for today and wake up tomorrow)
560
+ * Advance (or set) progress on a sticky note:
561
+ * - If delta is provided (default +1), increments `done` up to `total`.
562
+ * - If note had no progress yet, initializes a 4-step bar (`1/4`).
712
563
  */
713
- settle(id, evidence = {}, now = Date.now()) {
564
+ step(id, delta = 1, explicitTotal) {
714
565
  return this.withLock(() => {
715
566
  const state = this.loadState();
716
567
  const idx = this.findAnchorIndex(state.anchors, id);
717
- if (idx === -1) {
718
- throw new Error(`Anchor not found: ${id}`);
719
- }
720
- const anchor = state.anchors[idx];
721
- const today = getTodayDateString(now);
722
- if (anchor.recurrence === "daily") {
723
- anchor.lastCompletedDate = today;
724
- anchor.lastTouchedAt = now;
725
- anchor.updatedAt = now;
726
- anchor.evidence = {
727
- ...evidence,
728
- settledAt: now,
729
- settledBy: evidence.settledBy || "manual-command"
730
- };
731
- this.ensureDirs();
732
- import_node_fs.default.appendFileSync(this.archivePath, JSON.stringify({ ...anchor, settledForDate: today }) + "\n", "utf-8");
733
- this.saveState(state);
734
- return anchor;
568
+ if (idx === -1) throw new Error(`Note not found: ${id}`);
569
+ const current = state.anchors[idx];
570
+ const now = Date.now();
571
+ const total = explicitTotal && explicitTotal >= 2 ? explicitTotal : current.progress?.total || 4;
572
+ const prevDone = current.progress?.done || 0;
573
+ const nextDone = Math.max(0, Math.min(total, prevDone + delta));
574
+ current.progress = {
575
+ ...current.progress || {},
576
+ done: nextDone,
577
+ total
578
+ };
579
+ current.lastTouchedAt = now;
580
+ current.updatedAt = now;
581
+ if (current.status === "sleeping") {
582
+ current.status = "active";
735
583
  }
584
+ state.anchors[idx] = current;
585
+ this.saveState(state);
586
+ return { anchor: current, completed: nextDone >= total };
587
+ });
588
+ }
589
+ /**
590
+ * Tear off (settle) a completed sticky note and archive it to archive.jsonl
591
+ */
592
+ settle(id, evidence = {}, now = Date.now()) {
593
+ return this.withLock(() => {
594
+ const state = this.loadState();
595
+ const idx = this.findAnchorIndex(state.anchors, id);
596
+ if (idx === -1) throw new Error(`Note not found: ${id}`);
736
597
  const [settled] = state.anchors.splice(idx, 1);
737
598
  settled.status = "settled";
738
599
  settled.updatedAt = now;
739
600
  settled.evidence = {
740
601
  ...evidence,
741
602
  settledAt: now,
742
- settledBy: evidence.settledBy || "manual-command"
603
+ settledBy: evidence.settledBy || "manual"
743
604
  };
744
605
  this.ensureDirs();
745
606
  import_node_fs.default.appendFileSync(this.archivePath, JSON.stringify(settled) + "\n", "utf-8");
@@ -748,12 +609,12 @@ var AnchorStore = class {
748
609
  });
749
610
  }
750
611
  /**
751
- * Reverse/undo the last settled anchor, popping it from archive.jsonl back into state.json
612
+ * Put back (undo) the last torn-off sticky note
752
613
  */
753
614
  undoSettle() {
754
615
  return this.withLock(() => {
755
616
  if (!import_node_fs.default.existsSync(this.archivePath)) {
756
- throw new Error("No archived anchors to undo");
617
+ throw new Error("No archived notes to restore");
757
618
  }
758
619
  const raw = import_node_fs.default.readFileSync(this.archivePath, "utf-8");
759
620
  const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
@@ -762,23 +623,18 @@ var AnchorStore = class {
762
623
  const candidate = lines.pop();
763
624
  try {
764
625
  const parsed = JSON.parse(candidate);
765
- if (parsed && typeof parsed === "object" && typeof parsed.id === "string" && typeof parsed.title === "string") {
626
+ if (parsed && typeof parsed.id === "string" && typeof parsed.title === "string") {
766
627
  anchor = parsed;
767
628
  }
768
629
  } catch {
769
630
  }
770
631
  }
771
- if (!anchor) {
772
- throw new Error("Archive is empty, nothing to undo");
773
- }
632
+ if (!anchor) throw new Error("Archive is empty, nothing to undo");
774
633
  import_node_fs.default.writeFileSync(this.archivePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
775
634
  anchor.status = "active";
776
635
  anchor.updatedAt = Date.now();
636
+ anchor.lastTouchedAt = Date.now();
777
637
  delete anchor.evidence;
778
- delete anchor.settledForDate;
779
- if (anchor.recurrence === "daily") {
780
- delete anchor.lastCompletedDate;
781
- }
782
638
  const state = this.loadState();
783
639
  const existingIdx = state.anchors.findIndex((a) => a.id === anchor.id);
784
640
  if (existingIdx !== -1) {
@@ -789,320 +645,19 @@ var AnchorStore = class {
789
645
  state.anchors.push(anchor);
790
646
  }
791
647
  } else {
792
- const match = anchor.id.match(/^anc-(\d+)$/);
793
- if (match) {
794
- const restoredNum = parseInt(match[1], 10);
795
- state.nextId = Math.max(state.nextId || 1, restoredNum + 1);
796
- }
648
+ const m = anchor.id.match(/^anc-(\d+)$/);
649
+ if (m) state.nextId = Math.max(state.nextId || 1, parseInt(m[1], 10) + 1);
797
650
  state.anchors.push(anchor);
798
651
  }
799
652
  this.saveState(state);
800
653
  return anchor;
801
654
  });
802
655
  }
803
- /**
804
- * Evict anchor to graveyard.jsonl upon total decay expiry
805
- */
806
- dropToGraveyard(id, reason) {
807
- return this.withLock(() => {
808
- const state = this.loadState();
809
- const idx = state.anchors.findIndex((a) => a.id === id);
810
- if (idx === -1) {
811
- throw new Error(`Anchor not found: ${id}`);
812
- }
813
- const [anchor] = state.anchors.splice(idx, 1);
814
- anchor.status = "graveyard";
815
- anchor.updatedAt = Date.now();
816
- anchor.evidence = {
817
- summary: reason,
818
- settledAt: Date.now()
819
- };
820
- this.ensureDirs();
821
- import_node_fs.default.appendFileSync(this.graveyardPath, JSON.stringify(anchor) + "\n", "utf-8");
822
- this.saveState(state);
823
- return anchor;
824
- });
825
- }
826
- getArchive(filter) {
827
- if (!import_node_fs.default.existsSync(this.archivePath)) return [];
828
- try {
829
- const raw = import_node_fs.default.readFileSync(this.archivePath, "utf-8");
830
- const all = [];
831
- for (const l of raw.split(/\r?\n/)) {
832
- const trimmed = l.trim();
833
- if (!trimmed) continue;
834
- try {
835
- const parsed = JSON.parse(trimmed);
836
- if (parsed && typeof parsed === "object" && typeof parsed.id === "string") all.push(parsed);
837
- } catch {
838
- }
839
- }
840
- if (filter?.all || !filter?.cwd) return all;
841
- const targetCwd = normalizePath(filter.cwd);
842
- return all.filter((a) => !a.cwd || isSameOrSubPath(a.cwd, targetCwd));
843
- } catch {
844
- return [];
845
- }
846
- }
847
- getGraveyard(filter) {
848
- if (!import_node_fs.default.existsSync(this.graveyardPath)) return [];
849
- try {
850
- const raw = import_node_fs.default.readFileSync(this.graveyardPath, "utf-8");
851
- const all = [];
852
- for (const l of raw.split(/\r?\n/)) {
853
- const trimmed = l.trim();
854
- if (!trimmed) continue;
855
- try {
856
- const parsed = JSON.parse(trimmed);
857
- if (parsed && typeof parsed === "object" && typeof parsed.id === "string") all.push(parsed);
858
- } catch {
859
- }
860
- }
861
- if (filter?.all || !filter?.cwd) return all;
862
- const targetCwd = normalizePath(filter.cwd);
863
- return all.filter((a) => !a.cwd || isSameOrSubPath(a.cwd, targetCwd));
864
- } catch {
865
- return [];
866
- }
867
- }
868
656
  };
869
657
 
870
- // src/decay.ts
871
- var import_node_fs2 = __toESM(require("fs"), 1);
872
- var MS_PER_DAY = 864e5;
873
- function evaluateAnchorDecay(anchor, now = Date.now()) {
874
- if (anchor.status === "settled" || anchor.status === "graveyard") {
875
- return {
876
- currentStatus: anchor.status,
877
- nextStatus: anchor.status,
878
- daysUntouched: 0,
879
- remainingActiveDays: 0,
880
- remainingSleepDays: 0
881
- };
882
- }
883
- const elapsedMs = Math.max(0, now - anchor.lastTouchedAt);
884
- const daysUntouched = elapsedMs / MS_PER_DAY;
885
- const activeDays = anchor.decay.activeDays;
886
- const graveyardDays = anchor.decay.graveyardDays;
887
- let nextStatus = anchor.status;
888
- if (daysUntouched <= activeDays) {
889
- nextStatus = "active";
890
- } else if (daysUntouched <= graveyardDays) {
891
- nextStatus = "sleeping";
892
- } else {
893
- nextStatus = "graveyard";
894
- }
895
- return {
896
- currentStatus: anchor.status,
897
- nextStatus,
898
- daysUntouched: Math.round(daysUntouched * 10) / 10,
899
- remainingActiveDays: Math.max(0, Math.round((activeDays - daysUntouched) * 10) / 10),
900
- remainingSleepDays: Math.max(0, Math.round((graveyardDays - daysUntouched) * 10) / 10)
901
- };
902
- }
903
- function sweepStore(store, now = Date.now()) {
904
- return store.withLock(() => {
905
- const state = store.loadState();
906
- const result = {
907
- transitionedToSleeping: [],
908
- wokenToActive: [],
909
- evictedToGraveyard: []
910
- };
911
- const toEvict = [];
912
- let stateModified = false;
913
- for (const anchor of state.anchors) {
914
- const evalResult = evaluateAnchorDecay(anchor, now);
915
- if (evalResult.nextStatus === "graveyard") {
916
- toEvict.push(anchor);
917
- } else if (evalResult.nextStatus !== anchor.status) {
918
- if (evalResult.nextStatus === "sleeping" && anchor.status === "active") {
919
- anchor.status = "sleeping";
920
- anchor.updatedAt = now;
921
- result.transitionedToSleeping.push(anchor.id);
922
- stateModified = true;
923
- } else if (evalResult.nextStatus === "active" && anchor.status === "sleeping") {
924
- anchor.status = "active";
925
- anchor.updatedAt = now;
926
- result.wokenToActive.push(anchor.id);
927
- stateModified = true;
928
- }
929
- }
930
- }
931
- if (toEvict.length > 0) {
932
- const evictIds = new Set(toEvict.map((e) => e.id));
933
- state.anchors = state.anchors.filter((a) => !evictIds.has(a.id));
934
- stateModified = true;
935
- const graveyardLines = toEvict.map((exp) => {
936
- const rec = {
937
- ...exp,
938
- status: "graveyard",
939
- updatedAt: now,
940
- evictedAt: now,
941
- evictionReason: `Exceeded decay threshold (${exp.decay.graveyardDays} days untouched)`
942
- };
943
- return JSON.stringify(rec);
944
- }).join("\n") + "\n";
945
- import_node_fs2.default.appendFileSync(store.graveyardPath, graveyardLines, "utf-8");
946
- for (const exp of toEvict) {
947
- result.evictedToGraveyard.push(exp.id);
948
- }
949
- }
950
- state.lastSweepAt = now;
951
- if (stateModified) {
952
- store.saveState(state);
953
- }
954
- return result;
955
- });
956
- }
957
-
958
- // src/settlement.ts
959
- var import_node_child_process = require("child_process");
960
- function runPhysicalVerification(anchor, cwd) {
961
- if (!anchor.verifyCommand?.trim()) {
962
- return { success: false, output: "No verification command specified" };
963
- }
964
- const targetCwd = anchor.cwd || cwd;
965
- try {
966
- const stdout = (0, import_node_child_process.execSync)(anchor.verifyCommand, {
967
- cwd: targetCwd,
968
- encoding: "utf-8",
969
- timeout: 8e3,
970
- maxBuffer: 4 * 1024 * 1024,
971
- stdio: ["ignore", "pipe", "pipe"]
972
- });
973
- return { success: true, output: stdout.trim() };
974
- } catch (err) {
975
- return { success: false, output: String(err.stderr || err.stdout || err.message).trim() };
976
- }
977
- }
978
- function tryRealtimeAutoSettle(store, observer, cwd, executedCommand) {
979
- const candidates = store.list({ cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
980
- if (candidates.length === 0) return [];
981
- const settledList = [];
982
- const cmdTrimmed = (executedCommand || "").trim();
983
- for (const a of candidates) {
984
- if (observer.hasCommitted()) {
985
- const commitMatch = observer.matchesCommit(a);
986
- if (commitMatch.matched && (!cwd || isAnchorTargetingWorkspace(a, cwd))) {
987
- const settled = store.settle(a.id, {
988
- settledBy: "verification-test",
989
- summary: `Auto-settled via Git commit: ${commitMatch.message}`,
990
- touchedFiles: observer.getModifiedFiles()
991
- });
992
- settledList.push({ anchor: settled, reason: `git commit ("${commitMatch.message}")` });
993
- continue;
994
- }
995
- }
996
- if (a.verifyCommand && cmdTrimmed && cmdTrimmed.includes(a.verifyCommand.trim())) {
997
- const settled = store.settle(a.id, {
998
- settledBy: "verification-test",
999
- summary: `Auto-settled via passing command: ${a.verifyCommand}`,
1000
- touchedFiles: observer.getModifiedFiles()
1001
- });
1002
- settledList.push({ anchor: settled, reason: `verifyCommand (${a.verifyCommand})` });
1003
- }
1004
- }
1005
- return settledList;
1006
- }
1007
- function generateSettlementProposals(store, touchedFiles, cwd, observer) {
1008
- const activeAndSleeping = store.list({ cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
1009
- const matches = findMatchedAnchors(activeAndSleeping, touchedFiles);
1010
- const seenIds = /* @__PURE__ */ new Set();
1011
- const proposals = [];
1012
- for (const match of matches) {
1013
- seenIds.add(match.anchor.id);
1014
- const fileList = match.matchedFiles.slice(0, 3).join(", ");
1015
- const moreSuffix = match.matchedFiles.length > 3 ? ` (+${match.matchedFiles.length - 3} more)` : "";
1016
- proposals.push({
1017
- anchor: match.anchor,
1018
- matchedFiles: match.matchedFiles,
1019
- reason: `Modified ${fileList}${moreSuffix}`,
1020
- recommendedAction: match.score >= 0.85 ? "settle" : "defer"
1021
- });
1022
- }
1023
- if (observer && observer.hasCommitted()) {
1024
- for (const a of activeAndSleeping) {
1025
- if (seenIds.has(a.id)) continue;
1026
- const cm = observer.matchesCommit(a);
1027
- if (cm.matched) {
1028
- seenIds.add(a.id);
1029
- proposals.push({
1030
- anchor: a,
1031
- matchedFiles: touchedFiles.slice(0, 3),
1032
- reason: `Matched git commit: "${cm.message}"`,
1033
- recommendedAction: "settle"
1034
- });
1035
- }
1036
- }
1037
- }
1038
- return proposals;
1039
- }
1040
- function formatSettlementCard(proposal) {
1041
- const a = proposal.anchor;
1042
- const tagStr = a.tags.length > 0 ? ` [${a.tags.join(", ")}]` : "";
1043
- const fileStr = proposal.matchedFiles.slice(0, 2).join(", ");
1044
- return [
1045
- `\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`,
1046
- `\u2693 \u5173\u95E8\u7ED3\u6848\u63D0\u8BAE | Anchor Settlement`,
1047
- ` \u4EFB\u52A1 #${a.id}: ${a.title}${tagStr} (${a.priority.toUpperCase()})`,
1048
- ` \u8BC1\u636E\u89E6\u53D1: \u4FEE\u6539\u4E86 ${fileStr}`,
1049
- ` [Enter \u786E\u8BA4\u7ED3\u6848\u5E76\u5F52\u6863] / [Tab \u6682\u672A\u5B8C\u6210\uFF0C\u7EE7\u7EED\u6302\u8D77]`,
1050
- `\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`
1051
- ].join("\n");
1052
- }
1053
-
1054
658
  // src/tui.ts
1055
- var import_node_path3 = __toESM(require("path"), 1);
1056
- function classifyAnchor(anchor, now = Date.now()) {
1057
- if (anchor.recurrence === "daily") {
1058
- return "Habits";
1059
- }
1060
- if (!anchor.targetDate) {
1061
- return "Backlog";
1062
- }
1063
- const todayStr = getTodayDateString(now);
1064
- if (anchor.targetDate <= todayStr) {
1065
- return "Today";
1066
- }
1067
- return "Upcoming";
1068
- }
1069
- function groupAnchorsByQuadrant(anchors, now = Date.now()) {
1070
- const groups = {
1071
- today: [],
1072
- upcoming: [],
1073
- habits: [],
1074
- backlog: []
1075
- };
1076
- for (const a of anchors) {
1077
- const q = classifyAnchor(a, now);
1078
- if (q === "Today") groups.today.push(a);
1079
- else if (q === "Upcoming") groups.upcoming.push(a);
1080
- else if (q === "Habits") groups.habits.push(a);
1081
- else groups.backlog.push(a);
1082
- }
1083
- return groups;
1084
- }
1085
- var PRIORITY_RANK = { p0: 0, p1: 1, p2: 2 };
1086
- function sortAnchorsForDisplay(anchors, cwd, now = Date.now()) {
1087
- const todayStr = getTodayDateString(now);
1088
- return [...anchors].sort((a, b) => {
1089
- if (a.status !== b.status) {
1090
- return a.status === "active" ? -1 : 1;
1091
- }
1092
- if (cwd) {
1093
- const aMatch = isAnchorTargetingWorkspace(a, cwd) ? 0 : 1;
1094
- const bMatch = isAnchorTargetingWorkspace(b, cwd) ? 0 : 1;
1095
- if (aMatch !== bMatch) return aMatch - bMatch;
1096
- }
1097
- const aDue = a.targetDate && a.targetDate <= todayStr ? 0 : 1;
1098
- const bDue = b.targetDate && b.targetDate <= todayStr ? 0 : 1;
1099
- if (aDue !== bDue) return aDue - bDue;
1100
- const pA = PRIORITY_RANK[a.priority] ?? 1;
1101
- const pB = PRIORITY_RANK[b.priority] ?? 1;
1102
- if (pA !== pB) return pA - pB;
1103
- return a.createdAt - b.createdAt;
1104
- });
1105
- }
659
+ var import_node_fs2 = __toESM(require("fs"), 1);
660
+ var import_node_path2 = __toESM(require("path"), 1);
1106
661
  var ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
1107
662
  function stripAnsi(str) {
1108
663
  return str.replace(ANSI_REGEX, "");
@@ -1154,14 +709,12 @@ function truncateToWidth(str, maxWidth, ellipsis = "\u2026") {
1154
709
  const charLen = codePoint > 65535 ? 2 : 1;
1155
710
  const fullChar = singleLine.slice(i, i + charLen);
1156
711
  const charWidth = getDisplayWidth(fullChar);
1157
- if (accumWidth + charWidth > target) {
1158
- break;
1159
- }
712
+ if (accumWidth + charWidth > target) break;
1160
713
  accumulated += fullChar;
1161
714
  accumWidth += charWidth;
1162
715
  i += charLen;
1163
716
  }
1164
- accumulated = accumulated.replace(/\s*[\(\[\{(【]$/, "");
717
+ accumulated = accumulated.replace(/\s*[\(\[\{(【][^)\]})】]*$/, "");
1165
718
  return accumulated + ellipsis + (singleLine.includes("\x1B") ? "\x1B[0m" : "");
1166
719
  }
1167
720
  function padToWidth(str, targetWidth) {
@@ -1170,90 +723,68 @@ function padToWidth(str, targetWidth) {
1170
723
  if (current >= targetWidth) return truncated;
1171
724
  return truncated + " ".repeat(targetWidth - current);
1172
725
  }
1173
- function formatTargetDate(anchor, now = Date.now()) {
1174
- if (anchor.recurrence === "daily") {
1175
- return "Daily";
1176
- }
1177
- if (!anchor.targetDate) {
1178
- return "Someday";
1179
- }
1180
- const todayStr = getTodayDateString(now);
1181
- if (anchor.targetDate === todayStr) {
1182
- return "Today";
1183
- }
1184
- const MS_DAY = 24 * 60 * 60 * 1e3;
1185
- const tomorrowStr = getTodayDateString(now + MS_DAY);
1186
- if (anchor.targetDate === tomorrowStr) {
1187
- return "Tomorrow";
1188
- }
1189
- const in2dStr = getTodayDateString(now + 2 * MS_DAY);
1190
- if (anchor.targetDate === in2dStr) {
1191
- return "In 2d";
1192
- }
1193
- const in3dStr = getTodayDateString(now + 3 * MS_DAY);
1194
- if (anchor.targetDate === in3dStr) {
1195
- return "In 3d";
1196
- }
1197
- if (anchor.targetDate < todayStr) {
1198
- return "Overdue";
1199
- }
1200
- const parts = anchor.targetDate.split("-");
1201
- if (parts.length === 3) {
1202
- return `${parts[1]}-${parts[2]}`;
1203
- }
1204
- return anchor.targetDate;
726
+ function formatProgressBar(progress, cells = 4) {
727
+ if (!progress || !progress.total || progress.total < 1) return "";
728
+ const done = Math.max(0, Math.min(progress.total, progress.done || 0));
729
+ const filled = Math.min(cells, Math.max(done > 0 ? 1 : 0, Math.round(done / progress.total * cells)));
730
+ const finalFilled = done >= progress.total ? cells : done === 0 ? 0 : filled;
731
+ const bar = "\u25A0".repeat(finalFilled) + "\u25A1".repeat(Math.max(0, cells - finalFilled));
732
+ return `[${bar} ${done}/${progress.total}]`;
733
+ }
734
+ function formatRelativeAge(timestamp, now = Date.now()) {
735
+ const diffSec = Math.max(0, Math.floor((now - timestamp) / 1e3));
736
+ if (diffSec < 60) return "just now";
737
+ const diffMin = Math.floor(diffSec / 60);
738
+ if (diffMin < 60) return `${diffMin}m ago`;
739
+ const diffHours = Math.floor(diffMin / 60);
740
+ if (diffHours < 24) return `${diffHours}h ago`;
741
+ const diffDays = Math.floor(diffHours / 24);
742
+ return `${diffDays}d ago`;
1205
743
  }
1206
- function formatCreationTime(timestamp, now = Date.now()) {
1207
- const d = new Date(timestamp);
1208
- const nowD = new Date(now);
1209
- const pad = (n) => String(n).padStart(2, "0");
1210
- const timeStr = `${pad(d.getHours())}:${pad(d.getMinutes())}`;
1211
- const todayStr = getTodayDateString(now);
1212
- const createdDayStr = getTodayDateString(timestamp);
1213
- if (createdDayStr === todayStr) {
1214
- return `Today ${timeStr}`;
744
+ function formatOrigin(anchor) {
745
+ if (typeof anchor === "string") {
746
+ if (!anchor) return "global";
747
+ return import_node_path2.default.basename(anchor.replace(/\\/g, "/")) || "global";
1215
748
  }
1216
- const yesterdayStr = getTodayDateString(now - 24 * 60 * 60 * 1e3);
1217
- if (createdDayStr === yesterdayStr) {
1218
- return `Yesterday ${timeStr}`;
749
+ let proj = "global";
750
+ if (anchor.project && anchor.project !== "global") {
751
+ const normProj = anchor.project.replace(/\\/g, "/").replace(/\/+$/, "");
752
+ if (normProj.includes("/")) {
753
+ const parts = normProj.split("/").filter(Boolean);
754
+ proj = parts.slice(-2).join("/");
755
+ } else {
756
+ proj = normProj;
757
+ }
758
+ } else if (anchor.cwd) {
759
+ proj = import_node_path2.default.basename(anchor.cwd.replace(/\\/g, "/")) || "global";
1219
760
  }
1220
- const mmdd = `${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
1221
- if (d.getFullYear() === nowD.getFullYear()) {
1222
- return `${mmdd} ${timeStr}`;
761
+ if (anchor.files && anchor.files.length > 0) {
762
+ const shortFile = import_node_path2.default.basename(anchor.files[0].replace(/\\/g, "/"));
763
+ if (shortFile && !proj.endsWith(shortFile)) {
764
+ return `${proj} \xB7 ${shortFile}`;
765
+ }
1223
766
  }
1224
- return `${d.getFullYear()}-${mmdd}`;
1225
- }
1226
- function formatRelativeTime(timestamp, now = Date.now()) {
1227
- return formatCreationTime(timestamp, now);
767
+ return proj;
1228
768
  }
1229
- function formatRemainingTtl(anchor, now = Date.now()) {
1230
- return formatTargetDate(anchor, now);
1231
- }
1232
- function formatOrigin(anchor) {
1233
- if (typeof anchor === "object") {
1234
- if (anchor.project && anchor.project !== "global") {
1235
- const normProj = anchor.project.replace(/\\/g, "/").replace(/\/+$/, "");
1236
- if (normProj.includes("/")) {
1237
- const parts = normProj.split("/").filter(Boolean);
1238
- return parts.slice(-2).join("/");
1239
- }
1240
- return normProj;
769
+ function sortAnchorsForDisplay(anchors, cwd) {
770
+ return [...anchors].sort((a, b) => {
771
+ if (a.status !== b.status) {
772
+ return a.status === "active" ? -1 : 1;
1241
773
  }
1242
- if (!anchor.cwd) return "global";
1243
- return import_node_path3.default.basename(anchor.cwd.replace(/\\/g, "/")) || "global";
1244
- }
1245
- if (!anchor) return "global";
1246
- return import_node_path3.default.basename(anchor.replace(/\\/g, "/")) || "global";
774
+ if (cwd) {
775
+ const aMatch = isAnchorTargetingWorkspace(a, cwd) ? 0 : 1;
776
+ const bMatch = isAnchorTargetingWorkspace(b, cwd) ? 0 : 1;
777
+ if (aMatch !== bMatch) return aMatch - bMatch;
778
+ }
779
+ return b.lastTouchedAt - a.lastTouchedAt;
780
+ });
1247
781
  }
1248
782
  function updateAnchorStatusBar(ctx, store) {
1249
783
  if (!hasActiveUI(ctx)) return;
1250
784
  try {
1251
785
  const active = store.list({ status: "active", cwd: ctx.cwd });
1252
- if (active.length === 0) {
1253
- ctx.ui.setStatus("anchor", void 0);
1254
- return;
1255
- }
1256
- ctx.ui.setStatus("anchor", `\u2316 ${active.length}`);
786
+ ctx.ui.setStatus("anchor", active.length > 0 ? `\u2316 ${active.length}` : void 0);
787
+ updateStartupBanner(ctx, store);
1257
788
  } catch {
1258
789
  }
1259
790
  }
@@ -1262,446 +793,215 @@ function updateStartupBanner(ctx, store) {
1262
793
  try {
1263
794
  const active = store.list({ status: "active", cwd: ctx.cwd });
1264
795
  if (active.length === 0) {
1265
- ctx.ui.setWidget("anchor-startup", void 0);
796
+ ctx.ui.setWidget("anchor-startup", void 0, { placement: "belowEditor" });
1266
797
  return;
1267
798
  }
1268
- const groups = groupAnchorsByQuadrant(active);
1269
- const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
1270
- const topItems = sorted.slice(0, 3);
799
+ const sorted = sortAnchorsForDisplay(active, ctx.cwd);
1271
800
  const factory = (_tui, theme) => ({
1272
801
  render: (width) => {
1273
- const maxTitleWidth = Math.max(12, width - 36);
1274
802
  const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
1275
- const lines = [
1276
- truncateToWidth(fg("accent", `\u2316 Anchor \xB7 ${active.length} active commitments:`), width)
1277
- ];
1278
- for (let i = 0; i < topItems.length; i++) {
1279
- const a = topItems[i];
1280
- const num = a.id.replace(/^anc-/, "").padStart(2, "0");
1281
- const q = classifyAnchor(a);
1282
- const tgt = formatTargetDate(a);
1283
- const titlePadded = truncateToWidth(a.title, maxTitleWidth);
1284
- lines.push(truncateToWidth(` \u2022 ${fg("muted", `[${q}]`)} ${fg("dim", num)} ${titlePadded} ${fg("dim", `(${tgt})`)}`, width));
1285
- }
1286
- if (sorted.length > 3) {
1287
- lines.push(truncateToWidth(fg("dim", ` (+${sorted.length - 3} more \xB7 run /anchor to inspect)`), width));
803
+ if (sorted.length === 1) {
804
+ const a = sorted[0];
805
+ const num = a.id.replace(/^anc-/, "");
806
+ const bar = a.progress ? `${fg("accent", formatProgressBar(a.progress))} ` : "";
807
+ const nextStep = a.progress?.steps && a.progress.steps[a.progress.done] ? fg("muted", ` \u25B8 ${a.progress.steps[a.progress.done]}`) : "";
808
+ const origin = formatOrigin(a);
809
+ const sessionIcon = a.sessionFile ? " \u21AA" : "";
810
+ const prefix = `${fg("accent", "\u2316")} ${fg("dim", `#${num}`)} ${bar}`;
811
+ const suffix = ` ${fg("muted", `(${origin}${sessionIcon})`)}`;
812
+ const availTitle = Math.max(12, width - getDisplayWidth(prefix) - getDisplayWidth(nextStep) - getDisplayWidth(suffix) - 2);
813
+ const titleClipped = truncateToWidth(a.title, availTitle);
814
+ return [truncateToWidth(`${prefix}${titleClipped}${nextStep}${suffix}`, width)];
1288
815
  }
1289
- return lines;
816
+ const countTag = fg("accent", `\u2316 ${sorted.length} notes: `);
817
+ const segments = sorted.slice(0, 3).map((a) => {
818
+ const num = a.id.replace(/^anc-/, "");
819
+ const bar = a.progress ? `${formatProgressBar(a.progress)} ` : "";
820
+ const shortTitle = truncateToWidth(a.title, Math.max(14, Math.floor((width - 20) / Math.min(3, sorted.length)) - 12));
821
+ return `${fg("dim", `#${num}`)} ${bar}${shortTitle}`;
822
+ });
823
+ const more = sorted.length > 3 ? fg("dim", ` +${sorted.length - 3}`) : "";
824
+ return [truncateToWidth(`${countTag}${segments.join(fg("dim", " \xB7 "))}${more}`, width)];
1290
825
  },
1291
826
  invalidate: () => {
1292
827
  }
1293
828
  });
1294
829
  const content = ctx.ui?.theme ? factory : factory().render(80);
1295
- ctx.ui.setWidget("anchor-startup", content, { placement: "aboveEditor" });
830
+ ctx.ui.setWidget("anchor-startup", content, { placement: "belowEditor" });
1296
831
  } catch {
1297
832
  }
1298
833
  }
1299
834
  async function openAnchorDashboard(ctx, store) {
1300
835
  if (!hasActiveUI(ctx)) return;
1301
- const allUnfinished = store.list({ cwd: ctx.cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping");
1302
- if (allUnfinished.length === 0) {
1303
- ctx.ui.notify("\u2316 No active anchors. Use /pin <task> to record.", "info");
836
+ const allNotes = store.list({ cwd: ctx.cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping");
837
+ if (allNotes.length === 0) {
838
+ ctx.ui.notify("\u2316 No sticky notes. Type `pin: <note>` or `/pin <note>` to stick one.", "info");
1304
839
  return;
1305
840
  }
1306
- const sortedList = sortAnchorsForDisplay(allUnfinished, ctx.cwd);
841
+ const sorted = sortAnchorsForDisplay(allNotes, ctx.cwd);
1307
842
  const optionMap = /* @__PURE__ */ new Map();
1308
843
  const displayOptions = [];
1309
844
  const termCols = process.stdout?.columns || 96;
1310
- const isNarrow = termCols < 88;
1311
- const titleWidth = isNarrow ? Math.max(18, termCols - 36) : 36;
1312
- for (let i = 0; i < sortedList.length; i++) {
1313
- const a = sortedList[i];
845
+ const isNarrow = termCols < 84;
846
+ const titleWidth = isNarrow ? Math.max(20, termCols - 30) : 42;
847
+ for (const a of sorted) {
1314
848
  const num = a.id.replace(/^anc-/, "").padStart(2, "0");
1315
- const badge = a.status === "sleeping" ? padToWidth("[Quiet]", 10) : padToWidth(`[${a.priority.toUpperCase()}]`, 10);
1316
- const origin = formatOrigin(a);
1317
- const target = a.status === "sleeping" ? "Quiet (0 tok)" : formatTargetDate(a);
1318
- const created = formatCreationTime(a.createdAt);
1319
- const titleWithFiles = a.files && a.files.length > 0 ? `${a.title} [${a.files.slice(0, 1).join(", ")}]` : a.title;
849
+ const bar = a.progress ? `${formatProgressBar(a.progress)} ` : "";
850
+ const origin = formatOrigin(a) + (a.sessionFile ? " \u21AA" : "");
851
+ const age = a.status === "sleeping" ? `${formatRelativeAge(a.createdAt)} \xB7 quiet` : formatRelativeAge(a.createdAt);
1320
852
  const colNum = `${num} `;
1321
- const colTitle = padToWidth(titleWithFiles, titleWidth);
1322
- const colOrigin = padToWidth(origin, 12);
1323
- const colTarget = padToWidth(target, 14);
1324
- const colCreated = created;
1325
- const label = isNarrow ? `${colNum}${badge}${colTitle} ${colTarget}`.trimEnd() : `${colNum}${badge}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
853
+ const colTitle = padToWidth(`${bar}${a.title}`, titleWidth);
854
+ const colOrigin = padToWidth(origin, 22);
855
+ const label = isNarrow ? `${colNum}${colTitle} ${age}`.trimEnd() : `${colNum}${colTitle} ${colOrigin} ${age}`.trimEnd();
1326
856
  optionMap.set(label, a);
1327
857
  displayOptions.push(label);
1328
858
  }
1329
- const selected = await ctx.ui.select("\u2316 Anchors (select to settle, wake, or inspect):", displayOptions);
859
+ const selected = await ctx.ui.select("\u2316 Notes (select a note):", displayOptions);
1330
860
  if (!selected) return;
1331
861
  const anchor = optionMap.get(selected);
1332
862
  if (!anchor) return;
863
+ const currentSessionFile = ctx.sessionManager?.getSessionFile?.();
864
+ const canJumpSession = Boolean(
865
+ anchor.sessionFile && import_node_fs2.default.existsSync(anchor.sessionFile) && typeof ctx.switchSession === "function" && (!currentSessionFile || import_node_path2.default.resolve(anchor.sessionFile) !== import_node_path2.default.resolve(currentSessionFile))
866
+ );
867
+ 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)";
1333
868
  const actions = [
1334
- "\u2713 Settle (Mark completed and archive)",
1335
- ...anchor.status === "sleeping" ? ["\u26A1 Wake up (Activate for current session)"] : [],
1336
- "\u2139 View details & associated files",
1337
- "\u21A9 Cancel"
869
+ "\u2713 Done (tear off note)",
870
+ stepLabel,
871
+ ...canJumpSession ? ["\u21AA Jump to session (resume linked conversation)"] : [],
872
+ ...anchor.status === "sleeping" ? ["\u26A1 Wake up (mark active)"] : [],
873
+ "\u2139 Read full note",
874
+ "\u21A9 Back"
1338
875
  ];
1339
- const action = await ctx.ui.select(
1340
- `\u2316 Anchor #${anchor.id}: "${anchor.title}"`,
1341
- actions
1342
- );
876
+ const action = await ctx.ui.select(`\u2316 #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}"`, actions);
1343
877
  if (!action || action.startsWith("\u21A9")) return;
1344
878
  if (action.startsWith("\u2713")) {
1345
- store.settle(anchor.id, { settledBy: "manual-command" });
1346
- const successMsg = anchor.recurrence === "daily" ? `\u2316 Completed for today: "${anchor.title}" (resets tomorrow)` : `\u2316 Settled #${anchor.id}: "${anchor.title}" (/anchor undo to revert)`;
1347
- ctx.ui.notify(successMsg, "info");
879
+ store.settle(anchor.id, { settledBy: "manual" });
880
+ ctx.ui.notify(`\u2316 Torn off #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}" (/anchor undo to put back)`, "info");
881
+ updateAnchorStatusBar(ctx, store);
882
+ } else if (action.startsWith("\u25B6")) {
883
+ const { anchor: updated, completed } = store.step(anchor.id, 1);
1348
884
  updateAnchorStatusBar(ctx, store);
885
+ if (completed) {
886
+ ctx.ui.notify(
887
+ `\u2316 #${updated.id.replace(/^anc-/, "")} reached ${formatProgressBar(updated.progress)}! Run /done ${updated.id.replace(/^anc-/, "")} to tear off.`,
888
+ "info"
889
+ );
890
+ } else {
891
+ ctx.ui.notify(`\u2316 Advanced #${updated.id.replace(/^anc-/, "")} to ${formatProgressBar(updated.progress)}`, "info");
892
+ }
893
+ } else if (action.startsWith("\u21AA") && anchor.sessionFile) {
894
+ try {
895
+ await ctx.switchSession(anchor.sessionFile);
896
+ } catch (err) {
897
+ ctx.ui.notify(`Could not switch session: ${err.message}`, "error");
898
+ }
1349
899
  } else if (action.startsWith("\u26A1")) {
1350
900
  store.touch(anchor.id);
1351
- ctx.ui.notify(`\u2316 Woke up #${anchor.id}: "${anchor.title}"`, "info");
901
+ ctx.ui.notify(`\u2316 Woke up #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}"`, "info");
1352
902
  updateAnchorStatusBar(ctx, store);
1353
903
  } else if (action.startsWith("\u2139")) {
1354
904
  const details = [
1355
- `#${anchor.id} [${anchor.priority.toUpperCase()} \xB7 ${anchor.status.toUpperCase()}] ${anchor.title}`,
1356
- anchor.description ? `Description: ${anchor.description}` : null,
1357
- anchor.targetDate ? `Target Date: ${anchor.targetDate} (${formatTargetDate(anchor)})` : null,
1358
- anchor.files && anchor.files.length > 0 ? `Files: ${anchor.files.join(", ")}` : null,
1359
- anchor.verifyCommand ? `Verify Command: ${anchor.verifyCommand}` : null,
1360
- `Created: ${formatCreationTime(anchor.createdAt)} (in ${anchor.project || "global"})`
905
+ `\u2316 Note #${anchor.id.replace(/^anc-/, "")}: ${anchor.title}`,
906
+ anchor.progress ? `Progress: ${formatProgressBar(anchor.progress)}${anchor.progress.steps ? ` (${anchor.progress.steps.join(" \u2192 ")})` : ""}` : null,
907
+ anchor.description ? `Details: ${anchor.description}` : null,
908
+ anchor.files.length > 0 ? `Files: ${anchor.files.join(", ")}` : null,
909
+ anchor.sessionFile ? `Session: ${import_node_path2.default.basename(anchor.sessionFile)}` : null,
910
+ `Workspace: ${anchor.project || "global"} \xB7 Pinned ${formatRelativeAge(anchor.createdAt)}`
1361
911
  ].filter(Boolean).join("\n");
1362
912
  ctx.ui.notify(details, "info");
1363
913
  }
1364
914
  }
1365
915
 
1366
- // src/observer.ts
1367
- var MUTATION_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "patch", "apply_diff", "create_file", "modify"]);
1368
- var SessionTouchObserver = class {
1369
- touchedFiles = /* @__PURE__ */ new Set();
1370
- modifiedFiles = /* @__PURE__ */ new Set();
1371
- inspectedFiles = /* @__PURE__ */ new Set();
1372
- committed = false;
1373
- commitMessages = [];
1374
- clear() {
1375
- this.touchedFiles.clear();
1376
- this.modifiedFiles.clear();
1377
- this.inspectedFiles.clear();
1378
- this.committed = false;
1379
- this.commitMessages = [];
1380
- }
1381
- /**
1382
- * Observe and record a tool call invocation, separating inspection from mutation
1383
- */
1384
- recordToolCall(toolName, input) {
1385
- if (!input || typeof input !== "object") return;
1386
- const lowerTool = (toolName || "").toLowerCase();
1387
- const isMutation = MUTATION_TOOLS.has(lowerTool);
1388
- const paths = [];
1389
- const pathField = input.path || input.filePath || input.file;
1390
- if (typeof pathField === "string") {
1391
- paths.push(normalizePath(pathField));
1392
- }
1393
- if (Array.isArray(input.paths)) {
1394
- for (const p of input.paths) {
1395
- if (typeof p === "string") {
1396
- paths.push(normalizePath(p));
1397
- }
1398
- }
1399
- }
1400
- for (const p of paths) {
1401
- this.touchedFiles.add(p);
1402
- if (isMutation) {
1403
- this.modifiedFiles.add(p);
1404
- } else {
1405
- this.inspectedFiles.add(p);
1406
- }
1407
- }
1408
- if (lowerTool === "bash" || lowerTool === "powershell") {
1409
- const cmd = String(input.command || "");
1410
- if (cmd.includes("git commit")) {
1411
- this.committed = true;
1412
- const msgMatch = cmd.match(/-m\s+["']([^"']+)["']/);
1413
- if (msgMatch && msgMatch[1]) {
1414
- this.commitMessages.push(msgMatch[1]);
1415
- }
1416
- }
1417
- }
1418
- }
1419
- /**
1420
- * Add a file path manually (e.g. from git status diff)
1421
- */
1422
- addTouchedFile(filePath, isModified = true) {
1423
- if (filePath) {
1424
- const norm = normalizePath(filePath);
1425
- this.touchedFiles.add(norm);
1426
- if (isModified) {
1427
- this.modifiedFiles.add(norm);
1428
- } else {
1429
- this.inspectedFiles.add(norm);
1430
- }
1431
- }
1432
- }
1433
- getModifiedFiles() {
1434
- return Array.from(this.modifiedFiles);
1435
- }
1436
- getInspectedFiles() {
1437
- return Array.from(this.inspectedFiles);
1438
- }
1439
- getTouchedFiles() {
1440
- return Array.from(this.touchedFiles);
1441
- }
1442
- hasCommitted() {
1443
- return this.committed;
1444
- }
1445
- getCommitMessages() {
1446
- return [...this.commitMessages];
916
+ // src/extension.ts
917
+ var import_typebox = require("@sinclair/typebox");
918
+ var import_node_fs3 = __toESM(require("fs"), 1);
919
+ function parseAnchorCommandInput(raw) {
920
+ const trimmed = (raw || "").trim();
921
+ if (!trimmed || trimmed === "list" || trimmed === "ls") {
922
+ return { mode: "dashboard" };
923
+ }
924
+ if (trimmed === "undo") {
925
+ return { mode: "undo" };
926
+ }
927
+ const settleMatch = trimmed.match(/^(?:done|rm|close|settle)\s+(.+)$/i);
928
+ if (settleMatch) {
929
+ return {
930
+ mode: "settle",
931
+ targetId: settleMatch[1].trim().replace(/^#/, "")
932
+ };
1447
933
  }
1448
- /**
1449
- * Check if any commit message explicitly references or resolves an anchor
1450
- */
1451
- matchesCommit(anchor) {
1452
- const idLower = anchor.id.toLowerCase();
1453
- const title = anchor.title.toLowerCase();
1454
- let titleTokens = [];
1455
- if (typeof Intl !== "undefined" && Intl.Segmenter) {
1456
- try {
1457
- const segmenter = new Intl.Segmenter("und", { granularity: "word" });
1458
- titleTokens = Array.from(segmenter.segment(title)).filter((s) => s.isWordLike).map((s) => s.segment.trim().toLowerCase()).filter((t) => t.length >= 2);
1459
- } catch {
1460
- titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
1461
- }
1462
- } else {
1463
- titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
1464
- }
1465
- for (const msg of this.commitMessages) {
1466
- const msgLower = msg.toLowerCase();
1467
- if (msgLower.includes(idLower)) {
1468
- return { matched: true, message: msg };
1469
- }
1470
- if (titleTokens.length > 0) {
1471
- const matchingCount = titleTokens.filter((tok) => msgLower.includes(tok)).length;
1472
- if (matchingCount >= Math.min(2, titleTokens.length)) {
1473
- return { matched: true, message: msg };
1474
- }
1475
- }
1476
- }
1477
- return { matched: false };
934
+ const stepMatch = trimmed.match(/^(?:step|\+)\s*(\S+)(?:\s+([+-]?\d+))?$/i);
935
+ if (stepMatch) {
936
+ return {
937
+ mode: "step",
938
+ targetId: stepMatch[1].trim().replace(/^#/, ""),
939
+ delta: stepMatch[2] ? parseInt(stepMatch[2], 10) : 1
940
+ };
1478
941
  }
1479
- };
1480
-
1481
- // src/context_injector.ts
1482
- var import_node_path4 = __toESM(require("path"), 1);
1483
- var COMMENT_FORMATS = {
1484
- // Double slash //
1485
- ".ts": (m) => `// ${m}`,
1486
- ".tsx": (m) => `// ${m}`,
1487
- ".js": (m) => `// ${m}`,
1488
- ".jsx": (m) => `// ${m}`,
1489
- ".mjs": (m) => `// ${m}`,
1490
- ".cjs": (m) => `// ${m}`,
1491
- ".java": (m) => `// ${m}`,
1492
- ".kt": (m) => `// ${m}`,
1493
- ".swift": (m) => `// ${m}`,
1494
- ".go": (m) => `// ${m}`,
1495
- ".rs": (m) => `// ${m}`,
1496
- ".c": (m) => `// ${m}`,
1497
- ".cpp": (m) => `// ${m}`,
1498
- ".h": (m) => `// ${m}`,
1499
- ".hpp": (m) => `// ${m}`,
1500
- ".cs": (m) => `// ${m}`,
1501
- ".dart": (m) => `// ${m}`,
1502
- ".scala": (m) => `// ${m}`,
1503
- ".zig": (m) => `// ${m}`,
1504
- ".proto": (m) => `// ${m}`,
1505
- // Hash #
1506
- ".py": (m) => `# ${m}`,
1507
- ".rb": (m) => `# ${m}`,
1508
- ".sh": (m) => `# ${m}`,
1509
- ".bash": (m) => `# ${m}`,
1510
- ".zsh": (m) => `# ${m}`,
1511
- ".yaml": (m) => `# ${m}`,
1512
- ".yml": (m) => `# ${m}`,
1513
- ".toml": (m) => `# ${m}`,
1514
- ".conf": (m) => `# ${m}`,
1515
- ".dockerfile": (m) => `# ${m}`,
1516
- ".ps1": (m) => `# ${m}`,
1517
- ".r": (m) => `# ${m}`,
1518
- // HTML / XML <!-- -->
1519
- ".html": (m) => `<!-- ${m} -->`,
1520
- ".xml": (m) => `<!-- ${m} -->`,
1521
- ".svg": (m) => `<!-- ${m} -->`,
1522
- ".vue": (m) => `<!-- ${m} -->`,
1523
- ".svelte": (m) => `<!-- ${m} -->`,
1524
- // Block comment /* */
1525
- ".css": (m) => `/* ${m} */`,
1526
- ".scss": (m) => `/* ${m} */`,
1527
- ".less": (m) => `/* ${m} */`,
1528
- // SQL / Lua / Haskell --
1529
- ".sql": (m) => `-- ${m}`,
1530
- ".lua": (m) => `-- ${m}`,
1531
- ".hs": (m) => `-- ${m}`,
1532
- // Batch REM
1533
- ".bat": (m) => `REM ${m}`,
1534
- ".cmd": (m) => `REM ${m}`
1535
- };
1536
- function makeSafeTaskAnnotation(filePath, anchor) {
1537
- const ext = import_node_path4.default.extname(filePath).toLowerCase();
1538
- const formatter = COMMENT_FORMATS[ext];
1539
- if (!formatter) {
1540
- return null;
942
+ const jumpMatch = trimmed.match(/^(?:jump|resume|session)\s+(.+)$/i);
943
+ if (jumpMatch) {
944
+ return {
945
+ mode: "jump",
946
+ targetId: jumpMatch[1].trim().replace(/^#/, "")
947
+ };
1541
948
  }
1542
- return "\n\n" + formatter(`\u2316 anchor context: #${anchor.id} ${anchor.title} (${anchor.priority.toUpperCase()})`);
949
+ return {
950
+ mode: "pin",
951
+ title: trimmed
952
+ };
1543
953
  }
1544
- function renderColdStartAnchorsContext(store, cwdOrNow, nowArg, preflightFailures) {
1545
- let cwd;
1546
- let now = Date.now();
1547
- if (typeof cwdOrNow === "number") {
1548
- now = cwdOrNow;
1549
- } else if (typeof cwdOrNow === "string") {
1550
- cwd = cwdOrNow;
1551
- }
1552
- if (typeof nowArg === "number") {
1553
- now = nowArg;
1554
- }
1555
- sweepStore(store, now);
1556
- const activeAnchors = store.list({ status: "active", cwd });
1557
- if (activeAnchors.length === 0) {
1558
- return "";
1559
- }
1560
- const lines = activeAnchors.map((a) => {
1561
- const fileHint = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(", ")})` : "";
1562
- const tagHint = a.tags.length > 0 ? ` [${a.tags.join(", ")}]` : "";
1563
- const projHint = a.cwd ? `[${a.project}]` : "[global]";
1564
- let targetHint = "";
1565
- if (a.recurrence === "daily") {
1566
- targetHint = " [\u4ECA\u65E5\u5FAA\u73AF\xB7Daily]";
1567
- } else if (a.targetDate) {
1568
- const todayStr = getTodayDateString(now);
1569
- if (a.targetDate === todayStr) {
1570
- targetHint = " [\u4ECA\u65E5\u805A\u7126\xB7Due Today]";
1571
- } else if (a.targetDate < todayStr) {
1572
- targetHint = " [\u5DF2\u903E\u671F\xB7Overdue]";
1573
- } else {
1574
- targetHint = ` [\u6392\u671F:${a.targetDate}]`;
1575
- }
1576
- } else {
1577
- targetHint = " [\u957F\u671F\u613F\u666F\xB7Someday]";
1578
- }
1579
- const ageDays = Math.floor((now - a.createdAt) / (24 * 60 * 60 * 1e3));
1580
- const ageHint = ageDays > 0 ? ` (${ageDays}d old)` : "";
1581
- const failureSnippet = preflightFailures?.get(a.id);
1582
- const verifyHint = failureSnippet ? ` [\u26A1 \u7269\u7406\u9A8C\u8BC1\u5931\u8D25: ${failureSnippet.replace(/\s+/g, " ").slice(-100)}]` : a.verifyCommand ? ` [verify: ${a.verifyCommand}]` : "";
1583
- return `\u2022 #${a.id} ${projHint}${targetHint} [${a.priority.toUpperCase()}] ${a.title}${verifyHint}${ageHint}${tagHint}${fileHint}`;
1584
- });
1585
- return [
1586
- `<active-anchors count="${activeAnchors.length}">`,
1587
- `Unfinished cross-session commitments from previous sessions:`,
1588
- lines.join("\n"),
1589
- ``,
1590
- `CONVERSATIONAL PROTOCOL (HUMAN PARTNER TONE):`,
1591
- `- If the user opens the session with a casual greeting or continuation ("hi", "\u5728\u5417", "\u7EE7\u7EED", "\u65E9\u4E0A\u597D"), naturally prioritize items tagged [\u4ECA\u65E5\u805A\u7126\xB7Due Today] in a friendly assistant tone (e.g. "\u55E8\uFF01\u4ECA\u5929\u89C4\u5212\u7684\u3010...\u3011\u73B0\u5728\u5904\u7406\u8FD8\u662F\u770B\u522B\u7684\uFF1F").`,
1592
- `- If an anchor has [\u26A1 \u7269\u7406\u9A8C\u8BC1\u5931\u8D25], its verifyCommand failed on session start. Target the failing assertion directly.`,
1593
- `- If the user issues a direct, concrete new instruction (e.g. "\u5E2E\u6211\u67E5\u4E0B\u8FD9\u4E2A\u62A5\u9519"), DO NOT interrupt their flow. Focus directly on their instruction.`,
1594
- `- When a commitment is satisfied during work, automatically call anchor(action="settle", id=...) to clear it.`,
1595
- `</active-anchors>`
1596
- ].join("\n");
954
+ function parseQuickNoteInput(text) {
955
+ const m = (text || "").trim().match(/^(?:pin|note|便签|备忘)\s*[::]\s*(.+)$/i);
956
+ return m && m[1].trim() ? m[1].trim() : null;
1597
957
  }
1598
- var renderActiveAnchorsContext = renderColdStartAnchorsContext;
1599
-
1600
- // src/protocol.ts
1601
- var AnchorProtocol = class {
1602
- store;
1603
- observer;
1604
- annotatedThisSession = /* @__PURE__ */ new Set();
1605
- constructor(store) {
1606
- this.store = store;
1607
- this.observer = new SessionTouchObserver();
1608
- }
1609
- /**
1610
- * Run session initialization lifecycle: sweeps stale anchors and resets session state
1611
- */
1612
- handleSessionStart(now = Date.now()) {
1613
- this.observer.clear();
1614
- this.annotatedThisSession.clear();
1615
- const sweep = sweepStore(this.store, now);
1616
- return { sleepingCount: sweep.transitionedToSleeping.length };
1617
- }
1618
- /**
1619
- * Handle turn lifecycle: injects cold-start prompt on turn 1, returns null (0 tokens) on turn 2+
1620
- */
1621
- handleBeforeTurn(turnCount, cwd, now = Date.now()) {
1622
- if (turnCount <= 1) {
1623
- return renderColdStartAnchorsContext(this.store, cwd, now) || null;
958
+ function resolveAnchorTarget(store, idOrIndex, cwd) {
959
+ const clean = idOrIndex.trim().replace(/^#/, "");
960
+ if (/^\d+$/.test(clean)) {
961
+ const seqIndex = parseInt(clean, 10);
962
+ const visible = store.list({ cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping");
963
+ const sorted = sortAnchorsForDisplay(visible, cwd);
964
+ if (seqIndex >= 1 && seqIndex <= sorted.length) {
965
+ return sorted[seqIndex - 1].id;
1624
966
  }
1625
- return null;
1626
967
  }
1627
- /**
1628
- * Handle tool execution result: detects touched paths, refreshes mutation touch timers,
1629
- * and returns safe language-specific JIT annotation (or null if syntax incompatible/already annotated).
1630
- */
1631
- handleToolResult(input) {
1632
- if (!input.isError) {
1633
- this.observer.recordToolCall(input.toolName, input.filePath ? { path: input.filePath } : {});
1634
- }
1635
- if (!input.filePath) {
1636
- return { annotation: null, matchedAnchors: [] };
1637
- }
1638
- const touchedPath = normalizePath(input.filePath);
1639
- const anchors = this.store.list({ cwd: input.cwd }).filter((a2) => a2.status === "active" || a2.status === "sleeping");
1640
- const matches = findMatchedAnchors(anchors, [touchedPath]);
1641
- if (matches.length === 0) {
1642
- return { annotation: null, matchedAnchors: [] };
1643
- }
1644
- if (input.isMutation && !input.isError) {
1645
- for (const m of matches) {
1646
- this.store.touch(m.anchor.id);
1647
- }
1648
- }
1649
- const a = matches[0].anchor;
1650
- if (!this.annotatedThisSession.has(a.id)) {
1651
- this.annotatedThisSession.add(a.id);
1652
- const annotation = makeSafeTaskAnnotation(touchedPath, a);
1653
- return { annotation, matchedAnchors: matches.map((m) => m.anchor) };
1654
- }
1655
- return { annotation: null, matchedAnchors: matches.map((m) => m.anchor) };
1656
- }
1657
- };
968
+ return store.get(clean)?.id;
969
+ }
1658
970
  // Annotate the CommonJS export names for ESM import in node:
1659
971
  0 && (module.exports = {
1660
- AnchorProtocol,
1661
972
  AnchorStore,
1662
973
  COMMENT_FORMATS,
1663
- DEFAULT_DECAY_POLICY,
1664
- DURABLE_DECAY_POLICY,
1665
- EPHEMERAL_DECAY_POLICY,
1666
- SessionTouchObserver,
974
+ MS_PER_DAY,
975
+ QUIET_AFTER_DAYS,
1667
976
  acquireSyncLock,
1668
977
  atomicRenameWithRetry,
1669
- classifyAnchor,
1670
- detectDurability,
1671
- detectRecurrence,
1672
- detectTargetDate,
1673
- evaluateAnchorDecay,
1674
978
  extractPathsFromText,
979
+ extractProgressFromText,
1675
980
  findMatchedAnchors,
1676
- formatCreationTime,
1677
981
  formatOrigin,
1678
- formatRelativeTime,
1679
- formatRemainingTtl,
1680
- formatSettlementCard,
1681
- formatTargetDate,
1682
- generateSettlementProposals,
982
+ formatProgressBar,
983
+ formatRelativeAge,
1683
984
  getDefaultStorageDir,
1684
985
  getDisplayWidth,
1685
- getEphemeralDecayPolicy,
1686
- getTodayDateString,
1687
986
  globToRegExp,
1688
- groupAnchorsByQuadrant,
1689
987
  hasActiveUI,
1690
988
  isAnchorTargetingWorkspace,
1691
989
  isSameOrSubPath,
1692
990
  makeSafeTaskAnnotation,
1693
991
  matchAnchorAgainstTouchedFiles,
992
+ matchesCommitMessage,
1694
993
  normalizePath,
1695
994
  openAnchorDashboard,
1696
995
  padToWidth,
996
+ parseAnchorCommandInput,
997
+ parseQuickNoteInput,
1697
998
  renderActiveAnchorsContext,
1698
999
  renderColdStartAnchorsContext,
1699
- runPhysicalVerification,
1000
+ resolveAnchorTarget,
1700
1001
  sortAnchorsForDisplay,
1701
1002
  stripAnsi,
1702
- sweepStore,
1003
+ sweepQuietNotes,
1703
1004
  truncateToWidth,
1704
- tryRealtimeAutoSettle,
1705
1005
  updateAnchorStatusBar,
1706
1006
  updateStartupBanner
1707
1007
  });