pi-anchor 0.2.0 → 0.2.2

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.
@@ -59,20 +59,34 @@ var DURABLE_DECAY_POLICY = {
59
59
  // src/matcher.ts
60
60
  var import_node_path = __toESM(require("path"), 1);
61
61
  function globToRegExp(pattern) {
62
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\/\*\*\//g, "___GLOB_DIR_SLASH___").replace(/\*\*/g, "___GLOB_STAR_STAR___").replace(/\*/g, "[^/]*").replace(/___GLOB_DIR_SLASH___/g, "(?:/|/.+/)").replace(/___GLOB_STAR_STAR___/g, ".*");
62
+ const collapsed = pattern.slice(0, 512).replace(/\/+(\*\*\/+)+/g, "/**/").replace(/\*{3,}/g, "**");
63
+ 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, ".*");
63
64
  return new RegExp(`^${escaped}$`);
64
65
  }
65
66
  function normalizePath(filePath, baseCwd) {
66
67
  if (typeof filePath !== "string") return "";
67
68
  let norm = filePath.trim().replace(/\\/g, "/").replace(/^\.\//, "");
68
- if (baseCwd && (import_node_path.default.isAbsolute(filePath) || norm.startsWith("/"))) {
69
- const normBase = baseCwd.trim().replace(/\\/g, "/");
70
- if (norm.toLowerCase().startsWith(normBase.toLowerCase())) {
71
- norm = norm.slice(normBase.length).replace(/^\/+/, "");
69
+ if (baseCwd && (import_node_path.default.isAbsolute(filePath) || norm.startsWith("/") || /^[a-zA-Z]:\//.test(norm))) {
70
+ const normBase = baseCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
71
+ const lowerNorm = norm.toLowerCase();
72
+ const lowerBase = normBase.toLowerCase();
73
+ if (lowerNorm === lowerBase) {
74
+ norm = "";
75
+ } else if (lowerNorm.startsWith(lowerBase + "/")) {
76
+ norm = norm.slice(normBase.length + 1).replace(/^\/+/, "");
72
77
  }
73
78
  }
74
79
  return norm;
75
80
  }
81
+ function isSameOrSubPath(anchorCwd, targetCwd) {
82
+ if (!anchorCwd || !targetCwd) return false;
83
+ const normA = anchorCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
84
+ const normT = targetCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
85
+ const isWinStyle = process.platform === "win32" || /^[a-zA-Z]:\//.test(normA) || /^[a-zA-Z]:\//.test(normT);
86
+ const cmpA = isWinStyle ? normA.toLowerCase() : normA;
87
+ const cmpT = isWinStyle ? normT.toLowerCase() : normT;
88
+ return cmpA === cmpT || cmpT.startsWith(cmpA + "/") || cmpA.startsWith(cmpT + "/");
89
+ }
76
90
  function matchAnchorAgainstTouchedFiles(anchor, touchedFiles) {
77
91
  if (!anchor.files || anchor.files.length === 0) return null;
78
92
  const normTouched = touchedFiles.map((f) => normalizePath(f, anchor.cwd)).filter(Boolean);
@@ -434,9 +448,19 @@ var AnchorStore = class {
434
448
  }
435
449
  try {
436
450
  const data = JSON.parse(raw);
437
- if (!Array.isArray(data.anchors)) {
451
+ if (!data || typeof data !== "object" || !Array.isArray(data.anchors)) {
438
452
  throw new Error("Invalid state: anchors must be array");
439
453
  }
454
+ data.anchors = data.anchors.filter((a) => a && typeof a === "object" && typeof a.id === "string" && typeof a.title === "string").map((a) => ({
455
+ ...a,
456
+ files: Array.isArray(a.files) ? a.files.filter((f) => typeof f === "string") : [],
457
+ tags: Array.isArray(a.tags) ? a.tags.filter((t) => typeof t === "string") : [],
458
+ decay: {
459
+ activeDays: Number.isFinite(a.decay?.activeDays) && a.decay.activeDays > 0 ? a.decay.activeDays : DURABLE_DECAY_POLICY.activeDays,
460
+ sleepDays: Number.isFinite(a.decay?.sleepDays) && a.decay.sleepDays > 0 ? a.decay.sleepDays : DURABLE_DECAY_POLICY.sleepDays,
461
+ graveyardDays: Number.isFinite(a.decay?.graveyardDays) && a.decay.graveyardDays > 0 ? a.decay.graveyardDays : DURABLE_DECAY_POLICY.graveyardDays
462
+ }
463
+ }));
440
464
  return data;
441
465
  } catch {
442
466
  const backupPath = import_node_path2.default.join(this.storageDir, `state.corrupt.${Date.now()}.json`);
@@ -456,16 +480,19 @@ var AnchorStore = class {
456
480
  */
457
481
  saveState(state) {
458
482
  this.ensureDirs();
459
- const tempPath = import_node_path2.default.join(this.storageDir, `state.tmp.${process.pid}.${Date.now()}`);
483
+ const rand = Math.random().toString(36).slice(2, 7);
484
+ const tempPath = import_node_path2.default.join(this.storageDir, `state.tmp.${process.pid}.${Date.now()}.${rand}`);
460
485
  import_node_fs.default.writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf-8");
461
486
  atomicRenameWithRetry(tempPath, this.statePath);
462
487
  }
463
- generateId(existingAnchors) {
464
- const numbers = existingAnchors.map((a) => {
488
+ generateId(state) {
489
+ const numbers = state.anchors.map((a) => {
465
490
  const match = a.id.match(/^anc-(\d+)$/);
466
491
  return match ? parseInt(match[1], 10) : 0;
467
492
  }).filter((n) => n > 0);
468
- const nextNum = numbers.length > 0 ? Math.max(...numbers) + 1 : 1;
493
+ const maxExisting = numbers.length > 0 ? Math.max(...numbers) : 0;
494
+ const nextNum = Math.max(maxExisting + 1, state.nextId || 1);
495
+ state.nextId = nextNum + 1;
469
496
  return `anc-${nextNum}`;
470
497
  }
471
498
  /**
@@ -475,7 +502,7 @@ var AnchorStore = class {
475
502
  return this.withLock(() => {
476
503
  const state = this.loadState();
477
504
  const now = Date.now();
478
- const id = this.generateId(state.anchors);
505
+ const id = this.generateId(state);
479
506
  const cwd = input.cwd ? normalizePath(input.cwd) : "";
480
507
  const projectName = input.project ? input.project.trim() : cwd ? import_node_path2.default.basename(cwd) : "global";
481
508
  const recurrence = input.recurrence || detectRecurrence(input.title);
@@ -535,7 +562,7 @@ var AnchorStore = class {
535
562
  if (filter?.status && a.status !== filter.status) return false;
536
563
  if (filter?.priority && a.priority !== filter.priority) return false;
537
564
  if (!filter?.all && targetCwd) {
538
- if (a.files.length > 0 && a.cwd && a.cwd !== targetCwd) {
565
+ if (a.files.length > 0 && a.cwd && !isSameOrSubPath(a.cwd, targetCwd)) {
539
566
  return false;
540
567
  }
541
568
  }
@@ -636,18 +663,46 @@ var AnchorStore = class {
636
663
  throw new Error("No archived anchors to undo");
637
664
  }
638
665
  const raw = import_node_fs.default.readFileSync(this.archivePath, "utf-8");
639
- const lines = raw.split("\n").filter(Boolean);
640
- if (lines.length === 0) {
666
+ const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
667
+ let anchor = null;
668
+ while (lines.length > 0 && !anchor) {
669
+ const candidate = lines.pop();
670
+ try {
671
+ const parsed = JSON.parse(candidate);
672
+ if (parsed && typeof parsed === "object" && typeof parsed.id === "string" && typeof parsed.title === "string") {
673
+ anchor = parsed;
674
+ }
675
+ } catch {
676
+ }
677
+ }
678
+ if (!anchor) {
641
679
  throw new Error("Archive is empty, nothing to undo");
642
680
  }
643
- const lastLine = lines.pop();
644
- const anchor = JSON.parse(lastLine);
645
681
  import_node_fs.default.writeFileSync(this.archivePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
646
682
  anchor.status = "active";
647
683
  anchor.updatedAt = Date.now();
648
684
  delete anchor.evidence;
685
+ delete anchor.settledForDate;
686
+ if (anchor.recurrence === "daily") {
687
+ delete anchor.lastCompletedDate;
688
+ }
649
689
  const state = this.loadState();
650
- state.anchors.push(anchor);
690
+ const existingIdx = state.anchors.findIndex((a) => a.id === anchor.id);
691
+ if (existingIdx !== -1) {
692
+ if (state.anchors[existingIdx].title === anchor.title) {
693
+ state.anchors[existingIdx] = anchor;
694
+ } else {
695
+ anchor.id = this.generateId(state);
696
+ state.anchors.push(anchor);
697
+ }
698
+ } else {
699
+ const match = anchor.id.match(/^anc-(\d+)$/);
700
+ if (match) {
701
+ const restoredNum = parseInt(match[1], 10);
702
+ state.nextId = Math.max(state.nextId || 1, restoredNum + 1);
703
+ }
704
+ state.anchors.push(anchor);
705
+ }
651
706
  this.saveState(state);
652
707
  return anchor;
653
708
  });
@@ -679,10 +734,19 @@ var AnchorStore = class {
679
734
  if (!import_node_fs.default.existsSync(this.archivePath)) return [];
680
735
  try {
681
736
  const raw = import_node_fs.default.readFileSync(this.archivePath, "utf-8");
682
- const all = raw.split("\n").filter(Boolean).map((l) => JSON.parse(l));
737
+ const all = [];
738
+ for (const l of raw.split(/\r?\n/)) {
739
+ const trimmed = l.trim();
740
+ if (!trimmed) continue;
741
+ try {
742
+ const parsed = JSON.parse(trimmed);
743
+ if (parsed && typeof parsed === "object" && typeof parsed.id === "string") all.push(parsed);
744
+ } catch {
745
+ }
746
+ }
683
747
  if (filter?.all || !filter?.cwd) return all;
684
748
  const targetCwd = normalizePath(filter.cwd);
685
- return all.filter((a) => !a.cwd || a.cwd === targetCwd);
749
+ return all.filter((a) => !a.cwd || isSameOrSubPath(a.cwd, targetCwd));
686
750
  } catch {
687
751
  return [];
688
752
  }
@@ -691,10 +755,19 @@ var AnchorStore = class {
691
755
  if (!import_node_fs.default.existsSync(this.graveyardPath)) return [];
692
756
  try {
693
757
  const raw = import_node_fs.default.readFileSync(this.graveyardPath, "utf-8");
694
- const all = raw.split("\n").filter(Boolean).map((l) => JSON.parse(l));
758
+ const all = [];
759
+ for (const l of raw.split(/\r?\n/)) {
760
+ const trimmed = l.trim();
761
+ if (!trimmed) continue;
762
+ try {
763
+ const parsed = JSON.parse(trimmed);
764
+ if (parsed && typeof parsed === "object" && typeof parsed.id === "string") all.push(parsed);
765
+ } catch {
766
+ }
767
+ }
695
768
  if (filter?.all || !filter?.cwd) return all;
696
769
  const targetCwd = normalizePath(filter.cwd);
697
- return all.filter((a) => !a.cwd || a.cwd === targetCwd);
770
+ return all.filter((a) => !a.cwd || isSameOrSubPath(a.cwd, targetCwd));
698
771
  } catch {
699
772
  return [];
700
773
  }
@@ -850,56 +923,58 @@ function evaluateAnchorDecay(anchor, now = Date.now()) {
850
923
  };
851
924
  }
852
925
  function sweepStore(store, now = Date.now()) {
853
- const state = store.loadState();
854
- const result = {
855
- transitionedToSleeping: [],
856
- wokenToActive: [],
857
- evictedToGraveyard: []
858
- };
859
- const toEvict = [];
860
- let stateModified = false;
861
- for (const anchor of state.anchors) {
862
- const evalResult = evaluateAnchorDecay(anchor, now);
863
- if (evalResult.nextStatus === "graveyard") {
864
- toEvict.push(anchor);
865
- } else if (evalResult.nextStatus !== anchor.status) {
866
- if (evalResult.nextStatus === "sleeping" && anchor.status === "active") {
867
- anchor.status = "sleeping";
868
- anchor.updatedAt = now;
869
- result.transitionedToSleeping.push(anchor.id);
870
- stateModified = true;
871
- } else if (evalResult.nextStatus === "active" && anchor.status === "sleeping") {
872
- anchor.status = "active";
873
- anchor.updatedAt = now;
874
- result.wokenToActive.push(anchor.id);
875
- stateModified = true;
926
+ return store.withLock(() => {
927
+ const state = store.loadState();
928
+ const result = {
929
+ transitionedToSleeping: [],
930
+ wokenToActive: [],
931
+ evictedToGraveyard: []
932
+ };
933
+ const toEvict = [];
934
+ let stateModified = false;
935
+ for (const anchor of state.anchors) {
936
+ const evalResult = evaluateAnchorDecay(anchor, now);
937
+ if (evalResult.nextStatus === "graveyard") {
938
+ toEvict.push(anchor);
939
+ } else if (evalResult.nextStatus !== anchor.status) {
940
+ if (evalResult.nextStatus === "sleeping" && anchor.status === "active") {
941
+ anchor.status = "sleeping";
942
+ anchor.updatedAt = now;
943
+ result.transitionedToSleeping.push(anchor.id);
944
+ stateModified = true;
945
+ } else if (evalResult.nextStatus === "active" && anchor.status === "sleeping") {
946
+ anchor.status = "active";
947
+ anchor.updatedAt = now;
948
+ result.wokenToActive.push(anchor.id);
949
+ stateModified = true;
950
+ }
876
951
  }
877
952
  }
878
- }
879
- if (toEvict.length > 0) {
880
- const evictIds = new Set(toEvict.map((e) => e.id));
881
- state.anchors = state.anchors.filter((a) => !evictIds.has(a.id));
882
- stateModified = true;
883
- const graveyardLines = toEvict.map((exp) => {
884
- const rec = {
885
- ...exp,
886
- status: "graveyard",
887
- updatedAt: now,
888
- evictedAt: now,
889
- evictionReason: `Exceeded decay threshold (${exp.decay.graveyardDays} days untouched)`
890
- };
891
- return JSON.stringify(rec);
892
- }).join("\n") + "\n";
893
- import_node_fs2.default.appendFileSync(store.graveyardPath, graveyardLines, "utf-8");
894
- for (const exp of toEvict) {
895
- result.evictedToGraveyard.push(exp.id);
953
+ if (toEvict.length > 0) {
954
+ const evictIds = new Set(toEvict.map((e) => e.id));
955
+ state.anchors = state.anchors.filter((a) => !evictIds.has(a.id));
956
+ stateModified = true;
957
+ const graveyardLines = toEvict.map((exp) => {
958
+ const rec = {
959
+ ...exp,
960
+ status: "graveyard",
961
+ updatedAt: now,
962
+ evictedAt: now,
963
+ evictionReason: `Exceeded decay threshold (${exp.decay.graveyardDays} days untouched)`
964
+ };
965
+ return JSON.stringify(rec);
966
+ }).join("\n") + "\n";
967
+ import_node_fs2.default.appendFileSync(store.graveyardPath, graveyardLines, "utf-8");
968
+ for (const exp of toEvict) {
969
+ result.evictedToGraveyard.push(exp.id);
970
+ }
896
971
  }
897
- }
898
- state.lastSweepAt = now;
899
- if (stateModified) {
900
- store.saveState(state);
901
- }
902
- return result;
972
+ state.lastSweepAt = now;
973
+ if (stateModified) {
974
+ store.saveState(state);
975
+ }
976
+ return result;
977
+ });
903
978
  }
904
979
 
905
980
  // src/context_injector.ts
@@ -1023,14 +1098,16 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg, preflightFailure
1023
1098
  // src/settlement.ts
1024
1099
  var import_node_child_process = require("child_process");
1025
1100
  function runPhysicalVerification(anchor, cwd) {
1026
- if (!anchor.verifyCommand) {
1101
+ if (!anchor.verifyCommand?.trim()) {
1027
1102
  return { success: false, output: "No verification command specified" };
1028
1103
  }
1104
+ const targetCwd = anchor.cwd || cwd;
1029
1105
  try {
1030
1106
  const stdout = (0, import_node_child_process.execSync)(anchor.verifyCommand, {
1031
- cwd,
1107
+ cwd: targetCwd,
1032
1108
  encoding: "utf-8",
1033
- timeout: 3e4,
1109
+ timeout: 8e3,
1110
+ maxBuffer: 4 * 1024 * 1024,
1034
1111
  stdio: ["ignore", "pipe", "pipe"]
1035
1112
  });
1036
1113
  return { success: true, output: stdout.trim() };
@@ -1086,8 +1163,9 @@ function groupAnchorsByQuadrant(anchors, now = Date.now()) {
1086
1163
  }
1087
1164
  return groups;
1088
1165
  }
1166
+ var ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
1089
1167
  function stripAnsi(str) {
1090
- return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
1168
+ return str.replace(ANSI_REGEX, "");
1091
1169
  }
1092
1170
  function getDisplayWidth(str) {
1093
1171
  const clean = stripAnsi(str);
@@ -1105,23 +1183,46 @@ function getDisplayWidth(str) {
1105
1183
  }
1106
1184
  return width;
1107
1185
  }
1186
+ function hasActiveUI(ctx) {
1187
+ if (!ctx) return false;
1188
+ try {
1189
+ return Boolean(ctx.hasUI && ctx.ui);
1190
+ } catch {
1191
+ return false;
1192
+ }
1193
+ }
1108
1194
  function truncateToWidth(str, maxWidth, ellipsis = "\u2026") {
1109
- const current = getDisplayWidth(str);
1110
- if (current <= maxWidth) return str;
1195
+ if (maxWidth <= 0) return "";
1196
+ const singleLine = str.replace(/[\r\n\t]+/g, " ");
1197
+ const totalWidth = getDisplayWidth(singleLine);
1198
+ if (totalWidth <= maxWidth) return singleLine;
1111
1199
  const ellipsisWidth = getDisplayWidth(ellipsis);
1112
- const target = maxWidth - ellipsisWidth;
1113
- if (target <= 0) return ellipsis.slice(0, maxWidth);
1200
+ const target = Math.max(0, maxWidth - ellipsisWidth);
1201
+ if (target === 0) return ellipsis.slice(0, maxWidth);
1114
1202
  let accumulated = "";
1115
1203
  let accumWidth = 0;
1116
- for (const char of str) {
1117
- const charWidth = getDisplayWidth(char);
1204
+ let i = 0;
1205
+ while (i < singleLine.length) {
1206
+ ANSI_REGEX.lastIndex = i;
1207
+ const match = ANSI_REGEX.exec(singleLine);
1208
+ if (match && match.index === i) {
1209
+ accumulated += match[0];
1210
+ i += match[0].length;
1211
+ continue;
1212
+ }
1213
+ const codePoint = singleLine.codePointAt(i) || 0;
1214
+ const charLen = codePoint > 65535 ? 2 : 1;
1215
+ const fullChar = singleLine.slice(i, i + charLen);
1216
+ const charWidth = getDisplayWidth(fullChar);
1118
1217
  if (accumWidth + charWidth > target) {
1119
1218
  break;
1120
1219
  }
1121
- accumulated += char;
1220
+ accumulated += fullChar;
1122
1221
  accumWidth += charWidth;
1222
+ i += charLen;
1123
1223
  }
1124
- return accumulated + ellipsis;
1224
+ accumulated = accumulated.replace(/\s*[\(\[\{(【]$/, "");
1225
+ return accumulated + ellipsis + (singleLine.includes("\x1B") ? "\x1B[0m" : "");
1125
1226
  }
1126
1227
  function padToWidth(str, targetWidth) {
1127
1228
  const truncated = truncateToWidth(str, targetWidth);
@@ -1191,52 +1292,58 @@ function formatOrigin(anchor) {
1191
1292
  return import_node_path4.default.basename(anchor) || "global";
1192
1293
  }
1193
1294
  function updateAnchorStatusBar(ctx, store) {
1194
- if (!ctx.hasUI || !ctx.ui) return;
1195
- const active = store.list({ status: "active", cwd: ctx.cwd });
1196
- if (active.length === 0) {
1197
- ctx.ui.setStatus("anchor", void 0);
1198
- return;
1295
+ if (!hasActiveUI(ctx)) return;
1296
+ try {
1297
+ const active = store.list({ status: "active", cwd: ctx.cwd });
1298
+ if (active.length === 0) {
1299
+ ctx.ui.setStatus("anchor", void 0);
1300
+ return;
1301
+ }
1302
+ ctx.ui.setStatus("anchor", `\u2316 ${active.length}`);
1303
+ } catch {
1199
1304
  }
1200
- ctx.ui.setStatus("anchor", `\u2316 ${active.length}`);
1201
1305
  }
1202
1306
  function updateStartupBanner(ctx, store) {
1203
- if (!ctx.hasUI || !ctx.ui) return;
1204
- const active = store.list({ status: "active", cwd: ctx.cwd });
1205
- if (active.length === 0) {
1206
- ctx.ui.setWidget("anchor-startup", void 0);
1207
- return;
1208
- }
1209
- const groups = groupAnchorsByQuadrant(active);
1210
- const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
1211
- const topItems = sorted.slice(0, 3);
1212
- const factory = (_tui, theme) => ({
1213
- render: (width) => {
1214
- const maxTitleWidth = Math.max(16, width - 36);
1215
- const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
1216
- const lines = [
1217
- fg("accent", `\u2316 Anchor \xB7 ${active.length} active commitments:`)
1218
- ];
1219
- for (let i = 0; i < topItems.length; i++) {
1220
- const a = topItems[i];
1221
- const num = (i + 1).toString().padStart(2, "0");
1222
- const q = classifyAnchor(a);
1223
- const tgt = formatTargetDate(a);
1224
- const titlePadded = truncateToWidth(a.title, maxTitleWidth);
1225
- lines.push(` \u2022 ${fg("muted", `[${q}]`)} ${fg("dim", num)} ${titlePadded} ${fg("dim", `(${tgt})`)}`);
1226
- }
1227
- if (sorted.length > 3) {
1228
- lines.push(fg("dim", ` (+${sorted.length - 3} more \xB7 run /anchor to inspect)`));
1229
- }
1230
- return lines;
1231
- },
1232
- invalidate: () => {
1307
+ if (!hasActiveUI(ctx)) return;
1308
+ try {
1309
+ const active = store.list({ status: "active", cwd: ctx.cwd });
1310
+ if (active.length === 0) {
1311
+ ctx.ui.setWidget("anchor-startup", void 0);
1312
+ return;
1233
1313
  }
1234
- });
1235
- const content = ctx.ui?.theme ? factory : factory().render(80);
1236
- ctx.ui.setWidget("anchor-startup", content, { placement: "aboveEditor" });
1314
+ const groups = groupAnchorsByQuadrant(active);
1315
+ const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
1316
+ const topItems = sorted.slice(0, 3);
1317
+ const factory = (_tui, theme) => ({
1318
+ render: (width) => {
1319
+ const maxTitleWidth = Math.max(12, width - 36);
1320
+ const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
1321
+ const lines = [
1322
+ truncateToWidth(fg("accent", `\u2316 Anchor \xB7 ${active.length} active commitments:`), width)
1323
+ ];
1324
+ for (let i = 0; i < topItems.length; i++) {
1325
+ const a = topItems[i];
1326
+ const num = a.id.replace(/^anc-/, "").padStart(2, "0");
1327
+ const q = classifyAnchor(a);
1328
+ const tgt = formatTargetDate(a);
1329
+ const titlePadded = truncateToWidth(a.title, maxTitleWidth);
1330
+ lines.push(truncateToWidth(` \u2022 ${fg("muted", `[${q}]`)} ${fg("dim", num)} ${titlePadded} ${fg("dim", `(${tgt})`)}`, width));
1331
+ }
1332
+ if (sorted.length > 3) {
1333
+ lines.push(truncateToWidth(fg("dim", ` (+${sorted.length - 3} more \xB7 run /anchor to inspect)`), width));
1334
+ }
1335
+ return lines;
1336
+ },
1337
+ invalidate: () => {
1338
+ }
1339
+ });
1340
+ const content = ctx.ui?.theme ? factory : factory().render(80);
1341
+ ctx.ui.setWidget("anchor-startup", content, { placement: "aboveEditor" });
1342
+ } catch {
1343
+ }
1237
1344
  }
1238
1345
  async function openAnchorDashboard(ctx, store) {
1239
- if (!ctx.hasUI || !ctx.ui) return;
1346
+ if (!hasActiveUI(ctx)) return;
1240
1347
  const list = store.list({ status: "active", cwd: ctx.cwd });
1241
1348
  if (list.length === 0) {
1242
1349
  ctx.ui.notify("\u2316 No active anchors. Use /pin <task> to record.", "info");
@@ -1251,31 +1358,55 @@ async function openAnchorDashboard(ctx, store) {
1251
1358
  ];
1252
1359
  const optionMap = /* @__PURE__ */ new Map();
1253
1360
  const displayOptions = [];
1361
+ const termCols = process.stdout?.columns || 96;
1362
+ const isNarrow = termCols < 88;
1363
+ const titleWidth = isNarrow ? Math.max(18, termCols - 36) : 34;
1254
1364
  for (let i = 0; i < sortedList.length; i++) {
1255
1365
  const a = sortedList[i];
1256
- const num = (i + 1).toString().padStart(2, "0");
1366
+ const num = a.id.replace(/^anc-/, "").padStart(2, "0");
1257
1367
  const category = padToWidth(`[${classifyAnchor(a)}]`, 12);
1258
1368
  const origin = formatOrigin(a);
1259
1369
  const target = formatTargetDate(a);
1260
1370
  const created = formatCreationTime(a.createdAt);
1261
1371
  const titleWithFiles = a.files && a.files.length > 0 ? `${a.title} [${a.files.slice(0, 1).join(", ")}]` : a.title;
1262
1372
  const colNum = `${num} `;
1263
- const colTitle = padToWidth(titleWithFiles, 34);
1373
+ const colTitle = padToWidth(titleWithFiles, titleWidth);
1264
1374
  const colOrigin = padToWidth(origin, 10);
1265
1375
  const colTarget = padToWidth(target, 12);
1266
1376
  const colCreated = created;
1267
- const label = `${colNum}${category}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
1377
+ const label = isNarrow ? `${colNum}${category}${colTitle} ${colTarget}`.trimEnd() : `${colNum}${category}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
1268
1378
  optionMap.set(label, a);
1269
1379
  displayOptions.push(label);
1270
1380
  }
1271
- const selected = await ctx.ui.select("\u2316 Anchors (enter to complete):", displayOptions);
1381
+ const selected = await ctx.ui.select("\u2316 Anchors (select to inspect or settle):", displayOptions);
1272
1382
  if (!selected) return;
1273
1383
  const anchor = optionMap.get(selected);
1274
1384
  if (!anchor) return;
1275
- store.settle(anchor.id, { settledBy: "manual-command" });
1276
- const successMsg = anchor.recurrence === "daily" ? `\u2316 Completed for today: "${anchor.title}" (resets tomorrow)` : `\u2316 Settled: "${anchor.title}"`;
1277
- ctx.ui.notify(successMsg, "info");
1278
- updateAnchorStatusBar(ctx, store);
1385
+ const action = await ctx.ui.select(
1386
+ `\u2316 Anchor #${anchor.id}: "${anchor.title}"`,
1387
+ [
1388
+ "\u2713 Settle (Mark completed and archive)",
1389
+ "\u2139 View details & associated files",
1390
+ "\u21A9 Cancel"
1391
+ ]
1392
+ );
1393
+ if (!action || action.startsWith("\u21A9")) return;
1394
+ if (action.startsWith("\u2713")) {
1395
+ store.settle(anchor.id, { settledBy: "manual-command" });
1396
+ const successMsg = anchor.recurrence === "daily" ? `\u2316 Completed for today: "${anchor.title}" (resets tomorrow)` : `\u2316 Settled #${anchor.id}: "${anchor.title}" (/anchor undo to revert)`;
1397
+ ctx.ui.notify(successMsg, "info");
1398
+ updateAnchorStatusBar(ctx, store);
1399
+ } else if (action.startsWith("\u2139")) {
1400
+ const details = [
1401
+ `#${anchor.id} [${anchor.priority.toUpperCase()}] ${anchor.title}`,
1402
+ anchor.description ? `Description: ${anchor.description}` : null,
1403
+ anchor.targetDate ? `Target Date: ${anchor.targetDate} (${formatTargetDate(anchor)})` : null,
1404
+ anchor.files && anchor.files.length > 0 ? `Files: ${anchor.files.join(", ")}` : null,
1405
+ anchor.verifyCommand ? `Verify Command: ${anchor.verifyCommand}` : null,
1406
+ `Created: ${formatCreationTime(anchor.createdAt)} (in ${anchor.project || "global"})`
1407
+ ].filter(Boolean).join("\n");
1408
+ ctx.ui.notify(details, "info");
1409
+ }
1279
1410
  }
1280
1411
 
1281
1412
  // src/extension.ts
@@ -1298,7 +1429,7 @@ function extension_default(pi) {
1298
1429
  }
1299
1430
  const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
1300
1431
  for (const a of activeAnchors) {
1301
- if (a.verifyCommand) {
1432
+ if (a.verifyCommand && (!a.cwd || isSameOrSubPath(a.cwd, ctx.cwd))) {
1302
1433
  try {
1303
1434
  const res = runPhysicalVerification(a, ctx.cwd);
1304
1435
  if (res.success) {
@@ -1319,21 +1450,32 @@ function extension_default(pi) {
1319
1450
  const pendingPath = import_node_path5.default.join(home, ".anchor", "pending_exit.json");
1320
1451
  if (import_node_fs3.default.existsSync(pendingPath)) {
1321
1452
  const pending = JSON.parse(import_node_fs3.default.readFileSync(pendingPath, "utf-8"));
1322
- if (pending.cwd === ctx.cwd && Date.now() - pending.timestamp < 24 * 60 * 60 * 1e3) {
1323
- ctx.ui.notify(`\u2316 [Anchor \u9000\u51FA\u5B88\u62A4] \u68C0\u6D4B\u5230\u4E0A\u6B21\u5F02\u5E38\u9000\u51FA\u4E14\u6709 ${pending.modifiedCount} \u4E2A\u672A\u63D0\u4EA4\u6539\u52A8\uFF0C\u53EF\u7528 anchor(action="pin") \u56FA\u5316\u4EFB\u52A1\u3002`, "info");
1453
+ const isExpired = Date.now() - pending.timestamp >= 24 * 60 * 60 * 1e3;
1454
+ if (!isExpired && isSameOrSubPath(pending.cwd, ctx.cwd)) {
1455
+ ctx.ui.notify(`\u2316 [Exit Guard] Detected ${pending.modifiedCount} uncommitted changes from previous session exit. Run /pin to record.`, "info");
1456
+ import_node_fs3.default.unlinkSync(pendingPath);
1457
+ } else if (isExpired) {
1458
+ import_node_fs3.default.unlinkSync(pendingPath);
1324
1459
  }
1325
- import_node_fs3.default.unlinkSync(pendingPath);
1326
1460
  }
1327
1461
  } catch (_) {
1328
1462
  }
1329
1463
  updateAnchorStatusBar(ctx, store);
1330
1464
  if (event.reason !== "resume") {
1331
1465
  updateStartupBanner(ctx, store);
1466
+ } else if (hasActiveUI(ctx)) {
1467
+ try {
1468
+ ctx.ui.setWidget("anchor-startup", void 0);
1469
+ } catch {
1470
+ }
1332
1471
  }
1333
1472
  });
1334
1473
  pi.on("agent_start", async (_event, ctx) => {
1335
- if (ctx.hasUI && ctx.ui) {
1336
- ctx.ui.setWidget("anchor-startup", void 0);
1474
+ if (hasActiveUI(ctx)) {
1475
+ try {
1476
+ ctx.ui.setWidget("anchor-startup", void 0);
1477
+ } catch {
1478
+ }
1337
1479
  }
1338
1480
  });
1339
1481
  pi.on("before_agent_start", async (event, ctx) => {
@@ -1357,7 +1499,7 @@ ${contextSnippet}`
1357
1499
  }
1358
1500
  const pathInput = event.input?.path;
1359
1501
  if (typeof pathInput !== "string") return;
1360
- const touchedPath = normalizePath(pathInput);
1502
+ const touchedPath = normalizePath(pathInput, ctx.cwd);
1361
1503
  const anchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
1362
1504
  const matches = findMatchedAnchors(anchors, [touchedPath]);
1363
1505
  if (matches.length > 0) {
@@ -1385,20 +1527,37 @@ ${contextSnippet}`
1385
1527
  }
1386
1528
  }
1387
1529
  });
1388
- pi.on("session_shutdown", async (_event, ctx) => {
1530
+ pi.on("session_shutdown", async (event, ctx) => {
1531
+ let gitStatus = "";
1389
1532
  try {
1390
- const gitStatus = (0, import_node_child_process2.execSync)("git status --porcelain", {
1533
+ gitStatus = (0, import_node_child_process2.execSync)("git status --porcelain", {
1391
1534
  cwd: ctx.cwd,
1392
1535
  encoding: "utf-8",
1393
- timeout: 3e3,
1536
+ timeout: 2e3,
1394
1537
  stdio: ["ignore", "pipe", "ignore"]
1395
- });
1396
- const changed = gitStatus.split("\n").map((l) => l.slice(3).trim()).filter(Boolean);
1538
+ }).replace(/[\r\n]+$/, "");
1539
+ const changed = gitStatus.split(/\r?\n/).filter(Boolean).map((l) => l.slice(3).replace(/^"(.*)"$/, "$1").trim()).filter(Boolean);
1397
1540
  for (const f of changed) {
1398
1541
  observer.addTouchedFile(f);
1399
1542
  }
1400
1543
  } catch {
1401
1544
  }
1545
+ if (event?.reason === "quit" && gitStatus) {
1546
+ try {
1547
+ const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
1548
+ if (activeAnchors.length === 0) {
1549
+ const home = process.env.HOME || process.env.USERPROFILE || "";
1550
+ const pendingPath = import_node_path5.default.join(home, ".anchor", "pending_exit.json");
1551
+ const data = {
1552
+ cwd: ctx.cwd,
1553
+ timestamp: Date.now(),
1554
+ modifiedCount: gitStatus.split("\n").filter(Boolean).length
1555
+ };
1556
+ import_node_fs3.default.writeFileSync(pendingPath, JSON.stringify(data, null, 2), "utf-8");
1557
+ }
1558
+ } catch {
1559
+ }
1560
+ }
1402
1561
  const modified = observer.getModifiedFiles();
1403
1562
  if (modified.length === 0) return;
1404
1563
  const proposals = generateSettlementProposals(store, modified, ctx.cwd);
@@ -1412,7 +1571,12 @@ ${contextSnippet}`
1412
1571
  summary: `Auto-settled via Git commit: ${commitMatch.message}`,
1413
1572
  touchedFiles: prop.matchedFiles
1414
1573
  });
1415
- ctx.ui.notify(`\u2316 Auto-settled #${a.id} via Git commit (run /anchor undo to revert)`, "info");
1574
+ if (hasActiveUI(ctx)) {
1575
+ try {
1576
+ ctx.ui.notify(`\u2316 Auto-settled #${a.id} via Git commit (run /anchor undo to revert)`, "info");
1577
+ } catch {
1578
+ }
1579
+ }
1416
1580
  continue;
1417
1581
  }
1418
1582
  if (a.verifyCommand) {
@@ -1423,22 +1587,30 @@ ${contextSnippet}`
1423
1587
  summary: `Automated test passed: ${a.verifyCommand}`,
1424
1588
  touchedFiles: prop.matchedFiles
1425
1589
  });
1426
- ctx.ui.notify(`\u2316 Test passed (${a.verifyCommand}), auto-settled #${a.id}`, "info");
1590
+ if (hasActiveUI(ctx)) {
1591
+ try {
1592
+ ctx.ui.notify(`\u2316 Test passed (${a.verifyCommand}), auto-settled #${a.id}`, "info");
1593
+ } catch {
1594
+ }
1595
+ }
1427
1596
  continue;
1428
1597
  }
1429
1598
  }
1430
- if (!ctx.hasUI || !ctx.ui) continue;
1431
- const ok = await ctx.ui.confirm(
1432
- "\u2316 Settle Anchor Task",
1433
- `Task #${a.id} [${a.title}] touched files (${prop.matchedFiles.slice(0, 2).join(", ")}).
1599
+ if (!hasActiveUI(ctx)) continue;
1600
+ try {
1601
+ const ok = await ctx.ui.confirm(
1602
+ "\u2316 Settle Anchor Task",
1603
+ `Task #${a.id} [${a.title}] touched files (${prop.matchedFiles.slice(0, 2).join(", ")}).
1434
1604
  Mark as completed and archive?`
1435
- );
1436
- if (ok) {
1437
- store.settle(a.id, {
1438
- settledBy: "one-tap-settlement",
1439
- touchedFiles: prop.matchedFiles
1440
- });
1441
- ctx.ui.notify(`\u2316 Settled #${a.id}: "${a.title}"`, "info");
1605
+ );
1606
+ if (ok) {
1607
+ store.settle(a.id, {
1608
+ settledBy: "one-tap-settlement",
1609
+ touchedFiles: prop.matchedFiles
1610
+ });
1611
+ ctx.ui.notify(`\u2316 Settled #${a.id}: "${a.title}"`, "info");
1612
+ }
1613
+ } catch {
1442
1614
  }
1443
1615
  }
1444
1616
  });
@@ -1488,7 +1660,7 @@ Mark as completed and archive?`
1488
1660
  if (args.priority) title += ` ${theme.fg("muted", `[${args.priority}]`)}`;
1489
1661
  }
1490
1662
  return {
1491
- render: () => [title],
1663
+ render: (width) => [truncateToWidth(title, width)],
1492
1664
  invalidate: () => {
1493
1665
  }
1494
1666
  };
@@ -1497,14 +1669,14 @@ Mark as completed and archive?`
1497
1669
  if (result.isError) {
1498
1670
  const errText = result.content?.[0]?.text || "Execution failed";
1499
1671
  return {
1500
- render: () => [theme.fg("error", `\u2717 ${errText}`)],
1672
+ render: (width) => [truncateToWidth(theme.fg("error", `\u2717 ${errText}`), width)],
1501
1673
  invalidate: () => {
1502
1674
  }
1503
1675
  };
1504
1676
  }
1505
1677
  const text = result.content?.[0]?.text || "OK";
1506
1678
  return {
1507
- render: () => [theme.fg("success", `\u2714 ${text}`)],
1679
+ render: (width) => [truncateToWidth(theme.fg("success", `\u2714 ${text}`), width)],
1508
1680
  invalidate: () => {
1509
1681
  }
1510
1682
  };
@@ -1640,28 +1812,10 @@ Mark as completed and archive?`
1640
1812
  updateAnchorStatusBar(ctx, store);
1641
1813
  }
1642
1814
  });
1643
- pi.on("session_shutdown", async (event, ctx) => {
1644
- if (event.reason === "quit") {
1645
- try {
1646
- const gitStatus = (0, import_node_child_process2.execSync)("git status --porcelain", {
1647
- cwd: ctx.cwd,
1648
- encoding: "utf-8",
1649
- timeout: 2e3,
1650
- stdio: ["ignore", "pipe", "ignore"]
1651
- }).trim();
1652
- const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
1653
- if (gitStatus && activeAnchors.length === 0) {
1654
- const home = process.env.HOME || process.env.USERPROFILE || "";
1655
- const pendingPath = import_node_path5.default.join(home, ".anchor", "pending_exit.json");
1656
- const data = {
1657
- cwd: ctx.cwd,
1658
- timestamp: Date.now(),
1659
- modifiedCount: gitStatus.split("\n").filter(Boolean).length
1660
- };
1661
- import_node_fs3.default.writeFileSync(pendingPath, JSON.stringify(data, null, 2), "utf-8");
1662
- }
1663
- } catch (_) {
1664
- }
1815
+ pi.registerCommand("anchors", {
1816
+ description: "Inspect and settle active anchors",
1817
+ handler: async (_args, ctx) => {
1818
+ await openAnchorDashboard(ctx, store);
1665
1819
  }
1666
1820
  });
1667
1821
  }