pi-anchor 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/extension.js CHANGED
@@ -1,20 +1,24 @@
1
1
  import {
2
2
  AnchorStore,
3
3
  SessionTouchObserver,
4
+ detectTargetDate,
4
5
  findMatchedAnchors,
5
6
  generateSettlementProposals,
6
7
  hasActiveUI,
8
+ isAnchorTargetingWorkspace,
7
9
  isSameOrSubPath,
8
10
  makeSafeTaskAnnotation,
9
11
  normalizePath,
10
12
  openAnchorDashboard,
11
13
  renderColdStartAnchorsContext,
12
14
  runPhysicalVerification,
15
+ sortAnchorsForDisplay,
13
16
  sweepStore,
14
17
  truncateToWidth,
18
+ tryRealtimeAutoSettle,
15
19
  updateAnchorStatusBar,
16
20
  updateStartupBanner
17
- } from "./chunk-G72UHPBN.js";
21
+ } from "./chunk-Q7IILJFA.js";
18
22
 
19
23
  // src/extension.ts
20
24
  import { Type } from "@sinclair/typebox";
@@ -22,6 +26,49 @@ import { execSync } from "child_process";
22
26
  import fs from "fs";
23
27
  import path from "path";
24
28
  var MUTATION_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "patch", "apply_diff", "create_file", "modify"]);
29
+ function parseAnchorCommandInput(raw, now = Date.now()) {
30
+ const trimmed = (raw || "").trim();
31
+ if (!trimmed || trimmed === "list" || trimmed === "ls") {
32
+ return { mode: "dashboard" };
33
+ }
34
+ if (trimmed === "undo") {
35
+ return { mode: "undo" };
36
+ }
37
+ const settleMatch = trimmed.match(/^(?:done|rm|close|settle)\s+(.+)$/i);
38
+ if (settleMatch) {
39
+ return {
40
+ mode: "settle",
41
+ targetId: settleMatch[1].trim().replace(/^#/, "")
42
+ };
43
+ }
44
+ let targetDate;
45
+ const tokens = trimmed.split(/\s+/);
46
+ const dueIdx = tokens.findIndex((t) => t === "--due" || t === "-d");
47
+ if (dueIdx !== -1 && tokens[dueIdx + 1]) {
48
+ const rawDue = tokens[dueIdx + 1];
49
+ targetDate = detectTargetDate(rawDue, now) || rawDue;
50
+ tokens.splice(dueIdx, 2);
51
+ }
52
+ const cleanTitle = tokens.join(" ").trim();
53
+ return {
54
+ mode: "pin",
55
+ title: cleanTitle || trimmed,
56
+ targetDate
57
+ };
58
+ }
59
+ function resolveAnchorTarget(store, idOrIndex, cwd) {
60
+ const clean = idOrIndex.trim().replace(/^#/, "");
61
+ if (/^\d+$/.test(clean)) {
62
+ const seqIndex = parseInt(clean, 10);
63
+ const visible = store.list({ cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping");
64
+ const sorted = sortAnchorsForDisplay(visible, cwd);
65
+ if (seqIndex >= 1 && seqIndex <= sorted.length) {
66
+ return sorted[seqIndex - 1].id;
67
+ }
68
+ }
69
+ const direct = store.get(clean);
70
+ return direct?.id;
71
+ }
25
72
  function extension_default(pi) {
26
73
  const store = new AnchorStore();
27
74
  const observer = new SessionTouchObserver();
@@ -32,8 +79,12 @@ function extension_default(pi) {
32
79
  annotatedThisSession.clear();
33
80
  preflightFailures.clear();
34
81
  const sweep = sweepStore(store);
35
- if (sweep.transitionedToSleeping.length > 0) {
36
- ctx.ui.notify(`Anchor: ${sweep.transitionedToSleeping.length} \u4E2A\u975E\u6D3B\u8DC3\u4EFB\u52A1\u5DF2\u8FDB\u5165\u4F11\u7720`, "info");
82
+ 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));
83
+ for (const sa of sleepingForWorkspace) {
84
+ store.touch(sa.id);
85
+ }
86
+ if (sweep.transitionedToSleeping.length > 0 && sleepingForWorkspace.length === 0) {
87
+ ctx.ui.notify(`\u2316 Anchor: ${sweep.transitionedToSleeping.length} inactive task(s) moved to quiet mode (/anchor to view)`, "info");
37
88
  }
38
89
  const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
39
90
  for (const a of activeAnchors) {
@@ -45,7 +96,7 @@ function extension_default(pi) {
45
96
  settledBy: "verification-test",
46
97
  summary: `Physical pre-flight test passed: ${a.verifyCommand}`
47
98
  });
48
- ctx.ui.notify(`\u2693 \u7269\u7406\u9A8C\u8BC1\u901A\u8FC7\uFF01\u951A\u70B9 #${a.id} ("${a.title}") \u5DF2\u81EA\u52A8\u7ED3\u6848\u5F52\u6863\u3002`, "info");
99
+ ctx.ui.notify(`\u2316 Verification passed! Auto-settled #${a.id}: "${a.title}"`, "info");
49
100
  } else {
50
101
  preflightFailures.set(a.id, res.output.slice(-250));
51
102
  }
@@ -102,8 +153,26 @@ ${contextSnippet}`
102
153
  }
103
154
  });
104
155
  pi.on("tool_result", async (event, ctx) => {
156
+ const lowerTool = String(event.toolName || "").toLowerCase();
105
157
  if (!event.isError) {
106
158
  observer.recordToolCall(event.toolName, event.input || {});
159
+ if (lowerTool === "bash" || lowerTool === "powershell") {
160
+ const executedCmd = String(event.input?.command || "");
161
+ if (executedCmd.includes("git commit") || executedCmd.length > 0) {
162
+ const settledNow = tryRealtimeAutoSettle(store, observer, ctx.cwd, executedCmd);
163
+ if (settledNow.length > 0) {
164
+ updateAnchorStatusBar(ctx, store);
165
+ if (hasActiveUI(ctx)) {
166
+ for (const item of settledNow) {
167
+ try {
168
+ ctx.ui.notify(`\u2316 Auto-settled #${item.anchor.id} ("${item.anchor.title}") via ${item.reason}`, "info");
169
+ } catch {
170
+ }
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
107
176
  }
108
177
  const pathInput = event.input?.path;
109
178
  if (typeof pathInput !== "string") return;
@@ -167,8 +236,8 @@ ${contextSnippet}`
167
236
  }
168
237
  }
169
238
  const modified = observer.getModifiedFiles();
170
- if (modified.length === 0) return;
171
- const proposals = generateSettlementProposals(store, modified, ctx.cwd);
239
+ if (modified.length === 0 && !observer.hasCommitted()) return;
240
+ const proposals = generateSettlementProposals(store, modified, ctx.cwd, observer);
172
241
  if (proposals.length === 0) return;
173
242
  for (const prop of proposals) {
174
243
  const a = prop.anchor;
@@ -377,14 +446,14 @@ Mark as completed and archive?`
377
446
  }
378
447
  });
379
448
  pi.registerCommand("anchor", {
380
- description: "Inspect and settle active anchors",
449
+ description: "Inspect, pin, or settle active anchors (e.g. /anchor, /anchor done 1, /anchor undo)",
381
450
  handler: async (args, ctx) => {
382
- const input = (args || "").trim();
383
- if (!input || input === "list" || input === "ls") {
451
+ const parsed = parseAnchorCommandInput(args);
452
+ if (parsed.mode === "dashboard") {
384
453
  await openAnchorDashboard(ctx, store);
385
454
  return;
386
455
  }
387
- if (input === "undo") {
456
+ if (parsed.mode === "undo") {
388
457
  try {
389
458
  const restored = store.undoSettle();
390
459
  ctx.ui.notify(`\u2316 Restored #${restored.id}: "${restored.title}"`, "info");
@@ -394,29 +463,68 @@ Mark as completed and archive?`
394
463
  }
395
464
  return;
396
465
  }
397
- const targetId = input.startsWith("#") ? input.slice(1) : input;
398
- const item = store.get(targetId);
399
- if (item) {
400
- store.settle(targetId, { settledBy: "manual-command" });
401
- ctx.ui.notify(`\u2316 Settled: "${item.title}"`, "info");
466
+ if (parsed.mode === "settle" && parsed.targetId) {
467
+ const resolvedId = resolveAnchorTarget(store, parsed.targetId, ctx.cwd);
468
+ if (!resolvedId) {
469
+ ctx.ui.notify(`\u2316 Anchor not found: ${parsed.targetId}`, "error");
470
+ return;
471
+ }
472
+ const settled = store.settle(resolvedId, { settledBy: "manual-command" });
473
+ ctx.ui.notify(`\u2316 Settled #${settled.id}: "${settled.title}" (/anchor undo to revert)`, "info");
402
474
  updateAnchorStatusBar(ctx, store);
403
475
  return;
404
476
  }
405
- const anc = store.create({ title: input, cwd: ctx.cwd });
406
- ctx.ui.notify(`\u2316 Pinned #${anc.id}: "${anc.title}"`, "info");
477
+ const rawTrimmed = (args || "").trim();
478
+ if (/^(?:#?anc-\d+|#\d+)$/i.test(rawTrimmed)) {
479
+ const resolvedId = resolveAnchorTarget(store, rawTrimmed, ctx.cwd);
480
+ if (resolvedId) {
481
+ const settled = store.settle(resolvedId, { settledBy: "manual-command" });
482
+ ctx.ui.notify(`\u2316 Settled #${settled.id}: "${settled.title}" (/anchor undo to revert)`, "info");
483
+ updateAnchorStatusBar(ctx, store);
484
+ return;
485
+ }
486
+ }
487
+ const anc = store.create({
488
+ title: parsed.title || rawTrimmed,
489
+ targetDate: parsed.targetDate,
490
+ cwd: ctx.cwd
491
+ });
492
+ ctx.ui.notify(`\u2316 Pinned #${anc.id}: "${anc.title}"${anc.targetDate ? ` (due ${anc.targetDate})` : ""}`, "info");
407
493
  updateAnchorStatusBar(ctx, store);
408
494
  }
409
495
  });
410
496
  pi.registerCommand("pin", {
411
- description: "Quickly pin a task across sessions",
497
+ description: "Pin a task across sessions (e.g. /pin Refactor auth --due tomorrow)",
412
498
  handler: async (args, ctx) => {
413
- const title = (args || "").trim();
414
- if (!title) {
499
+ const parsed = parseAnchorCommandInput(args);
500
+ if (parsed.mode === "dashboard" || !parsed.title) {
415
501
  await openAnchorDashboard(ctx, store);
416
502
  return;
417
503
  }
418
- const anc = store.create({ title, cwd: ctx.cwd });
419
- ctx.ui.notify(`\u2316 Pinned #${anc.id}: "${anc.title}"`, "info");
504
+ const anc = store.create({
505
+ title: parsed.title,
506
+ targetDate: parsed.targetDate,
507
+ cwd: ctx.cwd
508
+ });
509
+ ctx.ui.notify(`\u2316 Pinned #${anc.id}: "${anc.title}"${anc.targetDate ? ` (due ${anc.targetDate})` : ""}`, "info");
510
+ updateAnchorStatusBar(ctx, store);
511
+ }
512
+ });
513
+ pi.registerCommand("done", {
514
+ description: "Complete an active anchor by row number or ID (e.g. /done 1)",
515
+ handler: async (args, ctx) => {
516
+ const target = (args || "").trim();
517
+ if (!target) {
518
+ await openAnchorDashboard(ctx, store);
519
+ return;
520
+ }
521
+ const resolvedId = resolveAnchorTarget(store, target, ctx.cwd);
522
+ if (!resolvedId) {
523
+ ctx.ui.notify(`\u2316 Anchor not found: ${target}`, "error");
524
+ return;
525
+ }
526
+ const settled = store.settle(resolvedId, { settledBy: "manual-command" });
527
+ ctx.ui.notify(`\u2316 Settled #${settled.id}: "${settled.title}" (/anchor undo to revert)`, "info");
420
528
  updateAnchorStatusBar(ctx, store);
421
529
  }
422
530
  });
@@ -428,5 +536,7 @@ Mark as completed and archive?`
428
536
  });
429
537
  }
430
538
  export {
431
- extension_default as default
539
+ extension_default as default,
540
+ parseAnchorCommandInput,
541
+ resolveAnchorTarget
432
542
  };
package/dist/index.cjs CHANGED
@@ -44,6 +44,7 @@ __export(index_exports, {
44
44
  detectRecurrence: () => detectRecurrence,
45
45
  detectTargetDate: () => detectTargetDate,
46
46
  evaluateAnchorDecay: () => evaluateAnchorDecay,
47
+ extractPathsFromText: () => extractPathsFromText,
47
48
  findMatchedAnchors: () => findMatchedAnchors,
48
49
  formatCreationTime: () => formatCreationTime,
49
50
  formatOrigin: () => formatOrigin,
@@ -59,6 +60,7 @@ __export(index_exports, {
59
60
  globToRegExp: () => globToRegExp,
60
61
  groupAnchorsByQuadrant: () => groupAnchorsByQuadrant,
61
62
  hasActiveUI: () => hasActiveUI,
63
+ isAnchorTargetingWorkspace: () => isAnchorTargetingWorkspace,
62
64
  isSameOrSubPath: () => isSameOrSubPath,
63
65
  makeSafeTaskAnnotation: () => makeSafeTaskAnnotation,
64
66
  matchAnchorAgainstTouchedFiles: () => matchAnchorAgainstTouchedFiles,
@@ -68,9 +70,11 @@ __export(index_exports, {
68
70
  renderActiveAnchorsContext: () => renderActiveAnchorsContext,
69
71
  renderColdStartAnchorsContext: () => renderColdStartAnchorsContext,
70
72
  runPhysicalVerification: () => runPhysicalVerification,
73
+ sortAnchorsForDisplay: () => sortAnchorsForDisplay,
71
74
  stripAnsi: () => stripAnsi,
72
75
  sweepStore: () => sweepStore,
73
76
  truncateToWidth: () => truncateToWidth,
77
+ tryRealtimeAutoSettle: () => tryRealtimeAutoSettle,
74
78
  updateAnchorStatusBar: () => updateAnchorStatusBar,
75
79
  updateStartupBanner: () => updateStartupBanner
76
80
  });
@@ -78,23 +82,23 @@ module.exports = __toCommonJS(index_exports);
78
82
 
79
83
  // src/types.ts
80
84
  var EPHEMERAL_DECAY_POLICY = {
81
- activeDays: 1,
82
- // 24 hours active
83
- sleepDays: 1,
84
- // 24 hours sleeping
85
- graveyardDays: 2
86
- // 48 hours total before auto-clearing
85
+ activeDays: 7,
86
+ // 7 days active before quiet sleep
87
+ sleepDays: 30,
88
+ graveyardDays: 9999
89
+ // Never silently auto-delete unfinished tasks
87
90
  };
88
91
  var DURABLE_DECAY_POLICY = {
89
- activeDays: 3,
92
+ activeDays: 7,
93
+ // 7 days active before quiet sleep
90
94
  sleepDays: 30,
91
95
  graveyardDays: 9999
92
96
  // Never permanently auto-dropped; preserved indefinitely
93
97
  };
94
98
  var DEFAULT_DECAY_POLICY = {
95
- activeDays: 3,
96
- sleepDays: 7,
97
- graveyardDays: 14
99
+ activeDays: 7,
100
+ sleepDays: 30,
101
+ graveyardDays: 9999
98
102
  };
99
103
 
100
104
  // src/store.ts
@@ -133,6 +137,39 @@ function isSameOrSubPath(anchorCwd, targetCwd) {
133
137
  const cmpT = isWinStyle ? normT.toLowerCase() : normT;
134
138
  return cmpA === cmpT || cmpT.startsWith(cmpA + "/") || cmpA.startsWith(cmpT + "/");
135
139
  }
140
+ function extractPathsFromText(text) {
141
+ if (!text) return [];
142
+ const results = /* @__PURE__ */ new Set();
143
+ const regex = /(?:[a-zA-Z]:[\\/][^\s"'`,。;!?、,;)>\]]+|(?:\b(?:src|lib|dist|tests|test|docs|scripts|packages|extensions|bin|assets)[\\/][^\s"'`,。;!?、,;)>\]]+)|(?:\b[\w.-]+\.(?:ts|tsx|js|jsx|mjs|cjs|py|rs|go|java|c|cpp|h|cs|md|json|yaml|yml|toml|html|css|sql|sh)\b))/g;
144
+ let match;
145
+ while ((match = regex.exec(text)) !== null) {
146
+ const cleaned = normalizePath(match[0].replace(/[.:;,'")\]}>]+$/, ""));
147
+ if (cleaned && cleaned.length >= 3) {
148
+ results.add(cleaned);
149
+ }
150
+ }
151
+ return Array.from(results);
152
+ }
153
+ function isAnchorTargetingWorkspace(anchor, targetCwd) {
154
+ if (!targetCwd) return true;
155
+ const normTarget = targetCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
156
+ const targetBase = import_node_path.default.basename(normTarget).toLowerCase();
157
+ if (anchor.project && anchor.project !== "global") {
158
+ const normProj = anchor.project.trim().replace(/\\/g, "/").replace(/\/+$/, "");
159
+ if (isSameOrSubPath(normProj, normTarget)) return true;
160
+ const projBase = import_node_path.default.basename(normProj).toLowerCase();
161
+ if (projBase && projBase === targetBase) return true;
162
+ }
163
+ if (anchor.files && anchor.files.length > 0) {
164
+ for (const f of anchor.files) {
165
+ if (isSameOrSubPath(f, normTarget)) return true;
166
+ }
167
+ }
168
+ if (anchor.cwd && isSameOrSubPath(anchor.cwd, normTarget)) {
169
+ return true;
170
+ }
171
+ return false;
172
+ }
136
173
  function matchAnchorAgainstTouchedFiles(anchor, touchedFiles) {
137
174
  if (!anchor.files || anchor.files.length === 0) return null;
138
175
  const normTouched = touchedFiles.map((f) => normalizePath(f, anchor.cwd)).filter(Boolean);
@@ -235,10 +272,10 @@ function detectDurability(title) {
235
272
  function getEphemeralDecayPolicy(title) {
236
273
  const lower = title.toLowerCase();
237
274
  if (lower.includes("\u5927\u540E\u5929")) {
238
- return { activeDays: 3, sleepDays: 1, graveyardDays: 4 };
275
+ return { activeDays: 7, sleepDays: 30, graveyardDays: 9999 };
239
276
  }
240
277
  if (lower.includes("\u540E\u5929")) {
241
- return { activeDays: 2, sleepDays: 1, graveyardDays: 3 };
278
+ return { activeDays: 7, sleepDays: 30, graveyardDays: 9999 };
242
279
  }
243
280
  return { ...EPHEMERAL_DECAY_POLICY };
244
281
  }
@@ -549,7 +586,17 @@ var AnchorStore = class {
549
586
  const state = this.loadState();
550
587
  const now = Date.now();
551
588
  const id = this.generateId(state);
552
- const cwd = input.cwd ? normalizePath(input.cwd) : "";
589
+ const explicitFiles = (input.files || []).map((f) => normalizePath(f)).filter(Boolean);
590
+ const inferredFiles = explicitFiles.length > 0 ? explicitFiles : extractPathsFromText(`${input.title} ${input.description || ""}`);
591
+ let cwd = input.cwd ? normalizePath(input.cwd) : "";
592
+ if (input.project && (/^[a-zA-Z]:[\\/]/.test(input.project.trim()) || input.project.trim().startsWith("/"))) {
593
+ cwd = normalizePath(input.project.trim());
594
+ } else if (inferredFiles.length > 0 && (/^[a-zA-Z]:\//.test(inferredFiles[0]) || inferredFiles[0].startsWith("/"))) {
595
+ const firstDir = import_node_path2.default.dirname(inferredFiles[0]);
596
+ if (firstDir && firstDir !== ".") {
597
+ cwd = normalizePath(firstDir);
598
+ }
599
+ }
553
600
  const projectName = input.project ? input.project.trim() : cwd ? import_node_path2.default.basename(cwd) : "global";
554
601
  const recurrence = input.recurrence || detectRecurrence(input.title);
555
602
  const targetDate = input.targetDate || detectTargetDate(input.title, now);
@@ -569,7 +616,7 @@ var AnchorStore = class {
569
616
  createdAt: now,
570
617
  updatedAt: now,
571
618
  lastTouchedAt: now,
572
- files: (input.files || []).map((f) => normalizePath(f)).filter(Boolean),
619
+ files: inferredFiles,
573
620
  tags: (input.tags || []).map((t) => t.trim()).filter(Boolean),
574
621
  verifyCommand: input.verifyCommand?.trim() || void 0,
575
622
  decay: input.decay ? { ...input.decay } : { ...decayPolicy }
@@ -608,7 +655,7 @@ var AnchorStore = class {
608
655
  if (filter?.status && a.status !== filter.status) return false;
609
656
  if (filter?.priority && a.priority !== filter.priority) return false;
610
657
  if (!filter?.all && targetCwd) {
611
- if (a.files.length > 0 && a.cwd && !isSameOrSubPath(a.cwd, targetCwd)) {
658
+ if (a.files.length > 0 && (a.cwd || a.project) && !isAnchorTargetingWorkspace(a, targetCwd)) {
612
659
  return false;
613
660
  }
614
661
  }
@@ -928,20 +975,66 @@ function runPhysicalVerification(anchor, cwd) {
928
975
  return { success: false, output: String(err.stderr || err.stdout || err.message).trim() };
929
976
  }
930
977
  }
931
- function generateSettlementProposals(store, touchedFiles, cwd) {
978
+ function tryRealtimeAutoSettle(store, observer, cwd, executedCommand) {
979
+ const candidates = store.list({ cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
980
+ if (candidates.length === 0) return [];
981
+ const settledList = [];
982
+ const cmdTrimmed = (executedCommand || "").trim();
983
+ for (const a of candidates) {
984
+ if (observer.hasCommitted()) {
985
+ const commitMatch = observer.matchesCommit(a);
986
+ if (commitMatch.matched && (!cwd || isAnchorTargetingWorkspace(a, cwd))) {
987
+ const settled = store.settle(a.id, {
988
+ settledBy: "verification-test",
989
+ summary: `Auto-settled via Git commit: ${commitMatch.message}`,
990
+ touchedFiles: observer.getModifiedFiles()
991
+ });
992
+ settledList.push({ anchor: settled, reason: `git commit ("${commitMatch.message}")` });
993
+ continue;
994
+ }
995
+ }
996
+ if (a.verifyCommand && cmdTrimmed && cmdTrimmed.includes(a.verifyCommand.trim())) {
997
+ const settled = store.settle(a.id, {
998
+ settledBy: "verification-test",
999
+ summary: `Auto-settled via passing command: ${a.verifyCommand}`,
1000
+ touchedFiles: observer.getModifiedFiles()
1001
+ });
1002
+ settledList.push({ anchor: settled, reason: `verifyCommand (${a.verifyCommand})` });
1003
+ }
1004
+ }
1005
+ return settledList;
1006
+ }
1007
+ function generateSettlementProposals(store, touchedFiles, cwd, observer) {
932
1008
  const activeAndSleeping = store.list({ cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
933
1009
  const matches = findMatchedAnchors(activeAndSleeping, touchedFiles);
1010
+ const seenIds = /* @__PURE__ */ new Set();
934
1011
  const proposals = [];
935
1012
  for (const match of matches) {
1013
+ seenIds.add(match.anchor.id);
936
1014
  const fileList = match.matchedFiles.slice(0, 3).join(", ");
937
- const moreSuffix = match.matchedFiles.length > 3 ? ` \u7B49 ${match.matchedFiles.length} \u4E2A\u6587\u4EF6` : "";
1015
+ const moreSuffix = match.matchedFiles.length > 3 ? ` (+${match.matchedFiles.length - 3} more)` : "";
938
1016
  proposals.push({
939
1017
  anchor: match.anchor,
940
1018
  matchedFiles: match.matchedFiles,
941
- reason: `\u672C\u6B21\u4F1A\u8BDD\u4FEE\u6539\u4E86 ${fileList}${moreSuffix}`,
1019
+ reason: `Modified ${fileList}${moreSuffix}`,
942
1020
  recommendedAction: match.score >= 0.85 ? "settle" : "defer"
943
1021
  });
944
1022
  }
1023
+ if (observer && observer.hasCommitted()) {
1024
+ for (const a of activeAndSleeping) {
1025
+ if (seenIds.has(a.id)) continue;
1026
+ const cm = observer.matchesCommit(a);
1027
+ if (cm.matched) {
1028
+ seenIds.add(a.id);
1029
+ proposals.push({
1030
+ anchor: a,
1031
+ matchedFiles: touchedFiles.slice(0, 3),
1032
+ reason: `Matched git commit: "${cm.message}"`,
1033
+ recommendedAction: "settle"
1034
+ });
1035
+ }
1036
+ }
1037
+ }
945
1038
  return proposals;
946
1039
  }
947
1040
  function formatSettlementCard(proposal) {
@@ -989,6 +1082,27 @@ function groupAnchorsByQuadrant(anchors, now = Date.now()) {
989
1082
  }
990
1083
  return groups;
991
1084
  }
1085
+ var PRIORITY_RANK = { p0: 0, p1: 1, p2: 2 };
1086
+ function sortAnchorsForDisplay(anchors, cwd, now = Date.now()) {
1087
+ const todayStr = getTodayDateString(now);
1088
+ return [...anchors].sort((a, b) => {
1089
+ if (a.status !== b.status) {
1090
+ return a.status === "active" ? -1 : 1;
1091
+ }
1092
+ if (cwd) {
1093
+ const aMatch = isAnchorTargetingWorkspace(a, cwd) ? 0 : 1;
1094
+ const bMatch = isAnchorTargetingWorkspace(b, cwd) ? 0 : 1;
1095
+ if (aMatch !== bMatch) return aMatch - bMatch;
1096
+ }
1097
+ const aDue = a.targetDate && a.targetDate <= todayStr ? 0 : 1;
1098
+ const bDue = b.targetDate && b.targetDate <= todayStr ? 0 : 1;
1099
+ if (aDue !== bDue) return aDue - bDue;
1100
+ const pA = PRIORITY_RANK[a.priority] ?? 1;
1101
+ const pB = PRIORITY_RANK[b.priority] ?? 1;
1102
+ if (pA !== pB) return pA - pB;
1103
+ return a.createdAt - b.createdAt;
1104
+ });
1105
+ }
992
1106
  var ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
993
1107
  function stripAnsi(str) {
994
1108
  return str.replace(ANSI_REGEX, "");
@@ -1117,11 +1231,19 @@ function formatRemainingTtl(anchor, now = Date.now()) {
1117
1231
  }
1118
1232
  function formatOrigin(anchor) {
1119
1233
  if (typeof anchor === "object") {
1120
- if (!anchor.cwd) return anchor.project || "global";
1121
- return import_node_path3.default.basename(anchor.cwd) || "global";
1234
+ if (anchor.project && anchor.project !== "global") {
1235
+ const normProj = anchor.project.replace(/\\/g, "/").replace(/\/+$/, "");
1236
+ if (normProj.includes("/")) {
1237
+ const parts = normProj.split("/").filter(Boolean);
1238
+ return parts.slice(-2).join("/");
1239
+ }
1240
+ return normProj;
1241
+ }
1242
+ if (!anchor.cwd) return "global";
1243
+ return import_node_path3.default.basename(anchor.cwd.replace(/\\/g, "/")) || "global";
1122
1244
  }
1123
1245
  if (!anchor) return "global";
1124
- return import_node_path3.default.basename(anchor) || "global";
1246
+ return import_node_path3.default.basename(anchor.replace(/\\/g, "/")) || "global";
1125
1247
  }
1126
1248
  function updateAnchorStatusBar(ctx, store) {
1127
1249
  if (!hasActiveUI(ctx)) return;
@@ -1176,51 +1298,47 @@ function updateStartupBanner(ctx, store) {
1176
1298
  }
1177
1299
  async function openAnchorDashboard(ctx, store) {
1178
1300
  if (!hasActiveUI(ctx)) return;
1179
- const list = store.list({ status: "active", cwd: ctx.cwd });
1180
- if (list.length === 0) {
1301
+ const allUnfinished = store.list({ cwd: ctx.cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping");
1302
+ if (allUnfinished.length === 0) {
1181
1303
  ctx.ui.notify("\u2316 No active anchors. Use /pin <task> to record.", "info");
1182
1304
  return;
1183
1305
  }
1184
- const groups = groupAnchorsByQuadrant(list);
1185
- const sortedList = [
1186
- ...groups.today,
1187
- ...groups.upcoming,
1188
- ...groups.habits,
1189
- ...groups.backlog
1190
- ];
1306
+ const sortedList = sortAnchorsForDisplay(allUnfinished, ctx.cwd);
1191
1307
  const optionMap = /* @__PURE__ */ new Map();
1192
1308
  const displayOptions = [];
1193
1309
  const termCols = process.stdout?.columns || 96;
1194
1310
  const isNarrow = termCols < 88;
1195
- const titleWidth = isNarrow ? Math.max(18, termCols - 36) : 34;
1311
+ const titleWidth = isNarrow ? Math.max(18, termCols - 36) : 36;
1196
1312
  for (let i = 0; i < sortedList.length; i++) {
1197
1313
  const a = sortedList[i];
1198
1314
  const num = a.id.replace(/^anc-/, "").padStart(2, "0");
1199
- const category = padToWidth(`[${classifyAnchor(a)}]`, 12);
1315
+ const badge = a.status === "sleeping" ? padToWidth("[Quiet]", 10) : padToWidth(`[${a.priority.toUpperCase()}]`, 10);
1200
1316
  const origin = formatOrigin(a);
1201
- const target = formatTargetDate(a);
1317
+ const target = a.status === "sleeping" ? "Quiet (0 tok)" : formatTargetDate(a);
1202
1318
  const created = formatCreationTime(a.createdAt);
1203
1319
  const titleWithFiles = a.files && a.files.length > 0 ? `${a.title} [${a.files.slice(0, 1).join(", ")}]` : a.title;
1204
1320
  const colNum = `${num} `;
1205
1321
  const colTitle = padToWidth(titleWithFiles, titleWidth);
1206
- const colOrigin = padToWidth(origin, 10);
1207
- const colTarget = padToWidth(target, 12);
1322
+ const colOrigin = padToWidth(origin, 12);
1323
+ const colTarget = padToWidth(target, 14);
1208
1324
  const colCreated = created;
1209
- const label = isNarrow ? `${colNum}${category}${colTitle} ${colTarget}`.trimEnd() : `${colNum}${category}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
1325
+ const label = isNarrow ? `${colNum}${badge}${colTitle} ${colTarget}`.trimEnd() : `${colNum}${badge}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
1210
1326
  optionMap.set(label, a);
1211
1327
  displayOptions.push(label);
1212
1328
  }
1213
- const selected = await ctx.ui.select("\u2316 Anchors (select to inspect or settle):", displayOptions);
1329
+ const selected = await ctx.ui.select("\u2316 Anchors (select to settle, wake, or inspect):", displayOptions);
1214
1330
  if (!selected) return;
1215
1331
  const anchor = optionMap.get(selected);
1216
1332
  if (!anchor) return;
1333
+ const actions = [
1334
+ "\u2713 Settle (Mark completed and archive)",
1335
+ ...anchor.status === "sleeping" ? ["\u26A1 Wake up (Activate for current session)"] : [],
1336
+ "\u2139 View details & associated files",
1337
+ "\u21A9 Cancel"
1338
+ ];
1217
1339
  const action = await ctx.ui.select(
1218
1340
  `\u2316 Anchor #${anchor.id}: "${anchor.title}"`,
1219
- [
1220
- "\u2713 Settle (Mark completed and archive)",
1221
- "\u2139 View details & associated files",
1222
- "\u21A9 Cancel"
1223
- ]
1341
+ actions
1224
1342
  );
1225
1343
  if (!action || action.startsWith("\u21A9")) return;
1226
1344
  if (action.startsWith("\u2713")) {
@@ -1228,9 +1346,13 @@ async function openAnchorDashboard(ctx, store) {
1228
1346
  const successMsg = anchor.recurrence === "daily" ? `\u2316 Completed for today: "${anchor.title}" (resets tomorrow)` : `\u2316 Settled #${anchor.id}: "${anchor.title}" (/anchor undo to revert)`;
1229
1347
  ctx.ui.notify(successMsg, "info");
1230
1348
  updateAnchorStatusBar(ctx, store);
1349
+ } else if (action.startsWith("\u26A1")) {
1350
+ store.touch(anchor.id);
1351
+ ctx.ui.notify(`\u2316 Woke up #${anchor.id}: "${anchor.title}"`, "info");
1352
+ updateAnchorStatusBar(ctx, store);
1231
1353
  } else if (action.startsWith("\u2139")) {
1232
1354
  const details = [
1233
- `#${anchor.id} [${anchor.priority.toUpperCase()}] ${anchor.title}`,
1355
+ `#${anchor.id} [${anchor.priority.toUpperCase()} \xB7 ${anchor.status.toUpperCase()}] ${anchor.title}`,
1234
1356
  anchor.description ? `Description: ${anchor.description}` : null,
1235
1357
  anchor.targetDate ? `Target Date: ${anchor.targetDate} (${formatTargetDate(anchor)})` : null,
1236
1358
  anchor.files && anchor.files.length > 0 ? `Files: ${anchor.files.join(", ")}` : null,
@@ -1549,6 +1671,7 @@ var AnchorProtocol = class {
1549
1671
  detectRecurrence,
1550
1672
  detectTargetDate,
1551
1673
  evaluateAnchorDecay,
1674
+ extractPathsFromText,
1552
1675
  findMatchedAnchors,
1553
1676
  formatCreationTime,
1554
1677
  formatOrigin,
@@ -1564,6 +1687,7 @@ var AnchorProtocol = class {
1564
1687
  globToRegExp,
1565
1688
  groupAnchorsByQuadrant,
1566
1689
  hasActiveUI,
1690
+ isAnchorTargetingWorkspace,
1567
1691
  isSameOrSubPath,
1568
1692
  makeSafeTaskAnnotation,
1569
1693
  matchAnchorAgainstTouchedFiles,
@@ -1573,9 +1697,11 @@ var AnchorProtocol = class {
1573
1697
  renderActiveAnchorsContext,
1574
1698
  renderColdStartAnchorsContext,
1575
1699
  runPhysicalVerification,
1700
+ sortAnchorsForDisplay,
1576
1701
  stripAnsi,
1577
1702
  sweepStore,
1578
1703
  truncateToWidth,
1704
+ tryRealtimeAutoSettle,
1579
1705
  updateAnchorStatusBar,
1580
1706
  updateStartupBanner
1581
1707
  });