pi-anchor 0.3.0 → 0.4.0

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