pi-anchor 0.3.0 → 0.4.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.
@@ -39,32 +39,14 @@ var import_typebox = require("@sinclair/typebox");
39
39
 
40
40
  // src/store.ts
41
41
  var import_node_fs = __toESM(require("fs"), 1);
42
- var import_node_path2 = __toESM(require("path"), 1);
42
+ var import_node_path = __toESM(require("path"), 1);
43
43
  var import_node_os = __toESM(require("os"), 1);
44
44
 
45
45
  // src/types.ts
46
- var EPHEMERAL_DECAY_POLICY = {
47
- activeDays: 7,
48
- // 7 days active before quiet sleep
49
- sleepDays: 30,
50
- graveyardDays: 9999
51
- // Never silently auto-delete unfinished tasks
52
- };
53
- var DURABLE_DECAY_POLICY = {
54
- activeDays: 7,
55
- // 7 days active before quiet sleep
56
- sleepDays: 30,
57
- graveyardDays: 9999
58
- // Never permanently auto-dropped; preserved indefinitely
59
- };
46
+ var QUIET_AFTER_DAYS = 7;
47
+ var MS_PER_DAY = 864e5;
60
48
 
61
- // src/matcher.ts
62
- var import_node_path = __toESM(require("path"), 1);
63
- function globToRegExp(pattern) {
64
- const collapsed = pattern.slice(0, 512).replace(/\/+(\*\*\/+)+/g, "/**/").replace(/\*{3,}/g, "**");
65
- 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, ".*");
66
- return new RegExp(`^${escaped}$`);
67
- }
49
+ // src/store.ts
68
50
  function normalizePath(filePath, baseCwd) {
69
51
  if (typeof filePath !== "string") return "";
70
52
  let norm = filePath.trim().replace(/\\/g, "/").replace(/^\.\//, "");
@@ -80,14 +62,19 @@ function normalizePath(filePath, baseCwd) {
80
62
  }
81
63
  return norm;
82
64
  }
83
- function isSameOrSubPath(anchorCwd, targetCwd) {
84
- if (!anchorCwd || !targetCwd) return false;
85
- const normA = anchorCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
86
- const normT = targetCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
87
- const isWinStyle = process.platform === "win32" || /^[a-zA-Z]:\//.test(normA) || /^[a-zA-Z]:\//.test(normT);
88
- const cmpA = isWinStyle ? normA.toLowerCase() : normA;
89
- const cmpT = isWinStyle ? normT.toLowerCase() : normT;
90
- return cmpA === cmpT || cmpT.startsWith(cmpA + "/") || cmpA.startsWith(cmpT + "/");
65
+ function isSameOrSubPath(dirA, dirB) {
66
+ if (!dirA || !dirB) return false;
67
+ const normA = dirA.trim().replace(/\\/g, "/").replace(/\/+$/, "");
68
+ const normB = dirB.trim().replace(/\\/g, "/").replace(/\/+$/, "");
69
+ const isWin = process.platform === "win32" || /^[a-zA-Z]:\//.test(normA) || /^[a-zA-Z]:\//.test(normB);
70
+ const a = isWin ? normA.toLowerCase() : normA;
71
+ const b = isWin ? normB.toLowerCase() : normB;
72
+ return a === b || b.startsWith(a + "/") || a.startsWith(b + "/");
73
+ }
74
+ function globToRegExp(pattern) {
75
+ const collapsed = pattern.slice(0, 512).replace(/\/+(\*\*\/+)+/g, "/**/").replace(/\*{3,}/g, "**");
76
+ 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, ".*");
77
+ return new RegExp(`^${escaped}$`);
91
78
  }
92
79
  function extractPathsFromText(text) {
93
80
  if (!text) return [];
@@ -109,8 +96,7 @@ function isAnchorTargetingWorkspace(anchor, targetCwd) {
109
96
  if (anchor.project && anchor.project !== "global") {
110
97
  const normProj = anchor.project.trim().replace(/\\/g, "/").replace(/\/+$/, "");
111
98
  if (isSameOrSubPath(normProj, normTarget)) return true;
112
- const projBase = import_node_path.default.basename(normProj).toLowerCase();
113
- if (projBase && projBase === targetBase) return true;
99
+ if (import_node_path.default.basename(normProj).toLowerCase() === targetBase) return true;
114
100
  }
115
101
  if (anchor.files && anchor.files.length > 0) {
116
102
  for (const f of anchor.files) {
@@ -135,12 +121,9 @@ function matchAnchorAgainstTouchedFiles(anchor, touchedFiles) {
135
121
  matchedExact.push(touched);
136
122
  break;
137
123
  }
138
- if (normPattern.includes("*")) {
139
- const regex = globToRegExp(normPattern);
140
- if (regex.test(touched)) {
141
- matchedExact.push(touched);
142
- break;
143
- }
124
+ if (normPattern.includes("*") && globToRegExp(normPattern).test(touched)) {
125
+ matchedExact.push(touched);
126
+ break;
144
127
  }
145
128
  const dirPrefix = normPattern.endsWith("/") ? normPattern : normPattern + "/";
146
129
  if (touched.startsWith(dirPrefix)) {
@@ -150,193 +133,136 @@ function matchAnchorAgainstTouchedFiles(anchor, touchedFiles) {
150
133
  }
151
134
  }
152
135
  if (matchedExact.length > 0) {
153
- return {
154
- anchor,
155
- score: 1,
156
- matchedFiles: Array.from(new Set(matchedExact)),
157
- reason: "exact-file"
158
- };
136
+ return { anchor, score: 1, matchedFiles: Array.from(new Set(matchedExact)), reason: "exact-file" };
159
137
  }
160
138
  if (matchedPrefix.length > 0) {
161
- return {
162
- anchor,
163
- score: 0.85,
164
- matchedFiles: Array.from(new Set(matchedPrefix)),
165
- reason: "dir-prefix"
166
- };
139
+ return { anchor, score: 0.85, matchedFiles: Array.from(new Set(matchedPrefix)), reason: "dir-prefix" };
167
140
  }
168
141
  return null;
169
142
  }
170
143
  function findMatchedAnchors(anchors, touchedFiles) {
171
- const priorityWeight = { p0: 3, p1: 2, p2: 1 };
172
144
  const results = [];
173
145
  for (const anchor of anchors) {
174
- const match = matchAnchorAgainstTouchedFiles(anchor, touchedFiles);
175
- if (match) {
176
- results.push(match);
177
- }
178
- }
179
- return results.sort((a, b) => {
180
- if (b.score !== a.score) {
181
- return b.score - a.score;
182
- }
183
- const weightA = priorityWeight[a.anchor.priority] || 0;
184
- const weightB = priorityWeight[b.anchor.priority] || 0;
185
- if (weightB !== weightA) {
186
- return weightB - weightA;
187
- }
188
- return b.anchor.lastTouchedAt - a.anchor.lastTouchedAt;
189
- });
190
- }
191
-
192
- // src/store.ts
193
- function detectDurability(title) {
194
- const temporalKeywords = [
195
- "\u4ECA\u5929",
196
- "\u4ECA\u65E5",
197
- "\u665A\u4E0A",
198
- "\u4ECA\u665A",
199
- "\u660E\u5929",
200
- "\u540E\u5929",
201
- "\u5927\u540E\u5929",
202
- "\u7A0D\u540E",
203
- "\u5F85\u4F1A",
204
- "\u4E0B\u5348",
205
- "\u4E0A\u5348",
206
- "\u4E34\u65F6",
207
- "\u4E00\u4F1A\u513F",
208
- "\u7B49\u7B49",
209
- "today",
210
- "tonight",
211
- "tomorrow",
212
- "later",
213
- "temp",
214
- "soon"
215
- ];
216
- const lower = title.toLowerCase();
217
- for (const kw of temporalKeywords) {
218
- if (lower.includes(kw)) {
219
- return "ephemeral";
220
- }
146
+ const m = matchAnchorAgainstTouchedFiles(anchor, touchedFiles);
147
+ if (m) results.push(m);
221
148
  }
222
- return "durable";
149
+ return results.sort((a, b) => b.score - a.score);
223
150
  }
224
- function getEphemeralDecayPolicy(title) {
225
- const lower = title.toLowerCase();
226
- if (lower.includes("\u5927\u540E\u5929")) {
227
- return { activeDays: 7, sleepDays: 30, graveyardDays: 9999 };
228
- }
229
- if (lower.includes("\u540E\u5929")) {
230
- return { activeDays: 7, sleepDays: 30, graveyardDays: 9999 };
231
- }
232
- return { ...EPHEMERAL_DECAY_POLICY };
151
+ var COMMENT_FORMATS = {
152
+ ".ts": (m) => `// ${m}`,
153
+ ".tsx": (m) => `// ${m}`,
154
+ ".js": (m) => `// ${m}`,
155
+ ".jsx": (m) => `// ${m}`,
156
+ ".mjs": (m) => `// ${m}`,
157
+ ".cjs": (m) => `// ${m}`,
158
+ ".go": (m) => `// ${m}`,
159
+ ".rs": (m) => `// ${m}`,
160
+ ".c": (m) => `// ${m}`,
161
+ ".cpp": (m) => `// ${m}`,
162
+ ".h": (m) => `// ${m}`,
163
+ ".cs": (m) => `// ${m}`,
164
+ ".java": (m) => `// ${m}`,
165
+ ".kt": (m) => `// ${m}`,
166
+ ".swift": (m) => `// ${m}`,
167
+ ".dart": (m) => `// ${m}`,
168
+ ".py": (m) => `# ${m}`,
169
+ ".rb": (m) => `# ${m}`,
170
+ ".sh": (m) => `# ${m}`,
171
+ ".bash": (m) => `# ${m}`,
172
+ ".yaml": (m) => `# ${m}`,
173
+ ".yml": (m) => `# ${m}`,
174
+ ".toml": (m) => `# ${m}`,
175
+ ".ps1": (m) => `# ${m}`,
176
+ ".html": (m) => `<!-- ${m} -->`,
177
+ ".xml": (m) => `<!-- ${m} -->`,
178
+ ".svg": (m) => `<!-- ${m} -->`,
179
+ ".vue": (m) => `<!-- ${m} -->`,
180
+ ".svelte": (m) => `<!-- ${m} -->`,
181
+ ".css": (m) => `/* ${m} */`,
182
+ ".scss": (m) => `/* ${m} */`,
183
+ ".sql": (m) => `-- ${m}`,
184
+ ".lua": (m) => `-- ${m}`
185
+ };
186
+ function makeSafeTaskAnnotation(filePath, anchor) {
187
+ const ext = import_node_path.default.extname(filePath).toLowerCase();
188
+ const fmt = COMMENT_FORMATS[ext];
189
+ if (!fmt) return null;
190
+ const num = anchor.id.replace(/^anc-/, "");
191
+ return "\n\n" + fmt(`\u2316 Note #${num}: ${anchor.title}`);
233
192
  }
234
- function detectTargetDate(titleOrDate, now = Date.now()) {
235
- const lower = titleOrDate.toLowerCase().trim();
236
- const MS_DAY = 24 * 60 * 60 * 1e3;
237
- if (lower.includes("\u4ECA\u5929") || lower.includes("\u4ECA\u65E5") || lower.includes("\u4ECA\u665A") || lower === "today" || lower === "tonight") {
238
- return getTodayDateString(now);
239
- }
240
- if (lower.includes("\u660E\u5929") || lower === "tomorrow") {
241
- return getTodayDateString(now + MS_DAY);
242
- }
243
- if (lower.includes("\u540E\u5929")) {
244
- return getTodayDateString(now + 2 * MS_DAY);
245
- }
246
- if (lower.includes("\u5927\u540E\u5929")) {
247
- return getTodayDateString(now + 3 * MS_DAY);
193
+ function matchesCommitMessage(anchor, commitMessages) {
194
+ const idLower = anchor.id.toLowerCase();
195
+ const numId = `#${anchor.id.replace(/^anc-/, "")}`;
196
+ const title = anchor.title.toLowerCase();
197
+ let titleTokens = [];
198
+ if (typeof Intl !== "undefined" && Intl.Segmenter) {
199
+ try {
200
+ const seg = new Intl.Segmenter("und", { granularity: "word" });
201
+ titleTokens = Array.from(seg.segment(title)).filter((s) => s.isWordLike).map((s) => s.segment.trim().toLowerCase()).filter((t) => t.length >= 2);
202
+ } catch {
203
+ titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
204
+ }
205
+ } else {
206
+ titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
248
207
  }
249
- const weekdayMap = {
250
- "\u5468\u65E5": 0,
251
- "\u661F\u671F\u65E5": 0,
252
- "sunday": 0,
253
- "sun": 0,
254
- "\u5468\u4E00": 1,
255
- "\u661F\u671F\u4E00": 1,
256
- "monday": 1,
257
- "mon": 1,
258
- "\u5468\u4E8C": 2,
259
- "\u661F\u671F\u4E8C": 2,
260
- "tuesday": 2,
261
- "tue": 2,
262
- "\u5468\u4E09": 3,
263
- "\u661F\u671F\u4E09": 3,
264
- "wednesday": 3,
265
- "wed": 3,
266
- "\u5468\u56DB": 4,
267
- "\u661F\u671F\u56DB": 4,
268
- "thursday": 4,
269
- "thu": 4,
270
- "\u5468\u4E94": 5,
271
- "\u661F\u671F\u4E94": 5,
272
- "friday": 5,
273
- "fri": 5,
274
- "\u5468\u516D": 6,
275
- "\u661F\u671F\u516D": 6,
276
- "saturday": 6,
277
- "sat": 6
278
- };
279
- const isNextWeek = lower.includes("\u4E0B\u5468") || lower.includes("next");
280
- for (const [name, targetDay] of Object.entries(weekdayMap)) {
281
- if (lower.includes(name)) {
282
- const curDay = new Date(now).getDay();
283
- let diff = targetDay - curDay;
284
- if (diff <= 0 || isNextWeek) {
285
- diff += 7;
208
+ for (const msg of commitMessages) {
209
+ const msgLower = msg.toLowerCase();
210
+ if (msgLower.includes(idLower) || msgLower.includes(numId)) {
211
+ return { matched: true, message: msg };
212
+ }
213
+ if (titleTokens.length > 0) {
214
+ const hits = titleTokens.filter((tok) => msgLower.includes(tok)).length;
215
+ if (hits >= Math.min(2, titleTokens.length)) {
216
+ return { matched: true, message: msg };
286
217
  }
287
- return getTodayDateString(now + diff * MS_DAY);
288
218
  }
289
219
  }
290
- const fullDateMatch = titleOrDate.match(/\b(20\d\d)-(\d{1,2})-(\d{1,2})\b/);
291
- if (fullDateMatch) {
292
- const y = fullDateMatch[1];
293
- const m = fullDateMatch[2].padStart(2, "0");
294
- const d = fullDateMatch[3].padStart(2, "0");
295
- return `${y}-${m}-${d}`;
296
- }
297
- const mmddMatch = titleOrDate.match(/\b(\d{1,2})-(\d{1,2})\b/);
298
- if (mmddMatch) {
299
- const curYear = new Date(now).getFullYear();
300
- const m = mmddMatch[1].padStart(2, "0");
301
- const d = mmddMatch[2].padStart(2, "0");
302
- return `${curYear}-${m}-${d}`;
303
- }
304
- return void 0;
220
+ return { matched: false };
305
221
  }
306
- function detectRecurrence(title) {
307
- const lower = title.toLowerCase();
308
- if (lower.includes("\u6BCF\u5929") || lower.includes("\u6BCF\u65E5") || lower.includes("daily") || lower.includes("every day")) {
309
- return "daily";
310
- }
311
- return void 0;
312
- }
313
- function getTodayDateString(timestamp = Date.now()) {
314
- const d = new Date(timestamp);
315
- const year = d.getFullYear();
316
- const month = String(d.getMonth() + 1).padStart(2, "0");
317
- const day = String(d.getDate()).padStart(2, "0");
318
- return `${year}-${month}-${day}`;
222
+ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
223
+ let cwd;
224
+ let now = Date.now();
225
+ if (typeof cwdOrNow === "number") now = cwdOrNow;
226
+ else if (typeof cwdOrNow === "string") cwd = cwdOrNow;
227
+ if (typeof nowArg === "number") now = nowArg;
228
+ sweepQuietNotes(store, now);
229
+ const activeNotes = store.list({ status: "active", cwd });
230
+ if (activeNotes.length === 0) return "";
231
+ const lines = activeNotes.map((a) => {
232
+ const num = a.id.replace(/^anc-/, "");
233
+ const files = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(", ")})` : "";
234
+ const desc = a.description ? ` \u2014 ${a.description}` : "";
235
+ return `\u2022 #${num} [${a.project}] ${a.title}${files}${desc}`;
236
+ });
237
+ return [
238
+ `<sticky-notes count="${activeNotes.length}">`,
239
+ `Pinned notes from previous sessions:`,
240
+ lines.join("\n"),
241
+ `When a note is completed, call anchor(action="settle", id="...") to tear it off.`,
242
+ `</sticky-notes>`
243
+ ].join("\n");
319
244
  }
320
- function getDefaultStorageDir() {
321
- if (process.env.ANCHOR_DIR) {
322
- return import_node_path2.default.resolve(process.env.ANCHOR_DIR);
323
- }
324
- const primaryDir = import_node_path2.default.join(import_node_os.default.homedir(), ".anchor");
325
- const legacyPiDir = import_node_path2.default.join(import_node_os.default.homedir(), ".pi", "agent", "anchors");
326
- if (!import_node_fs.default.existsSync(primaryDir) && import_node_fs.default.existsSync(legacyPiDir)) {
327
- try {
328
- import_node_fs.default.mkdirSync(primaryDir, { recursive: true });
329
- for (const file of ["state.json", "archive.jsonl", "graveyard.jsonl"]) {
330
- const src = import_node_path2.default.join(legacyPiDir, file);
331
- const dest = import_node_path2.default.join(primaryDir, file);
332
- if (import_node_fs.default.existsSync(src) && !import_node_fs.default.existsSync(dest)) {
333
- import_node_fs.default.copyFileSync(src, dest);
334
- }
245
+ function sweepQuietNotes(store, now = Date.now()) {
246
+ return store.withLock(() => {
247
+ const state = store.loadState();
248
+ const quieted = [];
249
+ for (const a of state.anchors) {
250
+ if (a.status === "active" && now - a.lastTouchedAt > QUIET_AFTER_DAYS * MS_PER_DAY) {
251
+ a.status = "sleeping";
252
+ a.updatedAt = now;
253
+ quieted.push(a.id);
335
254
  }
336
- } catch {
337
255
  }
338
- }
339
- return primaryDir;
256
+ if (quieted.length > 0) {
257
+ state.lastSweepAt = now;
258
+ store.saveState(state);
259
+ }
260
+ return { quieted };
261
+ });
262
+ }
263
+ function getDefaultStorageDir() {
264
+ if (process.env.ANCHOR_DIR) return import_node_path.default.resolve(process.env.ANCHOR_DIR);
265
+ return import_node_path.default.join(import_node_os.default.homedir(), ".anchor");
340
266
  }
341
267
  function sleepSync(ms) {
342
268
  const end = Date.now() + ms;
@@ -358,22 +284,18 @@ function acquireSyncLock(lockPath, maxWaitMs = 1500, staleTimeoutMs = 5e3) {
358
284
  attempt++;
359
285
  try {
360
286
  const fd = import_node_fs.default.openSync(lockPath, "wx");
361
- const meta = { pid: process.pid, createdAt: Date.now() };
362
- import_node_fs.default.writeFileSync(fd, JSON.stringify(meta), "utf-8");
287
+ import_node_fs.default.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: Date.now() }), "utf-8");
363
288
  import_node_fs.default.closeSync(fd);
364
289
  return () => {
365
290
  try {
366
- if (import_node_fs.default.existsSync(lockPath)) {
367
- import_node_fs.default.unlinkSync(lockPath);
368
- }
291
+ if (import_node_fs.default.existsSync(lockPath)) import_node_fs.default.unlinkSync(lockPath);
369
292
  } catch {
370
293
  }
371
294
  };
372
295
  } catch (err) {
373
296
  if (err.code === "EEXIST") {
374
297
  try {
375
- const content = import_node_fs.default.readFileSync(lockPath, "utf-8");
376
- const meta = JSON.parse(content);
298
+ const meta = JSON.parse(import_node_fs.default.readFileSync(lockPath, "utf-8"));
377
299
  const isStale = Date.now() - meta.createdAt > staleTimeoutMs;
378
300
  const isDead = meta.pid && !isProcessAlive(meta.pid);
379
301
  if (isStale || isDead) {
@@ -385,8 +307,7 @@ function acquireSyncLock(lockPath, maxWaitMs = 1500, staleTimeoutMs = 5e3) {
385
307
  }
386
308
  } catch {
387
309
  }
388
- const delay = Math.min(40, Math.floor(attempt * 4 + Math.random() * 8));
389
- sleepSync(delay);
310
+ sleepSync(Math.min(40, Math.floor(attempt * 4 + Math.random() * 8)));
390
311
  continue;
391
312
  }
392
313
  break;
@@ -395,50 +316,46 @@ function acquireSyncLock(lockPath, maxWaitMs = 1500, staleTimeoutMs = 5e3) {
395
316
  return () => {
396
317
  };
397
318
  }
398
- function atomicRenameWithRetry(tempPath, targetPath, maxAttempts = 6) {
399
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
319
+ function atomicRenameWithRetry(tempPath, targetPath, maxRetries = 5) {
320
+ let lastErr;
321
+ for (let i = 0; i < maxRetries; i++) {
400
322
  try {
401
323
  import_node_fs.default.renameSync(tempPath, targetPath);
402
324
  return;
403
325
  } catch (err) {
404
- const code = err?.code;
405
- if ((code === "EBUSY" || code === "EPERM" || code === "EACCES") && attempt < maxAttempts) {
406
- const delay = Math.floor(attempt * 20 + Math.random() * 15);
407
- sleepSync(delay);
326
+ lastErr = err;
327
+ if (err.code === "EPERM" || err.code === "EACCES" || err.code === "EBUSY") {
328
+ sleepSync(15 * (i + 1));
408
329
  continue;
409
330
  }
410
- const crashDump = `${tempPath}.failed`;
411
- try {
412
- if (import_node_fs.default.existsSync(tempPath)) import_node_fs.default.renameSync(tempPath, crashDump);
413
- } catch {
414
- }
415
- throw err;
331
+ break;
416
332
  }
417
333
  }
334
+ try {
335
+ import_node_fs.default.copyFileSync(tempPath, targetPath);
336
+ import_node_fs.default.unlinkSync(tempPath);
337
+ } catch {
338
+ throw lastErr;
339
+ }
418
340
  }
419
341
  var AnchorStore = class {
420
342
  storageDir;
421
343
  statePath;
344
+ lockPath;
422
345
  archivePath;
423
- graveyardPath;
424
- constructor(customStorageDir) {
425
- if (customStorageDir) {
426
- this.storageDir = import_node_path2.default.resolve(customStorageDir);
427
- } else {
428
- this.storageDir = getDefaultStorageDir();
429
- }
430
- this.statePath = import_node_path2.default.join(this.storageDir, "state.json");
431
- this.archivePath = import_node_path2.default.join(this.storageDir, "archive.jsonl");
432
- this.graveyardPath = import_node_path2.default.join(this.storageDir, "graveyard.jsonl");
433
- this.ensureDirs();
346
+ constructor(storageDir) {
347
+ this.storageDir = storageDir ? import_node_path.default.resolve(storageDir) : getDefaultStorageDir();
348
+ this.statePath = import_node_path.default.join(this.storageDir, "state.json");
349
+ this.lockPath = import_node_path.default.join(this.storageDir, "state.lock");
350
+ this.archivePath = import_node_path.default.join(this.storageDir, "archive.jsonl");
434
351
  }
435
- get lockPath() {
436
- return import_node_path2.default.join(this.storageDir, "state.lock");
352
+ ensureDirs() {
353
+ if (!import_node_fs.default.existsSync(this.storageDir)) {
354
+ import_node_fs.default.mkdirSync(this.storageDir, { recursive: true });
355
+ }
437
356
  }
438
- /**
439
- * Execute mutation within a cross-process exclusive lock
440
- */
441
357
  withLock(fn) {
358
+ this.ensureDirs();
442
359
  const release = acquireSyncLock(this.lockPath);
443
360
  try {
444
361
  return fn();
@@ -446,84 +363,35 @@ var AnchorStore = class {
446
363
  release();
447
364
  }
448
365
  }
449
- ensureDirs() {
450
- if (!import_node_fs.default.existsSync(this.storageDir)) {
451
- try {
452
- import_node_fs.default.mkdirSync(this.storageDir, { recursive: true });
453
- } catch {
454
- }
455
- }
456
- }
457
- /**
458
- * Load store state. If corrupt or missing, returns safe default.
459
- */
460
366
  loadState() {
461
367
  this.ensureDirs();
462
368
  if (!import_node_fs.default.existsSync(this.statePath)) {
463
- return {
464
- version: 1,
465
- anchors: [],
466
- lastSweepAt: Date.now()
467
- };
468
- }
469
- let raw = null;
470
- for (let attempt = 1; attempt <= 3; attempt++) {
471
- try {
472
- raw = import_node_fs.default.readFileSync(this.statePath, "utf-8");
473
- break;
474
- } catch (err) {
475
- if ((err?.code === "EBUSY" || err?.code === "EPERM") && attempt < 3) {
476
- sleepSync(15);
477
- continue;
478
- }
479
- }
480
- }
481
- if (raw === null) {
482
- return { version: 1, anchors: [], lastSweepAt: Date.now() };
369
+ return { version: 1, anchors: [], lastSweepAt: Date.now(), nextId: 1 };
483
370
  }
484
371
  try {
485
- const data = JSON.parse(raw);
486
- if (!data || typeof data !== "object" || !Array.isArray(data.anchors)) {
487
- throw new Error("Invalid state: anchors must be array");
372
+ const parsed = JSON.parse(import_node_fs.default.readFileSync(this.statePath, "utf-8"));
373
+ if (!parsed || !Array.isArray(parsed.anchors)) {
374
+ return { version: 1, anchors: [], lastSweepAt: Date.now(), nextId: 1 };
488
375
  }
489
- data.anchors = data.anchors.filter((a) => a && typeof a === "object" && typeof a.id === "string" && typeof a.title === "string").map((a) => ({
490
- ...a,
491
- files: Array.isArray(a.files) ? a.files.filter((f) => typeof f === "string") : [],
492
- tags: Array.isArray(a.tags) ? a.tags.filter((t) => typeof t === "string") : [],
493
- decay: {
494
- activeDays: Number.isFinite(a.decay?.activeDays) && a.decay.activeDays > 0 ? a.decay.activeDays : DURABLE_DECAY_POLICY.activeDays,
495
- sleepDays: Number.isFinite(a.decay?.sleepDays) && a.decay.sleepDays > 0 ? a.decay.sleepDays : DURABLE_DECAY_POLICY.sleepDays,
496
- graveyardDays: Number.isFinite(a.decay?.graveyardDays) && a.decay.graveyardDays > 0 ? a.decay.graveyardDays : DURABLE_DECAY_POLICY.graveyardDays
497
- }
498
- }));
499
- return data;
376
+ return parsed;
500
377
  } catch {
501
- const backupPath = import_node_path2.default.join(this.storageDir, `state.corrupt.${Date.now()}.json`);
502
378
  try {
503
- import_node_fs.default.renameSync(this.statePath, backupPath);
379
+ import_node_fs.default.copyFileSync(this.statePath, import_node_path.default.join(this.storageDir, `state.corrupt.${Date.now()}.json`));
504
380
  } catch {
505
381
  }
506
- return {
507
- version: 1,
508
- anchors: [],
509
- lastSweepAt: Date.now()
510
- };
382
+ return { version: 1, anchors: [], lastSweepAt: Date.now(), nextId: 1 };
511
383
  }
512
384
  }
513
- /**
514
- * Atomically save store state via temp file + atomic rename with Windows NTFS spin-retry
515
- */
516
385
  saveState(state) {
517
386
  this.ensureDirs();
518
- const rand = Math.random().toString(36).slice(2, 7);
519
- const tempPath = import_node_path2.default.join(this.storageDir, `state.tmp.${process.pid}.${Date.now()}.${rand}`);
387
+ const tempPath = `${this.statePath}.${process.pid}.${Date.now()}.tmp`;
520
388
  import_node_fs.default.writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf-8");
521
389
  atomicRenameWithRetry(tempPath, this.statePath);
522
390
  }
523
391
  generateId(state) {
524
392
  const numbers = state.anchors.map((a) => {
525
- const match = a.id.match(/^anc-(\d+)$/);
526
- return match ? parseInt(match[1], 10) : 0;
393
+ const m = a.id.match(/^anc-(\d+)$/);
394
+ return m ? parseInt(m[1], 10) : 0;
527
395
  }).filter((n) => n > 0);
528
396
  const maxExisting = numbers.length > 0 ? Math.max(...numbers) : 0;
529
397
  const nextNum = Math.max(maxExisting + 1, state.nextId || 1);
@@ -531,7 +399,7 @@ var AnchorStore = class {
531
399
  return `anc-${nextNum}`;
532
400
  }
533
401
  /**
534
- * Create a new Anchor with automatic project and cwd tagging
402
+ * Pin a new sticky note
535
403
  */
536
404
  create(input) {
537
405
  return this.withLock(() => {
@@ -544,34 +412,23 @@ var AnchorStore = class {
544
412
  if (input.project && (/^[a-zA-Z]:[\\/]/.test(input.project.trim()) || input.project.trim().startsWith("/"))) {
545
413
  cwd = normalizePath(input.project.trim());
546
414
  } else if (inferredFiles.length > 0 && (/^[a-zA-Z]:\//.test(inferredFiles[0]) || inferredFiles[0].startsWith("/"))) {
547
- const firstDir = import_node_path2.default.dirname(inferredFiles[0]);
548
- if (firstDir && firstDir !== ".") {
549
- cwd = normalizePath(firstDir);
550
- }
415
+ const firstDir = import_node_path.default.dirname(inferredFiles[0]);
416
+ if (firstDir && firstDir !== ".") cwd = normalizePath(firstDir);
551
417
  }
552
- const projectName = input.project ? input.project.trim() : cwd ? import_node_path2.default.basename(cwd) : "global";
553
- const recurrence = input.recurrence || detectRecurrence(input.title);
554
- const targetDate = input.targetDate || detectTargetDate(input.title, now);
555
- const durability = recurrence ? "durable" : input.durability || detectDurability(input.title);
556
- const decayPolicy = durability === "ephemeral" ? getEphemeralDecayPolicy(input.title) : DURABLE_DECAY_POLICY;
418
+ const projectName = input.project ? input.project.trim() : cwd ? import_node_path.default.basename(cwd) : "global";
557
419
  const anchor = {
558
420
  id,
559
421
  title: input.title.trim(),
560
422
  description: input.description?.trim(),
561
423
  priority: input.priority || "p1",
562
424
  status: "active",
563
- durability,
564
- recurrence,
565
- targetDate,
566
425
  project: projectName,
567
426
  cwd,
568
427
  createdAt: now,
569
428
  updatedAt: now,
570
429
  lastTouchedAt: now,
571
430
  files: inferredFiles,
572
- tags: (input.tags || []).map((t) => t.trim()).filter(Boolean),
573
- verifyCommand: input.verifyCommand?.trim() || void 0,
574
- decay: input.decay ? { ...input.decay } : { ...decayPolicy }
431
+ tags: (input.tags || []).map((t) => t.trim()).filter(Boolean)
575
432
  };
576
433
  state.anchors.push(anchor);
577
434
  this.saveState(state);
@@ -581,31 +438,19 @@ var AnchorStore = class {
581
438
  findAnchorIndex(anchors, id) {
582
439
  const clean = id.trim().replace(/^#/, "");
583
440
  return anchors.findIndex(
584
- (a) => a.id === clean || a.id === `anc-${clean}` || (clean.match(/^\d+$/) ? a.id === `anc-${parseInt(clean, 10)}` : false)
441
+ (a) => a.id === clean || a.id === `anc-${clean}` || (/^\d+$/.test(clean) ? a.id === `anc-${parseInt(clean, 10)}` : false)
585
442
  );
586
443
  }
587
- /**
588
- * Get an anchor by ID (supports 'anc-1', '#anc-1', '1', '01')
589
- */
590
444
  get(id) {
591
445
  const state = this.loadState();
592
446
  const idx = this.findAnchorIndex(state.anchors, id);
593
447
  return idx === -1 ? void 0 : state.anchors[idx];
594
448
  }
595
- /**
596
- * List anchors. By default, if cwd is provided, only returns anchors
597
- * belonging to this cwd or marked as global (''). Pass all: true for global view.
598
- */
599
449
  list(filter) {
600
450
  const state = this.loadState();
601
451
  const targetCwd = filter?.cwd ? normalizePath(filter.cwd) : null;
602
- const today = getTodayDateString(filter?.now);
603
452
  return state.anchors.filter((a) => {
604
- if (!filter?.all && filter?.status === "active" && a.recurrence === "daily" && a.lastCompletedDate === today) {
605
- return false;
606
- }
607
453
  if (filter?.status && a.status !== filter.status) return false;
608
- if (filter?.priority && a.priority !== filter.priority) return false;
609
454
  if (!filter?.all && targetCwd) {
610
455
  if (a.files.length > 0 && (a.cwd || a.project) && !isAnchorTargetingWorkspace(a, targetCwd)) {
611
456
  return false;
@@ -614,19 +459,13 @@ var AnchorStore = class {
614
459
  return true;
615
460
  });
616
461
  }
617
- /**
618
- * Update an existing anchor
619
- */
620
462
  update(id, patch) {
621
463
  return this.withLock(() => {
622
464
  const state = this.loadState();
623
465
  const idx = this.findAnchorIndex(state.anchors, id);
624
- if (idx === -1) {
625
- throw new Error(`Anchor not found: ${id}`);
626
- }
627
- const current = state.anchors[idx];
466
+ if (idx === -1) throw new Error(`Note not found: ${id}`);
628
467
  const updated = {
629
- ...current,
468
+ ...state.anchors[idx],
630
469
  ...patch,
631
470
  updatedAt: Date.now()
632
471
  };
@@ -638,16 +477,11 @@ var AnchorStore = class {
638
477
  return updated;
639
478
  });
640
479
  }
641
- /**
642
- * Touch an anchor to refresh its decay window and reactivate sleeping state
643
- */
644
480
  touch(id, timestamp = Date.now()) {
645
481
  return this.withLock(() => {
646
482
  const state = this.loadState();
647
483
  const idx = this.findAnchorIndex(state.anchors, id);
648
- if (idx === -1) {
649
- throw new Error(`Anchor not found: ${id}`);
650
- }
484
+ if (idx === -1) throw new Error(`Note not found: ${id}`);
651
485
  const current = state.anchors[idx];
652
486
  current.lastTouchedAt = timestamp;
653
487
  current.updatedAt = timestamp;
@@ -660,38 +494,20 @@ var AnchorStore = class {
660
494
  });
661
495
  }
662
496
  /**
663
- * Settle an anchor (recurring daily habits complete for today and wake up tomorrow)
497
+ * Tear off (settle) a completed sticky note and archive it to archive.jsonl
664
498
  */
665
499
  settle(id, evidence = {}, now = Date.now()) {
666
500
  return this.withLock(() => {
667
501
  const state = this.loadState();
668
502
  const idx = this.findAnchorIndex(state.anchors, id);
669
- if (idx === -1) {
670
- throw new Error(`Anchor not found: ${id}`);
671
- }
672
- const anchor = state.anchors[idx];
673
- const today = getTodayDateString(now);
674
- if (anchor.recurrence === "daily") {
675
- anchor.lastCompletedDate = today;
676
- anchor.lastTouchedAt = now;
677
- anchor.updatedAt = now;
678
- anchor.evidence = {
679
- ...evidence,
680
- settledAt: now,
681
- settledBy: evidence.settledBy || "manual-command"
682
- };
683
- this.ensureDirs();
684
- import_node_fs.default.appendFileSync(this.archivePath, JSON.stringify({ ...anchor, settledForDate: today }) + "\n", "utf-8");
685
- this.saveState(state);
686
- return anchor;
687
- }
503
+ if (idx === -1) throw new Error(`Note not found: ${id}`);
688
504
  const [settled] = state.anchors.splice(idx, 1);
689
505
  settled.status = "settled";
690
506
  settled.updatedAt = now;
691
507
  settled.evidence = {
692
508
  ...evidence,
693
509
  settledAt: now,
694
- settledBy: evidence.settledBy || "manual-command"
510
+ settledBy: evidence.settledBy || "manual"
695
511
  };
696
512
  this.ensureDirs();
697
513
  import_node_fs.default.appendFileSync(this.archivePath, JSON.stringify(settled) + "\n", "utf-8");
@@ -700,12 +516,12 @@ var AnchorStore = class {
700
516
  });
701
517
  }
702
518
  /**
703
- * Reverse/undo the last settled anchor, popping it from archive.jsonl back into state.json
519
+ * Put back (undo) the last torn-off sticky note
704
520
  */
705
521
  undoSettle() {
706
522
  return this.withLock(() => {
707
523
  if (!import_node_fs.default.existsSync(this.archivePath)) {
708
- throw new Error("No archived anchors to undo");
524
+ throw new Error("No archived notes to restore");
709
525
  }
710
526
  const raw = import_node_fs.default.readFileSync(this.archivePath, "utf-8");
711
527
  const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
@@ -714,23 +530,18 @@ var AnchorStore = class {
714
530
  const candidate = lines.pop();
715
531
  try {
716
532
  const parsed = JSON.parse(candidate);
717
- if (parsed && typeof parsed === "object" && typeof parsed.id === "string" && typeof parsed.title === "string") {
533
+ if (parsed && typeof parsed.id === "string" && typeof parsed.title === "string") {
718
534
  anchor = parsed;
719
535
  }
720
536
  } catch {
721
537
  }
722
538
  }
723
- if (!anchor) {
724
- throw new Error("Archive is empty, nothing to undo");
725
- }
539
+ if (!anchor) throw new Error("Archive is empty, nothing to undo");
726
540
  import_node_fs.default.writeFileSync(this.archivePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
727
541
  anchor.status = "active";
728
542
  anchor.updatedAt = Date.now();
543
+ anchor.lastTouchedAt = Date.now();
729
544
  delete anchor.evidence;
730
- delete anchor.settledForDate;
731
- if (anchor.recurrence === "daily") {
732
- delete anchor.lastCompletedDate;
733
- }
734
545
  const state = this.loadState();
735
546
  const existingIdx = state.anchors.findIndex((a) => a.id === anchor.id);
736
547
  if (existingIdx !== -1) {
@@ -741,540 +552,18 @@ var AnchorStore = class {
741
552
  state.anchors.push(anchor);
742
553
  }
743
554
  } else {
744
- const match = anchor.id.match(/^anc-(\d+)$/);
745
- if (match) {
746
- const restoredNum = parseInt(match[1], 10);
747
- state.nextId = Math.max(state.nextId || 1, restoredNum + 1);
748
- }
555
+ const m = anchor.id.match(/^anc-(\d+)$/);
556
+ if (m) state.nextId = Math.max(state.nextId || 1, parseInt(m[1], 10) + 1);
749
557
  state.anchors.push(anchor);
750
558
  }
751
559
  this.saveState(state);
752
560
  return anchor;
753
561
  });
754
562
  }
755
- /**
756
- * Evict anchor to graveyard.jsonl upon total decay expiry
757
- */
758
- dropToGraveyard(id, reason) {
759
- return this.withLock(() => {
760
- const state = this.loadState();
761
- const idx = state.anchors.findIndex((a) => a.id === id);
762
- if (idx === -1) {
763
- throw new Error(`Anchor not found: ${id}`);
764
- }
765
- const [anchor] = state.anchors.splice(idx, 1);
766
- anchor.status = "graveyard";
767
- anchor.updatedAt = Date.now();
768
- anchor.evidence = {
769
- summary: reason,
770
- settledAt: Date.now()
771
- };
772
- this.ensureDirs();
773
- import_node_fs.default.appendFileSync(this.graveyardPath, JSON.stringify(anchor) + "\n", "utf-8");
774
- this.saveState(state);
775
- return anchor;
776
- });
777
- }
778
- getArchive(filter) {
779
- if (!import_node_fs.default.existsSync(this.archivePath)) return [];
780
- try {
781
- const raw = import_node_fs.default.readFileSync(this.archivePath, "utf-8");
782
- const all = [];
783
- for (const l of raw.split(/\r?\n/)) {
784
- const trimmed = l.trim();
785
- if (!trimmed) continue;
786
- try {
787
- const parsed = JSON.parse(trimmed);
788
- if (parsed && typeof parsed === "object" && typeof parsed.id === "string") all.push(parsed);
789
- } catch {
790
- }
791
- }
792
- if (filter?.all || !filter?.cwd) return all;
793
- const targetCwd = normalizePath(filter.cwd);
794
- return all.filter((a) => !a.cwd || isSameOrSubPath(a.cwd, targetCwd));
795
- } catch {
796
- return [];
797
- }
798
- }
799
- getGraveyard(filter) {
800
- if (!import_node_fs.default.existsSync(this.graveyardPath)) return [];
801
- try {
802
- const raw = import_node_fs.default.readFileSync(this.graveyardPath, "utf-8");
803
- const all = [];
804
- for (const l of raw.split(/\r?\n/)) {
805
- const trimmed = l.trim();
806
- if (!trimmed) continue;
807
- try {
808
- const parsed = JSON.parse(trimmed);
809
- if (parsed && typeof parsed === "object" && typeof parsed.id === "string") all.push(parsed);
810
- } catch {
811
- }
812
- }
813
- if (filter?.all || !filter?.cwd) return all;
814
- const targetCwd = normalizePath(filter.cwd);
815
- return all.filter((a) => !a.cwd || isSameOrSubPath(a.cwd, targetCwd));
816
- } catch {
817
- return [];
818
- }
819
- }
820
- };
821
-
822
- // src/observer.ts
823
- var MUTATION_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "patch", "apply_diff", "create_file", "modify"]);
824
- var SessionTouchObserver = class {
825
- touchedFiles = /* @__PURE__ */ new Set();
826
- modifiedFiles = /* @__PURE__ */ new Set();
827
- inspectedFiles = /* @__PURE__ */ new Set();
828
- committed = false;
829
- commitMessages = [];
830
- clear() {
831
- this.touchedFiles.clear();
832
- this.modifiedFiles.clear();
833
- this.inspectedFiles.clear();
834
- this.committed = false;
835
- this.commitMessages = [];
836
- }
837
- /**
838
- * Observe and record a tool call invocation, separating inspection from mutation
839
- */
840
- recordToolCall(toolName, input) {
841
- if (!input || typeof input !== "object") return;
842
- const lowerTool = (toolName || "").toLowerCase();
843
- const isMutation = MUTATION_TOOLS.has(lowerTool);
844
- const paths = [];
845
- const pathField = input.path || input.filePath || input.file;
846
- if (typeof pathField === "string") {
847
- paths.push(normalizePath(pathField));
848
- }
849
- if (Array.isArray(input.paths)) {
850
- for (const p of input.paths) {
851
- if (typeof p === "string") {
852
- paths.push(normalizePath(p));
853
- }
854
- }
855
- }
856
- for (const p of paths) {
857
- this.touchedFiles.add(p);
858
- if (isMutation) {
859
- this.modifiedFiles.add(p);
860
- } else {
861
- this.inspectedFiles.add(p);
862
- }
863
- }
864
- if (lowerTool === "bash" || lowerTool === "powershell") {
865
- const cmd = String(input.command || "");
866
- if (cmd.includes("git commit")) {
867
- this.committed = true;
868
- const msgMatch = cmd.match(/-m\s+["']([^"']+)["']/);
869
- if (msgMatch && msgMatch[1]) {
870
- this.commitMessages.push(msgMatch[1]);
871
- }
872
- }
873
- }
874
- }
875
- /**
876
- * Add a file path manually (e.g. from git status diff)
877
- */
878
- addTouchedFile(filePath, isModified = true) {
879
- if (filePath) {
880
- const norm = normalizePath(filePath);
881
- this.touchedFiles.add(norm);
882
- if (isModified) {
883
- this.modifiedFiles.add(norm);
884
- } else {
885
- this.inspectedFiles.add(norm);
886
- }
887
- }
888
- }
889
- getModifiedFiles() {
890
- return Array.from(this.modifiedFiles);
891
- }
892
- getInspectedFiles() {
893
- return Array.from(this.inspectedFiles);
894
- }
895
- getTouchedFiles() {
896
- return Array.from(this.touchedFiles);
897
- }
898
- hasCommitted() {
899
- return this.committed;
900
- }
901
- getCommitMessages() {
902
- return [...this.commitMessages];
903
- }
904
- /**
905
- * Check if any commit message explicitly references or resolves an anchor
906
- */
907
- matchesCommit(anchor) {
908
- const idLower = anchor.id.toLowerCase();
909
- const title = anchor.title.toLowerCase();
910
- let titleTokens = [];
911
- if (typeof Intl !== "undefined" && Intl.Segmenter) {
912
- try {
913
- const segmenter = new Intl.Segmenter("und", { granularity: "word" });
914
- titleTokens = Array.from(segmenter.segment(title)).filter((s) => s.isWordLike).map((s) => s.segment.trim().toLowerCase()).filter((t) => t.length >= 2);
915
- } catch {
916
- titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
917
- }
918
- } else {
919
- titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
920
- }
921
- for (const msg of this.commitMessages) {
922
- const msgLower = msg.toLowerCase();
923
- if (msgLower.includes(idLower)) {
924
- return { matched: true, message: msg };
925
- }
926
- if (titleTokens.length > 0) {
927
- const matchingCount = titleTokens.filter((tok) => msgLower.includes(tok)).length;
928
- if (matchingCount >= Math.min(2, titleTokens.length)) {
929
- return { matched: true, message: msg };
930
- }
931
- }
932
- }
933
- return { matched: false };
934
- }
935
- };
936
-
937
- // src/decay.ts
938
- var import_node_fs2 = __toESM(require("fs"), 1);
939
- var MS_PER_DAY = 864e5;
940
- function evaluateAnchorDecay(anchor, now = Date.now()) {
941
- if (anchor.status === "settled" || anchor.status === "graveyard") {
942
- return {
943
- currentStatus: anchor.status,
944
- nextStatus: anchor.status,
945
- daysUntouched: 0,
946
- remainingActiveDays: 0,
947
- remainingSleepDays: 0
948
- };
949
- }
950
- const elapsedMs = Math.max(0, now - anchor.lastTouchedAt);
951
- const daysUntouched = elapsedMs / MS_PER_DAY;
952
- const activeDays = anchor.decay.activeDays;
953
- const graveyardDays = anchor.decay.graveyardDays;
954
- let nextStatus = anchor.status;
955
- if (daysUntouched <= activeDays) {
956
- nextStatus = "active";
957
- } else if (daysUntouched <= graveyardDays) {
958
- nextStatus = "sleeping";
959
- } else {
960
- nextStatus = "graveyard";
961
- }
962
- return {
963
- currentStatus: anchor.status,
964
- nextStatus,
965
- daysUntouched: Math.round(daysUntouched * 10) / 10,
966
- remainingActiveDays: Math.max(0, Math.round((activeDays - daysUntouched) * 10) / 10),
967
- remainingSleepDays: Math.max(0, Math.round((graveyardDays - daysUntouched) * 10) / 10)
968
- };
969
- }
970
- function sweepStore(store, now = Date.now()) {
971
- return store.withLock(() => {
972
- const state = store.loadState();
973
- const result = {
974
- transitionedToSleeping: [],
975
- wokenToActive: [],
976
- evictedToGraveyard: []
977
- };
978
- const toEvict = [];
979
- let stateModified = false;
980
- for (const anchor of state.anchors) {
981
- const evalResult = evaluateAnchorDecay(anchor, now);
982
- if (evalResult.nextStatus === "graveyard") {
983
- toEvict.push(anchor);
984
- } else if (evalResult.nextStatus !== anchor.status) {
985
- if (evalResult.nextStatus === "sleeping" && anchor.status === "active") {
986
- anchor.status = "sleeping";
987
- anchor.updatedAt = now;
988
- result.transitionedToSleeping.push(anchor.id);
989
- stateModified = true;
990
- } else if (evalResult.nextStatus === "active" && anchor.status === "sleeping") {
991
- anchor.status = "active";
992
- anchor.updatedAt = now;
993
- result.wokenToActive.push(anchor.id);
994
- stateModified = true;
995
- }
996
- }
997
- }
998
- if (toEvict.length > 0) {
999
- const evictIds = new Set(toEvict.map((e) => e.id));
1000
- state.anchors = state.anchors.filter((a) => !evictIds.has(a.id));
1001
- stateModified = true;
1002
- const graveyardLines = toEvict.map((exp) => {
1003
- const rec = {
1004
- ...exp,
1005
- status: "graveyard",
1006
- updatedAt: now,
1007
- evictedAt: now,
1008
- evictionReason: `Exceeded decay threshold (${exp.decay.graveyardDays} days untouched)`
1009
- };
1010
- return JSON.stringify(rec);
1011
- }).join("\n") + "\n";
1012
- import_node_fs2.default.appendFileSync(store.graveyardPath, graveyardLines, "utf-8");
1013
- for (const exp of toEvict) {
1014
- result.evictedToGraveyard.push(exp.id);
1015
- }
1016
- }
1017
- state.lastSweepAt = now;
1018
- if (stateModified) {
1019
- store.saveState(state);
1020
- }
1021
- return result;
1022
- });
1023
- }
1024
-
1025
- // src/context_injector.ts
1026
- var import_node_path3 = __toESM(require("path"), 1);
1027
- var COMMENT_FORMATS = {
1028
- // Double slash //
1029
- ".ts": (m) => `// ${m}`,
1030
- ".tsx": (m) => `// ${m}`,
1031
- ".js": (m) => `// ${m}`,
1032
- ".jsx": (m) => `// ${m}`,
1033
- ".mjs": (m) => `// ${m}`,
1034
- ".cjs": (m) => `// ${m}`,
1035
- ".java": (m) => `// ${m}`,
1036
- ".kt": (m) => `// ${m}`,
1037
- ".swift": (m) => `// ${m}`,
1038
- ".go": (m) => `// ${m}`,
1039
- ".rs": (m) => `// ${m}`,
1040
- ".c": (m) => `// ${m}`,
1041
- ".cpp": (m) => `// ${m}`,
1042
- ".h": (m) => `// ${m}`,
1043
- ".hpp": (m) => `// ${m}`,
1044
- ".cs": (m) => `// ${m}`,
1045
- ".dart": (m) => `// ${m}`,
1046
- ".scala": (m) => `// ${m}`,
1047
- ".zig": (m) => `// ${m}`,
1048
- ".proto": (m) => `// ${m}`,
1049
- // Hash #
1050
- ".py": (m) => `# ${m}`,
1051
- ".rb": (m) => `# ${m}`,
1052
- ".sh": (m) => `# ${m}`,
1053
- ".bash": (m) => `# ${m}`,
1054
- ".zsh": (m) => `# ${m}`,
1055
- ".yaml": (m) => `# ${m}`,
1056
- ".yml": (m) => `# ${m}`,
1057
- ".toml": (m) => `# ${m}`,
1058
- ".conf": (m) => `# ${m}`,
1059
- ".dockerfile": (m) => `# ${m}`,
1060
- ".ps1": (m) => `# ${m}`,
1061
- ".r": (m) => `# ${m}`,
1062
- // HTML / XML <!-- -->
1063
- ".html": (m) => `<!-- ${m} -->`,
1064
- ".xml": (m) => `<!-- ${m} -->`,
1065
- ".svg": (m) => `<!-- ${m} -->`,
1066
- ".vue": (m) => `<!-- ${m} -->`,
1067
- ".svelte": (m) => `<!-- ${m} -->`,
1068
- // Block comment /* */
1069
- ".css": (m) => `/* ${m} */`,
1070
- ".scss": (m) => `/* ${m} */`,
1071
- ".less": (m) => `/* ${m} */`,
1072
- // SQL / Lua / Haskell --
1073
- ".sql": (m) => `-- ${m}`,
1074
- ".lua": (m) => `-- ${m}`,
1075
- ".hs": (m) => `-- ${m}`,
1076
- // Batch REM
1077
- ".bat": (m) => `REM ${m}`,
1078
- ".cmd": (m) => `REM ${m}`
1079
563
  };
1080
- function makeSafeTaskAnnotation(filePath, anchor) {
1081
- const ext = import_node_path3.default.extname(filePath).toLowerCase();
1082
- const formatter = COMMENT_FORMATS[ext];
1083
- if (!formatter) {
1084
- return null;
1085
- }
1086
- return "\n\n" + formatter(`\u2316 anchor context: #${anchor.id} ${anchor.title} (${anchor.priority.toUpperCase()})`);
1087
- }
1088
- function renderColdStartAnchorsContext(store, cwdOrNow, nowArg, preflightFailures) {
1089
- let cwd;
1090
- let now = Date.now();
1091
- if (typeof cwdOrNow === "number") {
1092
- now = cwdOrNow;
1093
- } else if (typeof cwdOrNow === "string") {
1094
- cwd = cwdOrNow;
1095
- }
1096
- if (typeof nowArg === "number") {
1097
- now = nowArg;
1098
- }
1099
- sweepStore(store, now);
1100
- const activeAnchors = store.list({ status: "active", cwd });
1101
- if (activeAnchors.length === 0) {
1102
- return "";
1103
- }
1104
- const lines = activeAnchors.map((a) => {
1105
- const fileHint = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(", ")})` : "";
1106
- const tagHint = a.tags.length > 0 ? ` [${a.tags.join(", ")}]` : "";
1107
- const projHint = a.cwd ? `[${a.project}]` : "[global]";
1108
- let targetHint = "";
1109
- if (a.recurrence === "daily") {
1110
- targetHint = " [\u4ECA\u65E5\u5FAA\u73AF\xB7Daily]";
1111
- } else if (a.targetDate) {
1112
- const todayStr = getTodayDateString(now);
1113
- if (a.targetDate === todayStr) {
1114
- targetHint = " [\u4ECA\u65E5\u805A\u7126\xB7Due Today]";
1115
- } else if (a.targetDate < todayStr) {
1116
- targetHint = " [\u5DF2\u903E\u671F\xB7Overdue]";
1117
- } else {
1118
- targetHint = ` [\u6392\u671F:${a.targetDate}]`;
1119
- }
1120
- } else {
1121
- targetHint = " [\u957F\u671F\u613F\u666F\xB7Someday]";
1122
- }
1123
- const ageDays = Math.floor((now - a.createdAt) / (24 * 60 * 60 * 1e3));
1124
- const ageHint = ageDays > 0 ? ` (${ageDays}d old)` : "";
1125
- const failureSnippet = preflightFailures?.get(a.id);
1126
- const verifyHint = failureSnippet ? ` [\u26A1 \u7269\u7406\u9A8C\u8BC1\u5931\u8D25: ${failureSnippet.replace(/\s+/g, " ").slice(-100)}]` : a.verifyCommand ? ` [verify: ${a.verifyCommand}]` : "";
1127
- return `\u2022 #${a.id} ${projHint}${targetHint} [${a.priority.toUpperCase()}] ${a.title}${verifyHint}${ageHint}${tagHint}${fileHint}`;
1128
- });
1129
- return [
1130
- `<active-anchors count="${activeAnchors.length}">`,
1131
- `Unfinished cross-session commitments from previous sessions:`,
1132
- lines.join("\n"),
1133
- ``,
1134
- `CONVERSATIONAL PROTOCOL (HUMAN PARTNER TONE):`,
1135
- `- 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").`,
1136
- `- If an anchor has [\u26A1 \u7269\u7406\u9A8C\u8BC1\u5931\u8D25], its verifyCommand failed on session start. Target the failing assertion directly.`,
1137
- `- 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.`,
1138
- `- When a commitment is satisfied during work, automatically call anchor(action="settle", id=...) to clear it.`,
1139
- `</active-anchors>`
1140
- ].join("\n");
1141
- }
1142
-
1143
- // src/settlement.ts
1144
- var import_node_child_process = require("child_process");
1145
- function runPhysicalVerification(anchor, cwd) {
1146
- if (!anchor.verifyCommand?.trim()) {
1147
- return { success: false, output: "No verification command specified" };
1148
- }
1149
- const targetCwd = anchor.cwd || cwd;
1150
- try {
1151
- const stdout = (0, import_node_child_process.execSync)(anchor.verifyCommand, {
1152
- cwd: targetCwd,
1153
- encoding: "utf-8",
1154
- timeout: 8e3,
1155
- maxBuffer: 4 * 1024 * 1024,
1156
- stdio: ["ignore", "pipe", "pipe"]
1157
- });
1158
- return { success: true, output: stdout.trim() };
1159
- } catch (err) {
1160
- return { success: false, output: String(err.stderr || err.stdout || err.message).trim() };
1161
- }
1162
- }
1163
- function tryRealtimeAutoSettle(store, observer, cwd, executedCommand) {
1164
- const candidates = store.list({ cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
1165
- if (candidates.length === 0) return [];
1166
- const settledList = [];
1167
- const cmdTrimmed = (executedCommand || "").trim();
1168
- for (const a of candidates) {
1169
- if (observer.hasCommitted()) {
1170
- const commitMatch = observer.matchesCommit(a);
1171
- if (commitMatch.matched && (!cwd || isAnchorTargetingWorkspace(a, cwd))) {
1172
- const settled = store.settle(a.id, {
1173
- settledBy: "verification-test",
1174
- summary: `Auto-settled via Git commit: ${commitMatch.message}`,
1175
- touchedFiles: observer.getModifiedFiles()
1176
- });
1177
- settledList.push({ anchor: settled, reason: `git commit ("${commitMatch.message}")` });
1178
- continue;
1179
- }
1180
- }
1181
- if (a.verifyCommand && cmdTrimmed && cmdTrimmed.includes(a.verifyCommand.trim())) {
1182
- const settled = store.settle(a.id, {
1183
- settledBy: "verification-test",
1184
- summary: `Auto-settled via passing command: ${a.verifyCommand}`,
1185
- touchedFiles: observer.getModifiedFiles()
1186
- });
1187
- settledList.push({ anchor: settled, reason: `verifyCommand (${a.verifyCommand})` });
1188
- }
1189
- }
1190
- return settledList;
1191
- }
1192
- function generateSettlementProposals(store, touchedFiles, cwd, observer) {
1193
- const activeAndSleeping = store.list({ cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
1194
- const matches = findMatchedAnchors(activeAndSleeping, touchedFiles);
1195
- const seenIds = /* @__PURE__ */ new Set();
1196
- const proposals = [];
1197
- for (const match of matches) {
1198
- seenIds.add(match.anchor.id);
1199
- const fileList = match.matchedFiles.slice(0, 3).join(", ");
1200
- const moreSuffix = match.matchedFiles.length > 3 ? ` (+${match.matchedFiles.length - 3} more)` : "";
1201
- proposals.push({
1202
- anchor: match.anchor,
1203
- matchedFiles: match.matchedFiles,
1204
- reason: `Modified ${fileList}${moreSuffix}`,
1205
- recommendedAction: match.score >= 0.85 ? "settle" : "defer"
1206
- });
1207
- }
1208
- if (observer && observer.hasCommitted()) {
1209
- for (const a of activeAndSleeping) {
1210
- if (seenIds.has(a.id)) continue;
1211
- const cm = observer.matchesCommit(a);
1212
- if (cm.matched) {
1213
- seenIds.add(a.id);
1214
- proposals.push({
1215
- anchor: a,
1216
- matchedFiles: touchedFiles.slice(0, 3),
1217
- reason: `Matched git commit: "${cm.message}"`,
1218
- recommendedAction: "settle"
1219
- });
1220
- }
1221
- }
1222
- }
1223
- return proposals;
1224
- }
1225
564
 
1226
565
  // src/tui.ts
1227
- var import_node_path4 = __toESM(require("path"), 1);
1228
- function classifyAnchor(anchor, now = Date.now()) {
1229
- if (anchor.recurrence === "daily") {
1230
- return "Habits";
1231
- }
1232
- if (!anchor.targetDate) {
1233
- return "Backlog";
1234
- }
1235
- const todayStr = getTodayDateString(now);
1236
- if (anchor.targetDate <= todayStr) {
1237
- return "Today";
1238
- }
1239
- return "Upcoming";
1240
- }
1241
- function groupAnchorsByQuadrant(anchors, now = Date.now()) {
1242
- const groups = {
1243
- today: [],
1244
- upcoming: [],
1245
- habits: [],
1246
- backlog: []
1247
- };
1248
- for (const a of anchors) {
1249
- const q = classifyAnchor(a, now);
1250
- if (q === "Today") groups.today.push(a);
1251
- else if (q === "Upcoming") groups.upcoming.push(a);
1252
- else if (q === "Habits") groups.habits.push(a);
1253
- else groups.backlog.push(a);
1254
- }
1255
- return groups;
1256
- }
1257
- var PRIORITY_RANK = { p0: 0, p1: 1, p2: 2 };
1258
- function sortAnchorsForDisplay(anchors, cwd, now = Date.now()) {
1259
- const todayStr = getTodayDateString(now);
1260
- return [...anchors].sort((a, b) => {
1261
- if (a.status !== b.status) {
1262
- return a.status === "active" ? -1 : 1;
1263
- }
1264
- if (cwd) {
1265
- const aMatch = isAnchorTargetingWorkspace(a, cwd) ? 0 : 1;
1266
- const bMatch = isAnchorTargetingWorkspace(b, cwd) ? 0 : 1;
1267
- if (aMatch !== bMatch) return aMatch - bMatch;
1268
- }
1269
- const aDue = a.targetDate && a.targetDate <= todayStr ? 0 : 1;
1270
- const bDue = b.targetDate && b.targetDate <= todayStr ? 0 : 1;
1271
- if (aDue !== bDue) return aDue - bDue;
1272
- const pA = PRIORITY_RANK[a.priority] ?? 1;
1273
- const pB = PRIORITY_RANK[b.priority] ?? 1;
1274
- if (pA !== pB) return pA - pB;
1275
- return a.createdAt - b.createdAt;
1276
- });
1277
- }
566
+ var import_node_path2 = __toESM(require("path"), 1);
1278
567
  var ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
1279
568
  function stripAnsi(str) {
1280
569
  return str.replace(ANSI_REGEX, "");
@@ -1326,14 +615,12 @@ function truncateToWidth(str, maxWidth, ellipsis = "\u2026") {
1326
615
  const charLen = codePoint > 65535 ? 2 : 1;
1327
616
  const fullChar = singleLine.slice(i, i + charLen);
1328
617
  const charWidth = getDisplayWidth(fullChar);
1329
- if (accumWidth + charWidth > target) {
1330
- break;
1331
- }
618
+ if (accumWidth + charWidth > target) break;
1332
619
  accumulated += fullChar;
1333
620
  accumWidth += charWidth;
1334
621
  i += charLen;
1335
622
  }
1336
- accumulated = accumulated.replace(/\s*[\(\[\{(【]$/, "");
623
+ accumulated = accumulated.replace(/\s*[\(\[\{(【][^)\]})】]*$/, "");
1337
624
  return accumulated + ellipsis + (singleLine.includes("\x1B") ? "\x1B[0m" : "");
1338
625
  }
1339
626
  function padToWidth(str, targetWidth) {
@@ -1342,84 +629,59 @@ function padToWidth(str, targetWidth) {
1342
629
  if (current >= targetWidth) return truncated;
1343
630
  return truncated + " ".repeat(targetWidth - current);
1344
631
  }
1345
- function formatTargetDate(anchor, now = Date.now()) {
1346
- if (anchor.recurrence === "daily") {
1347
- return "Daily";
1348
- }
1349
- if (!anchor.targetDate) {
1350
- return "Someday";
1351
- }
1352
- const todayStr = getTodayDateString(now);
1353
- if (anchor.targetDate === todayStr) {
1354
- return "Today";
1355
- }
1356
- const MS_DAY = 24 * 60 * 60 * 1e3;
1357
- const tomorrowStr = getTodayDateString(now + MS_DAY);
1358
- if (anchor.targetDate === tomorrowStr) {
1359
- return "Tomorrow";
1360
- }
1361
- const in2dStr = getTodayDateString(now + 2 * MS_DAY);
1362
- if (anchor.targetDate === in2dStr) {
1363
- return "In 2d";
1364
- }
1365
- const in3dStr = getTodayDateString(now + 3 * MS_DAY);
1366
- if (anchor.targetDate === in3dStr) {
1367
- return "In 3d";
1368
- }
1369
- if (anchor.targetDate < todayStr) {
1370
- return "Overdue";
1371
- }
1372
- const parts = anchor.targetDate.split("-");
1373
- if (parts.length === 3) {
1374
- return `${parts[1]}-${parts[2]}`;
1375
- }
1376
- return anchor.targetDate;
632
+ function formatRelativeAge(timestamp, now = Date.now()) {
633
+ const diffSec = Math.max(0, Math.floor((now - timestamp) / 1e3));
634
+ if (diffSec < 60) return "just now";
635
+ const diffMin = Math.floor(diffSec / 60);
636
+ if (diffMin < 60) return `${diffMin}m ago`;
637
+ const diffHours = Math.floor(diffMin / 60);
638
+ if (diffHours < 24) return `${diffHours}h ago`;
639
+ const diffDays = Math.floor(diffHours / 24);
640
+ return `${diffDays}d ago`;
1377
641
  }
1378
- function formatCreationTime(timestamp, now = Date.now()) {
1379
- const d = new Date(timestamp);
1380
- const nowD = new Date(now);
1381
- const pad = (n) => String(n).padStart(2, "0");
1382
- const timeStr = `${pad(d.getHours())}:${pad(d.getMinutes())}`;
1383
- const todayStr = getTodayDateString(now);
1384
- const createdDayStr = getTodayDateString(timestamp);
1385
- if (createdDayStr === todayStr) {
1386
- return `Today ${timeStr}`;
642
+ function formatOrigin(anchor) {
643
+ if (typeof anchor === "string") {
644
+ if (!anchor) return "global";
645
+ return import_node_path2.default.basename(anchor.replace(/\\/g, "/")) || "global";
1387
646
  }
1388
- const yesterdayStr = getTodayDateString(now - 24 * 60 * 60 * 1e3);
1389
- if (createdDayStr === yesterdayStr) {
1390
- return `Yesterday ${timeStr}`;
647
+ let proj = "global";
648
+ if (anchor.project && anchor.project !== "global") {
649
+ const normProj = anchor.project.replace(/\\/g, "/").replace(/\/+$/, "");
650
+ if (normProj.includes("/")) {
651
+ const parts = normProj.split("/").filter(Boolean);
652
+ proj = parts.slice(-2).join("/");
653
+ } else {
654
+ proj = normProj;
655
+ }
656
+ } else if (anchor.cwd) {
657
+ proj = import_node_path2.default.basename(anchor.cwd.replace(/\\/g, "/")) || "global";
1391
658
  }
1392
- const mmdd = `${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
1393
- if (d.getFullYear() === nowD.getFullYear()) {
1394
- return `${mmdd} ${timeStr}`;
659
+ if (anchor.files && anchor.files.length > 0) {
660
+ const shortFile = import_node_path2.default.basename(anchor.files[0].replace(/\\/g, "/"));
661
+ if (shortFile && !proj.endsWith(shortFile)) {
662
+ return `${proj} \xB7 ${shortFile}`;
663
+ }
1395
664
  }
1396
- return `${d.getFullYear()}-${mmdd}`;
665
+ return proj;
1397
666
  }
1398
- function formatOrigin(anchor) {
1399
- if (typeof anchor === "object") {
1400
- if (anchor.project && anchor.project !== "global") {
1401
- const normProj = anchor.project.replace(/\\/g, "/").replace(/\/+$/, "");
1402
- if (normProj.includes("/")) {
1403
- const parts = normProj.split("/").filter(Boolean);
1404
- return parts.slice(-2).join("/");
1405
- }
1406
- return normProj;
667
+ function sortAnchorsForDisplay(anchors, cwd) {
668
+ return [...anchors].sort((a, b) => {
669
+ if (a.status !== b.status) {
670
+ return a.status === "active" ? -1 : 1;
1407
671
  }
1408
- if (!anchor.cwd) return "global";
1409
- return import_node_path4.default.basename(anchor.cwd.replace(/\\/g, "/")) || "global";
1410
- }
1411
- if (!anchor) return "global";
1412
- return import_node_path4.default.basename(anchor.replace(/\\/g, "/")) || "global";
672
+ if (cwd) {
673
+ const aMatch = isAnchorTargetingWorkspace(a, cwd) ? 0 : 1;
674
+ const bMatch = isAnchorTargetingWorkspace(b, cwd) ? 0 : 1;
675
+ if (aMatch !== bMatch) return aMatch - bMatch;
676
+ }
677
+ return b.lastTouchedAt - a.lastTouchedAt;
678
+ });
1413
679
  }
1414
680
  function updateAnchorStatusBar(ctx, store) {
1415
681
  if (!hasActiveUI(ctx)) return;
1416
682
  try {
1417
683
  const active = store.list({ status: "active", cwd: ctx.cwd });
1418
- if (active.length === 0) {
1419
- ctx.ui.setStatus("anchor", void 0);
1420
- return;
1421
- }
1422
- ctx.ui.setStatus("anchor", `\u2316 ${active.length}`);
684
+ ctx.ui.setStatus("anchor", active.length > 0 ? `\u2316 ${active.length}` : void 0);
1423
685
  } catch {
1424
686
  }
1425
687
  }
@@ -1431,26 +693,30 @@ function updateStartupBanner(ctx, store) {
1431
693
  ctx.ui.setWidget("anchor-startup", void 0);
1432
694
  return;
1433
695
  }
1434
- const groups = groupAnchorsByQuadrant(active);
1435
- const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
696
+ const sorted = sortAnchorsForDisplay(active, ctx.cwd);
1436
697
  const topItems = sorted.slice(0, 3);
1437
698
  const factory = (_tui, theme) => ({
1438
699
  render: (width) => {
1439
- const maxTitleWidth = Math.max(12, width - 36);
700
+ const maxTitleWidth = Math.max(14, width - 34);
1440
701
  const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
702
+ const label = active.length === 1 ? "1 note" : `${active.length} notes`;
1441
703
  const lines = [
1442
- truncateToWidth(fg("accent", `\u2316 Anchor \xB7 ${active.length} active commitments:`), width)
704
+ truncateToWidth(fg("accent", `\u2316 ${label}`), width)
1443
705
  ];
1444
- for (let i = 0; i < topItems.length; i++) {
1445
- const a = topItems[i];
706
+ for (const a of topItems) {
1446
707
  const num = a.id.replace(/^anc-/, "").padStart(2, "0");
1447
- const q = classifyAnchor(a);
1448
- const tgt = formatTargetDate(a);
1449
- const titlePadded = truncateToWidth(a.title, maxTitleWidth);
1450
- lines.push(truncateToWidth(` \u2022 ${fg("muted", `[${q}]`)} ${fg("dim", num)} ${titlePadded} ${fg("dim", `(${tgt})`)}`, width));
708
+ const origin = formatOrigin(a);
709
+ const age = formatRelativeAge(a.createdAt);
710
+ const titleClipped = truncateToWidth(a.title, maxTitleWidth);
711
+ lines.push(
712
+ truncateToWidth(
713
+ ` ${fg("dim", num)} ${titleClipped} ${fg("muted", `\xB7 ${origin}`)} ${fg("dim", `(${age})`)}`,
714
+ width
715
+ )
716
+ );
1451
717
  }
1452
718
  if (sorted.length > 3) {
1453
- lines.push(truncateToWidth(fg("dim", ` (+${sorted.length - 3} more \xB7 run /anchor to inspect)`), width));
719
+ lines.push(truncateToWidth(fg("dim", ` +${sorted.length - 3} more \xB7 /anchor`), width));
1454
720
  }
1455
721
  return lines;
1456
722
  },
@@ -1464,77 +730,62 @@ function updateStartupBanner(ctx, store) {
1464
730
  }
1465
731
  async function openAnchorDashboard(ctx, store) {
1466
732
  if (!hasActiveUI(ctx)) return;
1467
- const allUnfinished = store.list({ cwd: ctx.cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping");
1468
- if (allUnfinished.length === 0) {
1469
- ctx.ui.notify("\u2316 No active anchors. Use /pin <task> to record.", "info");
733
+ const allNotes = store.list({ cwd: ctx.cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping");
734
+ if (allNotes.length === 0) {
735
+ ctx.ui.notify("\u2316 No sticky notes. Use /pin <note> to stick one.", "info");
1470
736
  return;
1471
737
  }
1472
- const sortedList = sortAnchorsForDisplay(allUnfinished, ctx.cwd);
738
+ const sorted = sortAnchorsForDisplay(allNotes, ctx.cwd);
1473
739
  const optionMap = /* @__PURE__ */ new Map();
1474
740
  const displayOptions = [];
1475
741
  const termCols = process.stdout?.columns || 96;
1476
- const isNarrow = termCols < 88;
1477
- const titleWidth = isNarrow ? Math.max(18, termCols - 36) : 36;
1478
- for (let i = 0; i < sortedList.length; i++) {
1479
- const a = sortedList[i];
742
+ const isNarrow = termCols < 84;
743
+ const titleWidth = isNarrow ? Math.max(20, termCols - 30) : 42;
744
+ for (const a of sorted) {
1480
745
  const num = a.id.replace(/^anc-/, "").padStart(2, "0");
1481
- const badge = a.status === "sleeping" ? padToWidth("[Quiet]", 10) : padToWidth(`[${a.priority.toUpperCase()}]`, 10);
1482
746
  const origin = formatOrigin(a);
1483
- const target = a.status === "sleeping" ? "Quiet (0 tok)" : formatTargetDate(a);
1484
- const created = formatCreationTime(a.createdAt);
1485
- const titleWithFiles = a.files && a.files.length > 0 ? `${a.title} [${a.files.slice(0, 1).join(", ")}]` : a.title;
747
+ const age = a.status === "sleeping" ? `${formatRelativeAge(a.createdAt)} \xB7 quiet` : formatRelativeAge(a.createdAt);
1486
748
  const colNum = `${num} `;
1487
- const colTitle = padToWidth(titleWithFiles, titleWidth);
1488
- const colOrigin = padToWidth(origin, 12);
1489
- const colTarget = padToWidth(target, 14);
1490
- const colCreated = created;
1491
- const label = isNarrow ? `${colNum}${badge}${colTitle} ${colTarget}`.trimEnd() : `${colNum}${badge}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
749
+ const colTitle = padToWidth(a.title, titleWidth);
750
+ const colOrigin = padToWidth(origin, 22);
751
+ const label = isNarrow ? `${colNum}${colTitle} ${age}`.trimEnd() : `${colNum}${colTitle} ${colOrigin} ${age}`.trimEnd();
1492
752
  optionMap.set(label, a);
1493
753
  displayOptions.push(label);
1494
754
  }
1495
- const selected = await ctx.ui.select("\u2316 Anchors (select to settle, wake, or inspect):", displayOptions);
755
+ const selected = await ctx.ui.select("\u2316 Notes (select a note):", displayOptions);
1496
756
  if (!selected) return;
1497
757
  const anchor = optionMap.get(selected);
1498
758
  if (!anchor) return;
1499
759
  const actions = [
1500
- "\u2713 Settle (Mark completed and archive)",
1501
- ...anchor.status === "sleeping" ? ["\u26A1 Wake up (Activate for current session)"] : [],
1502
- "\u2139 View details & associated files",
1503
- "\u21A9 Cancel"
760
+ "\u2713 Done (tear off note)",
761
+ ...anchor.status === "sleeping" ? ["\u26A1 Wake up (mark active)"] : [],
762
+ "\u2139 Read full note",
763
+ "\u21A9 Back"
1504
764
  ];
1505
- const action = await ctx.ui.select(
1506
- `\u2316 Anchor #${anchor.id}: "${anchor.title}"`,
1507
- actions
1508
- );
765
+ const action = await ctx.ui.select(`\u2316 #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}"`, actions);
1509
766
  if (!action || action.startsWith("\u21A9")) return;
1510
767
  if (action.startsWith("\u2713")) {
1511
- store.settle(anchor.id, { settledBy: "manual-command" });
1512
- const successMsg = anchor.recurrence === "daily" ? `\u2316 Completed for today: "${anchor.title}" (resets tomorrow)` : `\u2316 Settled #${anchor.id}: "${anchor.title}" (/anchor undo to revert)`;
1513
- ctx.ui.notify(successMsg, "info");
768
+ store.settle(anchor.id, { settledBy: "manual" });
769
+ ctx.ui.notify(`\u2316 Torn off #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}" (/anchor undo to put back)`, "info");
1514
770
  updateAnchorStatusBar(ctx, store);
1515
771
  } else if (action.startsWith("\u26A1")) {
1516
772
  store.touch(anchor.id);
1517
- ctx.ui.notify(`\u2316 Woke up #${anchor.id}: "${anchor.title}"`, "info");
773
+ ctx.ui.notify(`\u2316 Woke up #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}"`, "info");
1518
774
  updateAnchorStatusBar(ctx, store);
1519
775
  } else if (action.startsWith("\u2139")) {
1520
776
  const details = [
1521
- `#${anchor.id} [${anchor.priority.toUpperCase()} \xB7 ${anchor.status.toUpperCase()}] ${anchor.title}`,
1522
- anchor.description ? `Description: ${anchor.description}` : null,
1523
- anchor.targetDate ? `Target Date: ${anchor.targetDate} (${formatTargetDate(anchor)})` : null,
1524
- anchor.files && anchor.files.length > 0 ? `Files: ${anchor.files.join(", ")}` : null,
1525
- anchor.verifyCommand ? `Verify Command: ${anchor.verifyCommand}` : null,
1526
- `Created: ${formatCreationTime(anchor.createdAt)} (in ${anchor.project || "global"})`
777
+ `\u2316 Note #${anchor.id.replace(/^anc-/, "")}: ${anchor.title}`,
778
+ anchor.description ? `Details: ${anchor.description}` : null,
779
+ anchor.files.length > 0 ? `Files: ${anchor.files.join(", ")}` : null,
780
+ `Workspace: ${anchor.project || "global"} \xB7 Pinned ${formatRelativeAge(anchor.createdAt)}`
1527
781
  ].filter(Boolean).join("\n");
1528
782
  ctx.ui.notify(details, "info");
1529
783
  }
1530
784
  }
1531
785
 
1532
786
  // src/extension.ts
1533
- var import_node_child_process2 = require("child_process");
1534
- var import_node_fs3 = __toESM(require("fs"), 1);
1535
- var import_node_path5 = __toESM(require("path"), 1);
1536
- var MUTATION_TOOLS2 = /* @__PURE__ */ new Set(["edit", "write", "patch", "apply_diff", "create_file", "modify"]);
1537
- function parseAnchorCommandInput(raw, now = Date.now()) {
787
+ var MUTATION_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "patch", "apply_diff", "create_file", "modify"]);
788
+ function parseAnchorCommandInput(raw) {
1538
789
  const trimmed = (raw || "").trim();
1539
790
  if (!trimmed || trimmed === "list" || trimmed === "ls") {
1540
791
  return { mode: "dashboard" };
@@ -1549,19 +800,9 @@ function parseAnchorCommandInput(raw, now = Date.now()) {
1549
800
  targetId: settleMatch[1].trim().replace(/^#/, "")
1550
801
  };
1551
802
  }
1552
- let targetDate;
1553
- const tokens = trimmed.split(/\s+/);
1554
- const dueIdx = tokens.findIndex((t) => t === "--due" || t === "-d");
1555
- if (dueIdx !== -1 && tokens[dueIdx + 1]) {
1556
- const rawDue = tokens[dueIdx + 1];
1557
- targetDate = detectTargetDate(rawDue, now) || rawDue;
1558
- tokens.splice(dueIdx, 2);
1559
- }
1560
- const cleanTitle = tokens.join(" ").trim();
1561
803
  return {
1562
804
  mode: "pin",
1563
- title: cleanTitle || trimmed,
1564
- targetDate
805
+ title: trimmed
1565
806
  };
1566
807
  }
1567
808
  function resolveAnchorTarget(store, idOrIndex, cwd) {
@@ -1574,58 +815,17 @@ function resolveAnchorTarget(store, idOrIndex, cwd) {
1574
815
  return sorted[seqIndex - 1].id;
1575
816
  }
1576
817
  }
1577
- const direct = store.get(clean);
1578
- return direct?.id;
818
+ return store.get(clean)?.id;
1579
819
  }
1580
820
  function extension_default(pi) {
1581
821
  const store = new AnchorStore();
1582
- const observer = new SessionTouchObserver();
1583
822
  const annotatedThisSession = /* @__PURE__ */ new Set();
1584
- const preflightFailures = /* @__PURE__ */ new Map();
1585
823
  pi.on("session_start", async (event, ctx) => {
1586
- observer.clear();
1587
824
  annotatedThisSession.clear();
1588
- preflightFailures.clear();
1589
- const sweep = sweepStore(store);
1590
- const sleepingForWorkspace = store.list({ status: "sleeping", all: true }).filter((a) => (a.files.length > 0 || a.project && a.project !== "global" || a.cwd) && isAnchorTargetingWorkspace(a, ctx.cwd));
1591
- for (const sa of sleepingForWorkspace) {
1592
- store.touch(sa.id);
1593
- }
1594
- if (sweep.transitionedToSleeping.length > 0 && sleepingForWorkspace.length === 0) {
1595
- ctx.ui.notify(`\u2316 Anchor: ${sweep.transitionedToSleeping.length} inactive task(s) moved to quiet mode (/anchor to view)`, "info");
1596
- }
1597
- const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
1598
- for (const a of activeAnchors) {
1599
- if (a.verifyCommand && (!a.cwd || isSameOrSubPath(a.cwd, ctx.cwd))) {
1600
- try {
1601
- const res = runPhysicalVerification(a, ctx.cwd);
1602
- if (res.success) {
1603
- store.settle(a.id, {
1604
- settledBy: "verification-test",
1605
- summary: `Physical pre-flight test passed: ${a.verifyCommand}`
1606
- });
1607
- ctx.ui.notify(`\u2316 Verification passed! Auto-settled #${a.id}: "${a.title}"`, "info");
1608
- } else {
1609
- preflightFailures.set(a.id, res.output.slice(-250));
1610
- }
1611
- } catch (_) {
1612
- }
1613
- }
1614
- }
1615
- try {
1616
- const home = process.env.HOME || process.env.USERPROFILE || "";
1617
- const pendingPath = import_node_path5.default.join(home, ".anchor", "pending_exit.json");
1618
- if (import_node_fs3.default.existsSync(pendingPath)) {
1619
- const pending = JSON.parse(import_node_fs3.default.readFileSync(pendingPath, "utf-8"));
1620
- const isExpired = Date.now() - pending.timestamp >= 24 * 60 * 60 * 1e3;
1621
- if (!isExpired && isSameOrSubPath(pending.cwd, ctx.cwd)) {
1622
- ctx.ui.notify(`\u2316 [Exit Guard] Detected ${pending.modifiedCount} uncommitted changes from previous session exit. Run /pin to record.`, "info");
1623
- import_node_fs3.default.unlinkSync(pendingPath);
1624
- } else if (isExpired) {
1625
- import_node_fs3.default.unlinkSync(pendingPath);
1626
- }
1627
- }
1628
- } catch (_) {
825
+ sweepQuietNotes(store);
826
+ const quietForHere = store.list({ status: "sleeping", all: true }).filter((a) => (a.files.length > 0 || a.project && a.project !== "global" || a.cwd) && isAnchorTargetingWorkspace(a, ctx.cwd));
827
+ for (const q of quietForHere) {
828
+ store.touch(q.id);
1629
829
  }
1630
830
  updateAnchorStatusBar(ctx, store);
1631
831
  if (event.reason !== "resume") {
@@ -1648,32 +848,32 @@ function extension_default(pi) {
1648
848
  pi.on("before_agent_start", async (event, ctx) => {
1649
849
  const entries = ctx.sessionManager?.getEntries() || [];
1650
850
  const messageTurns = entries.filter((e) => e.type === "message");
1651
- const isColdStart = messageTurns.length <= 1;
1652
- if (isColdStart) {
1653
- const contextSnippet = renderColdStartAnchorsContext(store, ctx.cwd, void 0, preflightFailures);
1654
- if (contextSnippet) {
1655
- return {
1656
- systemPrompt: `${event.systemPrompt}
851
+ if (messageTurns.length <= 1) {
852
+ const snippet = renderColdStartAnchorsContext(store, ctx.cwd);
853
+ if (snippet) {
854
+ return { systemPrompt: `${event.systemPrompt}
1657
855
 
1658
- ${contextSnippet}`
1659
- };
856
+ ${snippet}` };
1660
857
  }
1661
858
  }
1662
859
  });
1663
860
  pi.on("tool_result", async (event, ctx) => {
861
+ if (event.isError) return;
1664
862
  const lowerTool = String(event.toolName || "").toLowerCase();
1665
- if (!event.isError) {
1666
- observer.recordToolCall(event.toolName, event.input || {});
1667
- if (lowerTool === "bash" || lowerTool === "powershell") {
1668
- const executedCmd = String(event.input?.command || "");
1669
- if (executedCmd.includes("git commit") || executedCmd.length > 0) {
1670
- const settledNow = tryRealtimeAutoSettle(store, observer, ctx.cwd, executedCmd);
1671
- if (settledNow.length > 0) {
1672
- updateAnchorStatusBar(ctx, store);
1673
- if (hasActiveUI(ctx)) {
1674
- for (const item of settledNow) {
863
+ if (lowerTool === "bash" || lowerTool === "powershell") {
864
+ const cmd = String(event.input?.command || "");
865
+ if (cmd.includes("git commit")) {
866
+ const msgMatch = cmd.match(/-m\s+["']([^"']+)["']/);
867
+ if (msgMatch && msgMatch[1]) {
868
+ const commitMsg = msgMatch[1];
869
+ const candidates = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
870
+ for (const a of candidates) {
871
+ if (matchesCommitMessage(a, [commitMsg]).matched && isAnchorTargetingWorkspace(a, ctx.cwd)) {
872
+ store.settle(a.id, { settledBy: "commit", summary: commitMsg });
873
+ updateAnchorStatusBar(ctx, store);
874
+ if (hasActiveUI(ctx)) {
1675
875
  try {
1676
- ctx.ui.notify(`\u2316 Auto-settled #${item.anchor.id} ("${item.anchor.title}") via ${item.reason}`, "info");
876
+ ctx.ui.notify(`\u2316 Auto-torn off #${a.id.replace(/^anc-/, "")} ("${a.title}") on git commit`, "info");
1677
877
  } catch {
1678
878
  }
1679
879
  }
@@ -1685,132 +885,38 @@ ${contextSnippet}`
1685
885
  const pathInput = event.input?.path;
1686
886
  if (typeof pathInput !== "string") return;
1687
887
  const touchedPath = normalizePath(pathInput, ctx.cwd);
1688
- const anchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
1689
- const matches = findMatchedAnchors(anchors, [touchedPath]);
1690
- if (matches.length > 0) {
1691
- const isMutation = MUTATION_TOOLS2.has((event.toolName || "").toLowerCase());
1692
- if (isMutation && !event.isError) {
1693
- for (const m of matches) {
1694
- store.touch(m.anchor.id);
1695
- }
1696
- updateAnchorStatusBar(ctx, store);
1697
- }
1698
- const a = matches[0].anchor;
1699
- if (!annotatedThisSession.has(a.id)) {
1700
- annotatedThisSession.add(a.id);
1701
- const alert = makeSafeTaskAnnotation(touchedPath, a);
1702
- if (alert) {
1703
- const contents = [...event.content || []];
1704
- for (let i = contents.length - 1; i >= 0; i--) {
1705
- const item = contents[i];
1706
- if (item && item.type === "text") {
1707
- contents[i] = { ...item, text: item.text + alert };
1708
- return { content: contents };
1709
- }
1710
- }
1711
- }
1712
- }
1713
- }
1714
- });
1715
- pi.on("session_shutdown", async (event, ctx) => {
1716
- let gitStatus = "";
1717
- try {
1718
- gitStatus = (0, import_node_child_process2.execSync)("git status --porcelain", {
1719
- cwd: ctx.cwd,
1720
- encoding: "utf-8",
1721
- timeout: 2e3,
1722
- stdio: ["ignore", "pipe", "ignore"]
1723
- }).replace(/[\r\n]+$/, "");
1724
- const changed = gitStatus.split(/\r?\n/).filter(Boolean).map((l) => l.slice(3).replace(/^"(.*)"$/, "$1").trim()).filter(Boolean);
1725
- for (const f of changed) {
1726
- observer.addTouchedFile(f);
1727
- }
1728
- } catch {
1729
- }
1730
- if (event?.reason === "quit" && gitStatus) {
1731
- try {
1732
- const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
1733
- if (activeAnchors.length === 0) {
1734
- const home = process.env.HOME || process.env.USERPROFILE || "";
1735
- const pendingPath = import_node_path5.default.join(home, ".anchor", "pending_exit.json");
1736
- const data = {
1737
- cwd: ctx.cwd,
1738
- timestamp: Date.now(),
1739
- modifiedCount: gitStatus.split("\n").filter(Boolean).length
1740
- };
1741
- import_node_fs3.default.writeFileSync(pendingPath, JSON.stringify(data, null, 2), "utf-8");
1742
- }
1743
- } catch {
1744
- }
888
+ const notes = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
889
+ const matches = findMatchedAnchors(notes, [touchedPath]);
890
+ if (matches.length === 0) return;
891
+ if (MUTATION_TOOLS.has(lowerTool)) {
892
+ for (const m of matches) store.touch(m.anchor.id);
893
+ updateAnchorStatusBar(ctx, store);
1745
894
  }
1746
- const modified = observer.getModifiedFiles();
1747
- if (modified.length === 0 && !observer.hasCommitted()) return;
1748
- const proposals = generateSettlementProposals(store, modified, ctx.cwd, observer);
1749
- if (proposals.length === 0) return;
1750
- for (const prop of proposals) {
1751
- const a = prop.anchor;
1752
- const commitMatch = observer.matchesCommit(a);
1753
- if (commitMatch.matched) {
1754
- store.settle(a.id, {
1755
- settledBy: "verification-test",
1756
- summary: `Auto-settled via Git commit: ${commitMatch.message}`,
1757
- touchedFiles: prop.matchedFiles
1758
- });
1759
- if (hasActiveUI(ctx)) {
1760
- try {
1761
- ctx.ui.notify(`\u2316 Auto-settled #${a.id} via Git commit (run /anchor undo to revert)`, "info");
1762
- } catch {
1763
- }
1764
- }
1765
- continue;
1766
- }
1767
- if (a.verifyCommand) {
1768
- const verifyRes = runPhysicalVerification(a, ctx.cwd);
1769
- if (verifyRes.success) {
1770
- store.settle(a.id, {
1771
- settledBy: "verification-test",
1772
- summary: `Automated test passed: ${a.verifyCommand}`,
1773
- touchedFiles: prop.matchedFiles
1774
- });
1775
- if (hasActiveUI(ctx)) {
1776
- try {
1777
- ctx.ui.notify(`\u2316 Test passed (${a.verifyCommand}), auto-settled #${a.id}`, "info");
1778
- } catch {
1779
- }
895
+ const targetNote = matches[0].anchor;
896
+ if (!annotatedThisSession.has(targetNote.id)) {
897
+ annotatedThisSession.add(targetNote.id);
898
+ const whisper = makeSafeTaskAnnotation(touchedPath, targetNote);
899
+ if (whisper) {
900
+ const contents = [...event.content || []];
901
+ for (let i = contents.length - 1; i >= 0; i--) {
902
+ const item = contents[i];
903
+ if (item && item.type === "text") {
904
+ contents[i] = { ...item, text: item.text + whisper };
905
+ return { content: contents };
1780
906
  }
1781
- continue;
1782
907
  }
1783
908
  }
1784
- if (!hasActiveUI(ctx)) continue;
1785
- try {
1786
- const ok = await ctx.ui.confirm(
1787
- "\u2316 Settle Anchor Task",
1788
- `Task #${a.id} [${a.title}] touched files (${prop.matchedFiles.slice(0, 2).join(", ")}).
1789
- Mark as completed and archive?`
1790
- );
1791
- if (ok) {
1792
- store.settle(a.id, {
1793
- settledBy: "one-tap-settlement",
1794
- touchedFiles: prop.matchedFiles
1795
- });
1796
- ctx.ui.notify(`\u2316 Settled #${a.id}: "${a.title}"`, "info");
1797
- }
1798
- } catch {
1799
- }
1800
909
  }
1801
910
  });
1802
911
  pi.registerTool({
1803
912
  name: "anchor",
1804
- label: "Anchor (Cross-session Task Protocol)",
1805
- description: "Manage cross-session persistent tasks that survive terminal restarts and auto-evict upon code changes or settlement. Use when the user asks to retain, pin, remember, or track a multi-session goal across sessions, or when an ongoing commitment must not be forgotten. Actions: pin (create new cross-session anchor), list (view active and sleeping anchors), settle (close and archive a completed anchor), touch (refresh activity), sweep (run decay cleanup). Stored in ~/.anchor/ with zero project repository pollution.",
1806
- promptSnippet: "Anchor cross-session task contracts that survive terminal restarts and auto-evict",
913
+ label: "Anchor (Sticky Note)",
914
+ description: "Pin, list, or tear off cross-session sticky notes stored in ~/.anchor/ (zero git repo clutter). Actions: pin (stick a new note), list (view notes), settle (tear off completed note).",
915
+ promptSnippet: "Pin or tear off cross-session sticky notes that survive terminal restarts",
1807
916
  promptGuidelines: [
1808
- 'Use `anchor` when the user asks to retain a goal across sessions or record a reminder for later/tonight/tomorrow (e.g. "\u665A\u4E0A\u6E05\u7406\u5783\u573E", "\u660E\u5929\u4F18\u5316X", "\u4FDD\u7559\u4EFB\u52A1\u76F4\u5230\u5B8C\u6210").',
1809
- "BOUNDARY WITH TODO: `todo` is strictly for intra-session active work breakdown (step 1, step 2, step 3 right now). For future reminders or cross-session goals, ONLY use `anchor`. NEVER duplicate a cross-session reminder into both `todo` and `anchor`.",
1810
- "DO NOT over-engineer or assume automated scheduled tasks unless the user explicitly requests Windows Task Scheduler or cron.",
1811
- "Tasks are automatically scoped to the current project context without cluttering the project git repository.",
1812
- 'When code for an anchor is completed and verified, call `anchor` with action "settle" to archive it and free context.',
1813
- "Active anchors are automatically injected into future sessions in an ultra-compact block."
917
+ 'Use `anchor` when the user asks to remember something for later/tomorrow or across sessions (e.g. "\u8BB0\u4E00\u4E0B\u660E\u5929\u91CD\u6784\u9274\u6743", "\u665A\u4E0A\u6E05\u7406\u7F13\u5B58").',
918
+ "BOUNDARY WITH TODO: `todo` is for multi-step work right now in this session; `anchor` is a sticky note for future sessions.",
919
+ 'When a pinned note is completed, call `anchor` with action "settle" to tear it off.'
1814
920
  ],
1815
921
  parameters: import_typebox.Type.Object({
1816
922
  action: import_typebox.Type.Union([
@@ -1820,29 +926,19 @@ Mark as completed and archive?`
1820
926
  import_typebox.Type.Literal("touch"),
1821
927
  import_typebox.Type.Literal("sweep")
1822
928
  ]),
1823
- title: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Short imperative task title (for pin)" })),
1824
- description: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Detailed context, acceptance criteria, or technical notes" })),
1825
- priority: import_typebox.Type.Optional(import_typebox.Type.Union([import_typebox.Type.Literal("p0"), import_typebox.Type.Literal("p1"), import_typebox.Type.Literal("p2")])),
1826
- project: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Target project name or workspace (defaults to current directory if omitted)" })),
1827
- targetDate: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Expected completion date (e.g. YYYY-MM-DD, today, tomorrow)" })),
1828
- files: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String(), { description: "Associated file paths or directory prefixes" })),
1829
- tags: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String(), { description: "Domain tags" })),
1830
- verifyCommand: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Optional shell command for automated physical verification" })),
1831
- id: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Anchor ID, e.g. anc-1 (for settle or touch)" }))
929
+ title: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Sticky note text (for pin)" })),
930
+ description: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Optional extra details" })),
931
+ project: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Optional project name or folder" })),
932
+ files: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String(), { description: "Optional associated file paths" })),
933
+ id: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Note ID, e.g. anc-1 or 1 (for settle)" }))
1832
934
  }),
1833
935
  renderCall(args, theme) {
1834
936
  const action = args.action || "list";
1835
937
  let title = theme.fg("toolTitle", theme.bold("\u2316 anchor ")) + theme.fg("accent", action);
1836
938
  if (action === "pin" && args.title) {
1837
- const due = args.targetDate ? ` ${theme.fg("dim", `[${args.targetDate}]`)}` : "";
1838
- const files = args.files && args.files.length > 0 ? ` ${theme.fg("muted", `(${args.files.slice(0, 2).join(", ")})`)}` : "";
1839
- title += ` "${args.title}"${due}${files}`;
1840
- } else if (action === "settle" && args.id) {
1841
- title += ` ${theme.fg("dim", `#${args.id}`)}`;
1842
- } else if (action === "touch" && args.id) {
1843
- title += ` ${theme.fg("dim", `#${args.id}`)}`;
1844
- } else if (action === "list") {
1845
- if (args.priority) title += ` ${theme.fg("muted", `[${args.priority}]`)}`;
939
+ title += ` "${args.title}"`;
940
+ } else if ((action === "settle" || action === "touch") && args.id) {
941
+ title += ` ${theme.fg("dim", `#${String(args.id).replace(/^anc-/, "")}`)}`;
1846
942
  }
1847
943
  return {
1848
944
  render: (width) => [truncateToWidth(title, width)],
@@ -1851,17 +947,11 @@ Mark as completed and archive?`
1851
947
  };
1852
948
  },
1853
949
  renderResult(result, _options, theme) {
1854
- if (result.isError) {
1855
- const errText = result.content?.[0]?.text || "Execution failed";
1856
- return {
1857
- render: (width) => [truncateToWidth(theme.fg("error", `\u2717 ${errText}`), width)],
1858
- invalidate: () => {
1859
- }
1860
- };
1861
- }
1862
- const text = result.content?.[0]?.text || "OK";
950
+ const text = result.content?.[0]?.text || (result.isError ? "Failed" : "OK");
951
+ const color = result.isError ? "error" : "success";
952
+ const icon = result.isError ? "\u2717" : "\u2714";
1863
953
  return {
1864
- render: (width) => [truncateToWidth(theme.fg("success", `\u2714 ${text}`), width)],
954
+ render: (width) => [truncateToWidth(theme.fg(color, `${icon} ${text}`), width)],
1865
955
  invalidate: () => {
1866
956
  }
1867
957
  };
@@ -1870,91 +960,63 @@ Mark as completed and archive?`
1870
960
  if (!ctx) return { content: [{ type: "text", text: "Error: context required" }], isError: true };
1871
961
  if (params.action === "pin") {
1872
962
  if (!params.title) {
1873
- return { content: [{ type: "text", text: "Error: title is required for pin action" }], isError: true };
963
+ return { content: [{ type: "text", text: "Error: title is required for pin" }], isError: true };
1874
964
  }
1875
965
  const anc = store.create({
1876
966
  title: params.title,
1877
967
  description: params.description,
1878
- priority: params.priority || "p1",
1879
968
  project: params.project,
1880
- targetDate: params.targetDate,
1881
969
  cwd: ctx.cwd,
1882
- files: params.files || [],
1883
- tags: params.tags || [],
1884
- verifyCommand: params.verifyCommand
970
+ files: params.files || []
1885
971
  });
1886
972
  updateAnchorStatusBar(ctx, store);
1887
973
  return {
1888
- content: [{
1889
- type: "text",
1890
- text: `Successfully anchored task #${anc.id}: "${anc.title}" [${anc.project}]. Stored in ~/.anchor/.`
1891
- }],
974
+ content: [{ type: "text", text: `Pinned note #${anc.id.replace(/^anc-/, "")}: "${anc.title}"` }],
1892
975
  isError: false
1893
976
  };
1894
977
  }
1895
978
  if (params.action === "list") {
1896
- const active = store.list({ status: "active", cwd: ctx.cwd });
1897
- const sleeping = store.list({ status: "sleeping", cwd: ctx.cwd });
979
+ const notes = store.list({ cwd: ctx.cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping");
1898
980
  return {
1899
- content: [{
1900
- type: "text",
1901
- text: JSON.stringify({ active, sleeping }, null, 2)
1902
- }],
981
+ content: [{ type: "text", text: JSON.stringify(notes, null, 2) }],
1903
982
  isError: false
1904
983
  };
1905
984
  }
1906
985
  if (params.action === "settle") {
1907
986
  if (!params.id) {
1908
- return { content: [{ type: "text", text: "Error: id is required for settle action" }], isError: true };
987
+ return { content: [{ type: "text", text: "Error: id is required for settle" }], isError: true };
1909
988
  }
1910
989
  try {
1911
- const settled = store.settle(params.id, { settledBy: "verification-test" });
990
+ const targetId = resolveAnchorTarget(store, String(params.id), ctx.cwd) || String(params.id);
991
+ const settled = store.settle(targetId, { settledBy: "manual" });
1912
992
  updateAnchorStatusBar(ctx, store);
1913
993
  return {
1914
- content: [{
1915
- type: "text",
1916
- text: `Anchor #${settled.id} successfully settled and evicted from active context.`
1917
- }],
994
+ content: [{ type: "text", text: `Torn off note #${settled.id.replace(/^anc-/, "")}: "${settled.title}"` }],
1918
995
  isError: false
1919
996
  };
1920
997
  } catch (err) {
1921
998
  return { content: [{ type: "text", text: err.message }], isError: true };
1922
999
  }
1923
1000
  }
1924
- if (params.action === "touch") {
1925
- if (!params.id) {
1926
- return { content: [{ type: "text", text: "Error: id is required for touch action" }], isError: true };
1927
- }
1001
+ if (params.action === "touch" && params.id) {
1928
1002
  try {
1929
- const touched = store.touch(params.id);
1003
+ const touched = store.touch(String(params.id));
1930
1004
  updateAnchorStatusBar(ctx, store);
1931
- return {
1932
- content: [{
1933
- type: "text",
1934
- text: `Anchor #${touched.id} touched and decay timer refreshed.`
1935
- }],
1936
- isError: false
1937
- };
1005
+ return { content: [{ type: "text", text: `Refreshed note #${touched.id}` }], isError: false };
1938
1006
  } catch (err) {
1939
1007
  return { content: [{ type: "text", text: err.message }], isError: true };
1940
1008
  }
1941
1009
  }
1942
1010
  if (params.action === "sweep") {
1943
- const res = sweepStore(store);
1011
+ const res = sweepQuietNotes(store);
1944
1012
  updateAnchorStatusBar(ctx, store);
1945
- return {
1946
- content: [{
1947
- type: "text",
1948
- text: `Sweep complete: ${res.transitionedToSleeping.length} sleeping, ${res.evictedToGraveyard.length} evicted to graveyard.`
1949
- }],
1950
- isError: false
1951
- };
1013
+ return { content: [{ type: "text", text: `Quieted ${res.quieted.length} old note(s).` }], isError: false };
1952
1014
  }
1953
1015
  return { content: [{ type: "text", text: "Unknown action" }], isError: true };
1954
1016
  }
1955
1017
  });
1956
1018
  pi.registerCommand("anchor", {
1957
- description: "Inspect, pin, or settle active anchors (e.g. /anchor, /anchor done 1, /anchor undo)",
1019
+ description: "View, pin, or tear off sticky notes (/anchor, /anchor done 1, /anchor undo)",
1958
1020
  handler: async (args, ctx) => {
1959
1021
  const parsed = parseAnchorCommandInput(args);
1960
1022
  if (parsed.mode === "dashboard") {
@@ -1964,21 +1026,21 @@ Mark as completed and archive?`
1964
1026
  if (parsed.mode === "undo") {
1965
1027
  try {
1966
1028
  const restored = store.undoSettle();
1967
- ctx.ui.notify(`\u2316 Restored #${restored.id}: "${restored.title}"`, "info");
1029
+ ctx.ui.notify(`\u2316 Put back #${restored.id.replace(/^anc-/, "")}: "${restored.title}"`, "info");
1968
1030
  updateAnchorStatusBar(ctx, store);
1969
1031
  } catch (err) {
1970
- ctx.ui.notify(`Revert failed: ${err.message}`, "error");
1032
+ ctx.ui.notify(`Undo failed: ${err.message}`, "error");
1971
1033
  }
1972
1034
  return;
1973
1035
  }
1974
1036
  if (parsed.mode === "settle" && parsed.targetId) {
1975
1037
  const resolvedId = resolveAnchorTarget(store, parsed.targetId, ctx.cwd);
1976
1038
  if (!resolvedId) {
1977
- ctx.ui.notify(`\u2316 Anchor not found: ${parsed.targetId}`, "error");
1039
+ ctx.ui.notify(`\u2316 Note not found: ${parsed.targetId}`, "error");
1978
1040
  return;
1979
1041
  }
1980
- const settled = store.settle(resolvedId, { settledBy: "manual-command" });
1981
- ctx.ui.notify(`\u2316 Settled #${settled.id}: "${settled.title}" (/anchor undo to revert)`, "info");
1042
+ const settled = store.settle(resolvedId, { settledBy: "manual" });
1043
+ ctx.ui.notify(`\u2316 Torn off #${settled.id.replace(/^anc-/, "")}: "${settled.title}" (/anchor undo to put back)`, "info");
1982
1044
  updateAnchorStatusBar(ctx, store);
1983
1045
  return;
1984
1046
  }
@@ -1986,40 +1048,32 @@ Mark as completed and archive?`
1986
1048
  if (/^(?:#?anc-\d+|#\d+)$/i.test(rawTrimmed)) {
1987
1049
  const resolvedId = resolveAnchorTarget(store, rawTrimmed, ctx.cwd);
1988
1050
  if (resolvedId) {
1989
- const settled = store.settle(resolvedId, { settledBy: "manual-command" });
1990
- ctx.ui.notify(`\u2316 Settled #${settled.id}: "${settled.title}" (/anchor undo to revert)`, "info");
1051
+ const settled = store.settle(resolvedId, { settledBy: "manual" });
1052
+ ctx.ui.notify(`\u2316 Torn off #${settled.id.replace(/^anc-/, "")}: "${settled.title}" (/anchor undo to put back)`, "info");
1991
1053
  updateAnchorStatusBar(ctx, store);
1992
1054
  return;
1993
1055
  }
1994
1056
  }
1995
- const anc = store.create({
1996
- title: parsed.title || rawTrimmed,
1997
- targetDate: parsed.targetDate,
1998
- cwd: ctx.cwd
1999
- });
2000
- ctx.ui.notify(`\u2316 Pinned #${anc.id}: "${anc.title}"${anc.targetDate ? ` (due ${anc.targetDate})` : ""}`, "info");
1057
+ const anc = store.create({ title: parsed.title || rawTrimmed, cwd: ctx.cwd });
1058
+ ctx.ui.notify(`\u2316 Pinned #${anc.id.replace(/^anc-/, "")}: "${anc.title}"`, "info");
2001
1059
  updateAnchorStatusBar(ctx, store);
2002
1060
  }
2003
1061
  });
2004
1062
  pi.registerCommand("pin", {
2005
- description: "Pin a task across sessions (e.g. /pin Refactor auth --due tomorrow)",
1063
+ description: "Stick a quick note across sessions (e.g. /pin Check auth token expiry)",
2006
1064
  handler: async (args, ctx) => {
2007
- const parsed = parseAnchorCommandInput(args);
2008
- if (parsed.mode === "dashboard" || !parsed.title) {
1065
+ const text = (args || "").trim();
1066
+ if (!text) {
2009
1067
  await openAnchorDashboard(ctx, store);
2010
1068
  return;
2011
1069
  }
2012
- const anc = store.create({
2013
- title: parsed.title,
2014
- targetDate: parsed.targetDate,
2015
- cwd: ctx.cwd
2016
- });
2017
- ctx.ui.notify(`\u2316 Pinned #${anc.id}: "${anc.title}"${anc.targetDate ? ` (due ${anc.targetDate})` : ""}`, "info");
1070
+ const anc = store.create({ title: text, cwd: ctx.cwd });
1071
+ ctx.ui.notify(`\u2316 Pinned #${anc.id.replace(/^anc-/, "")}: "${anc.title}"`, "info");
2018
1072
  updateAnchorStatusBar(ctx, store);
2019
1073
  }
2020
1074
  });
2021
1075
  pi.registerCommand("done", {
2022
- description: "Complete an active anchor by row number or ID (e.g. /done 1)",
1076
+ description: "Tear off a sticky note by number or ID (e.g. /done 1)",
2023
1077
  handler: async (args, ctx) => {
2024
1078
  const target = (args || "").trim();
2025
1079
  if (!target) {
@@ -2028,16 +1082,16 @@ Mark as completed and archive?`
2028
1082
  }
2029
1083
  const resolvedId = resolveAnchorTarget(store, target, ctx.cwd);
2030
1084
  if (!resolvedId) {
2031
- ctx.ui.notify(`\u2316 Anchor not found: ${target}`, "error");
1085
+ ctx.ui.notify(`\u2316 Note not found: ${target}`, "error");
2032
1086
  return;
2033
1087
  }
2034
- const settled = store.settle(resolvedId, { settledBy: "manual-command" });
2035
- ctx.ui.notify(`\u2316 Settled #${settled.id}: "${settled.title}" (/anchor undo to revert)`, "info");
1088
+ const settled = store.settle(resolvedId, { settledBy: "manual" });
1089
+ ctx.ui.notify(`\u2316 Torn off #${settled.id.replace(/^anc-/, "")}: "${settled.title}" (/anchor undo to put back)`, "info");
2036
1090
  updateAnchorStatusBar(ctx, store);
2037
1091
  }
2038
1092
  });
2039
1093
  pi.registerCommand("anchors", {
2040
- description: "Inspect and settle active anchors",
1094
+ description: "View sticky notes",
2041
1095
  handler: async (_args, ctx) => {
2042
1096
  await openAnchorDashboard(ctx, store);
2043
1097
  }