pi-anchor 0.3.0 → 0.5.0

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