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