pi-anchor 0.2.1 → 0.2.3

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() };
@@ -1106,27 +1183,36 @@ function getDisplayWidth(str) {
1106
1183
  }
1107
1184
  return width;
1108
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
+ }
1109
1194
  function truncateToWidth(str, maxWidth, ellipsis = "\u2026") {
1110
1195
  if (maxWidth <= 0) return "";
1111
- const totalWidth = getDisplayWidth(str);
1112
- if (totalWidth <= maxWidth) return str;
1196
+ const singleLine = str.replace(/[\r\n\t]+/g, " ");
1197
+ const totalWidth = getDisplayWidth(singleLine);
1198
+ if (totalWidth <= maxWidth) return singleLine;
1113
1199
  const ellipsisWidth = getDisplayWidth(ellipsis);
1114
1200
  const target = Math.max(0, maxWidth - ellipsisWidth);
1115
1201
  if (target === 0) return ellipsis.slice(0, maxWidth);
1116
1202
  let accumulated = "";
1117
1203
  let accumWidth = 0;
1118
1204
  let i = 0;
1119
- while (i < str.length) {
1205
+ while (i < singleLine.length) {
1120
1206
  ANSI_REGEX.lastIndex = i;
1121
- const match = ANSI_REGEX.exec(str);
1207
+ const match = ANSI_REGEX.exec(singleLine);
1122
1208
  if (match && match.index === i) {
1123
1209
  accumulated += match[0];
1124
1210
  i += match[0].length;
1125
1211
  continue;
1126
1212
  }
1127
- const codePoint = str.codePointAt(i) || 0;
1213
+ const codePoint = singleLine.codePointAt(i) || 0;
1128
1214
  const charLen = codePoint > 65535 ? 2 : 1;
1129
- const fullChar = str.slice(i, i + charLen);
1215
+ const fullChar = singleLine.slice(i, i + charLen);
1130
1216
  const charWidth = getDisplayWidth(fullChar);
1131
1217
  if (accumWidth + charWidth > target) {
1132
1218
  break;
@@ -1136,7 +1222,7 @@ function truncateToWidth(str, maxWidth, ellipsis = "\u2026") {
1136
1222
  i += charLen;
1137
1223
  }
1138
1224
  accumulated = accumulated.replace(/\s*[\(\[\{(【]$/, "");
1139
- return accumulated + ellipsis + (str.includes("\x1B") ? "\x1B[0m" : "");
1225
+ return accumulated + ellipsis + (singleLine.includes("\x1B") ? "\x1B[0m" : "");
1140
1226
  }
1141
1227
  function padToWidth(str, targetWidth) {
1142
1228
  const truncated = truncateToWidth(str, targetWidth);
@@ -1206,52 +1292,58 @@ function formatOrigin(anchor) {
1206
1292
  return import_node_path4.default.basename(anchor) || "global";
1207
1293
  }
1208
1294
  function updateAnchorStatusBar(ctx, store) {
1209
- if (!ctx.hasUI || !ctx.ui) return;
1210
- const active = store.list({ status: "active", cwd: ctx.cwd });
1211
- if (active.length === 0) {
1212
- ctx.ui.setStatus("anchor", void 0);
1213
- 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 {
1214
1304
  }
1215
- ctx.ui.setStatus("anchor", `\u2316 ${active.length}`);
1216
1305
  }
1217
1306
  function updateStartupBanner(ctx, store) {
1218
- if (!ctx.hasUI || !ctx.ui) return;
1219
- const active = store.list({ status: "active", cwd: ctx.cwd });
1220
- if (active.length === 0) {
1221
- ctx.ui.setWidget("anchor-startup", void 0);
1222
- return;
1223
- }
1224
- const groups = groupAnchorsByQuadrant(active);
1225
- const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
1226
- const topItems = sorted.slice(0, 3);
1227
- const factory = (_tui, theme) => ({
1228
- render: (width) => {
1229
- const maxTitleWidth = Math.max(16, width - 36);
1230
- const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
1231
- const lines = [
1232
- fg("accent", `\u2316 Anchor \xB7 ${active.length} active commitments:`)
1233
- ];
1234
- for (let i = 0; i < topItems.length; i++) {
1235
- const a = topItems[i];
1236
- const num = (i + 1).toString().padStart(2, "0");
1237
- const q = classifyAnchor(a);
1238
- const tgt = formatTargetDate(a);
1239
- const titlePadded = truncateToWidth(a.title, maxTitleWidth);
1240
- lines.push(` \u2022 ${fg("muted", `[${q}]`)} ${fg("dim", num)} ${titlePadded} ${fg("dim", `(${tgt})`)}`);
1241
- }
1242
- if (sorted.length > 3) {
1243
- lines.push(fg("dim", ` (+${sorted.length - 3} more \xB7 run /anchor to inspect)`));
1244
- }
1245
- return lines;
1246
- },
1247
- 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;
1248
1313
  }
1249
- });
1250
- const content = ctx.ui?.theme ? factory : factory().render(80);
1251
- 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
+ }
1252
1344
  }
1253
1345
  async function openAnchorDashboard(ctx, store) {
1254
- if (!ctx.hasUI || !ctx.ui) return;
1346
+ if (!hasActiveUI(ctx)) return;
1255
1347
  const list = store.list({ status: "active", cwd: ctx.cwd });
1256
1348
  if (list.length === 0) {
1257
1349
  ctx.ui.notify("\u2316 No active anchors. Use /pin <task> to record.", "info");
@@ -1266,20 +1358,23 @@ async function openAnchorDashboard(ctx, store) {
1266
1358
  ];
1267
1359
  const optionMap = /* @__PURE__ */ new Map();
1268
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;
1269
1364
  for (let i = 0; i < sortedList.length; i++) {
1270
1365
  const a = sortedList[i];
1271
- const num = (i + 1).toString().padStart(2, "0");
1366
+ const num = a.id.replace(/^anc-/, "").padStart(2, "0");
1272
1367
  const category = padToWidth(`[${classifyAnchor(a)}]`, 12);
1273
1368
  const origin = formatOrigin(a);
1274
1369
  const target = formatTargetDate(a);
1275
1370
  const created = formatCreationTime(a.createdAt);
1276
1371
  const titleWithFiles = a.files && a.files.length > 0 ? `${a.title} [${a.files.slice(0, 1).join(", ")}]` : a.title;
1277
1372
  const colNum = `${num} `;
1278
- const colTitle = padToWidth(titleWithFiles, 34);
1373
+ const colTitle = padToWidth(titleWithFiles, titleWidth);
1279
1374
  const colOrigin = padToWidth(origin, 10);
1280
1375
  const colTarget = padToWidth(target, 12);
1281
1376
  const colCreated = created;
1282
- 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();
1283
1378
  optionMap.set(label, a);
1284
1379
  displayOptions.push(label);
1285
1380
  }
@@ -1334,7 +1429,7 @@ function extension_default(pi) {
1334
1429
  }
1335
1430
  const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
1336
1431
  for (const a of activeAnchors) {
1337
- if (a.verifyCommand) {
1432
+ if (a.verifyCommand && (!a.cwd || isSameOrSubPath(a.cwd, ctx.cwd))) {
1338
1433
  try {
1339
1434
  const res = runPhysicalVerification(a, ctx.cwd);
1340
1435
  if (res.success) {
@@ -1355,21 +1450,32 @@ function extension_default(pi) {
1355
1450
  const pendingPath = import_node_path5.default.join(home, ".anchor", "pending_exit.json");
1356
1451
  if (import_node_fs3.default.existsSync(pendingPath)) {
1357
1452
  const pending = JSON.parse(import_node_fs3.default.readFileSync(pendingPath, "utf-8"));
1358
- if (pending.cwd === ctx.cwd && Date.now() - pending.timestamp < 24 * 60 * 60 * 1e3) {
1359
- 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);
1360
1459
  }
1361
- import_node_fs3.default.unlinkSync(pendingPath);
1362
1460
  }
1363
1461
  } catch (_) {
1364
1462
  }
1365
1463
  updateAnchorStatusBar(ctx, store);
1366
1464
  if (event.reason !== "resume") {
1367
1465
  updateStartupBanner(ctx, store);
1466
+ } else if (hasActiveUI(ctx)) {
1467
+ try {
1468
+ ctx.ui.setWidget("anchor-startup", void 0);
1469
+ } catch {
1470
+ }
1368
1471
  }
1369
1472
  });
1370
1473
  pi.on("agent_start", async (_event, ctx) => {
1371
- if (ctx.hasUI && ctx.ui) {
1372
- ctx.ui.setWidget("anchor-startup", void 0);
1474
+ if (hasActiveUI(ctx)) {
1475
+ try {
1476
+ ctx.ui.setWidget("anchor-startup", void 0);
1477
+ } catch {
1478
+ }
1373
1479
  }
1374
1480
  });
1375
1481
  pi.on("before_agent_start", async (event, ctx) => {
@@ -1393,7 +1499,7 @@ ${contextSnippet}`
1393
1499
  }
1394
1500
  const pathInput = event.input?.path;
1395
1501
  if (typeof pathInput !== "string") return;
1396
- const touchedPath = normalizePath(pathInput);
1502
+ const touchedPath = normalizePath(pathInput, ctx.cwd);
1397
1503
  const anchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
1398
1504
  const matches = findMatchedAnchors(anchors, [touchedPath]);
1399
1505
  if (matches.length > 0) {
@@ -1421,20 +1527,37 @@ ${contextSnippet}`
1421
1527
  }
1422
1528
  }
1423
1529
  });
1424
- pi.on("session_shutdown", async (_event, ctx) => {
1530
+ pi.on("session_shutdown", async (event, ctx) => {
1531
+ let gitStatus = "";
1425
1532
  try {
1426
- const gitStatus = (0, import_node_child_process2.execSync)("git status --porcelain", {
1533
+ gitStatus = (0, import_node_child_process2.execSync)("git status --porcelain", {
1427
1534
  cwd: ctx.cwd,
1428
1535
  encoding: "utf-8",
1429
- timeout: 3e3,
1536
+ timeout: 2e3,
1430
1537
  stdio: ["ignore", "pipe", "ignore"]
1431
- });
1432
- 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);
1433
1540
  for (const f of changed) {
1434
1541
  observer.addTouchedFile(f);
1435
1542
  }
1436
1543
  } catch {
1437
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
+ }
1438
1561
  const modified = observer.getModifiedFiles();
1439
1562
  if (modified.length === 0) return;
1440
1563
  const proposals = generateSettlementProposals(store, modified, ctx.cwd);
@@ -1448,7 +1571,12 @@ ${contextSnippet}`
1448
1571
  summary: `Auto-settled via Git commit: ${commitMatch.message}`,
1449
1572
  touchedFiles: prop.matchedFiles
1450
1573
  });
1451
- 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
+ }
1452
1580
  continue;
1453
1581
  }
1454
1582
  if (a.verifyCommand) {
@@ -1459,22 +1587,30 @@ ${contextSnippet}`
1459
1587
  summary: `Automated test passed: ${a.verifyCommand}`,
1460
1588
  touchedFiles: prop.matchedFiles
1461
1589
  });
1462
- 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
+ }
1463
1596
  continue;
1464
1597
  }
1465
1598
  }
1466
- if (!ctx.hasUI || !ctx.ui) continue;
1467
- const ok = await ctx.ui.confirm(
1468
- "\u2316 Settle Anchor Task",
1469
- `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(", ")}).
1470
1604
  Mark as completed and archive?`
1471
- );
1472
- if (ok) {
1473
- store.settle(a.id, {
1474
- settledBy: "one-tap-settlement",
1475
- touchedFiles: prop.matchedFiles
1476
- });
1477
- 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 {
1478
1614
  }
1479
1615
  }
1480
1616
  });
@@ -1676,28 +1812,10 @@ Mark as completed and archive?`
1676
1812
  updateAnchorStatusBar(ctx, store);
1677
1813
  }
1678
1814
  });
1679
- pi.on("session_shutdown", async (event, ctx) => {
1680
- if (event.reason === "quit") {
1681
- try {
1682
- const gitStatus = (0, import_node_child_process2.execSync)("git status --porcelain", {
1683
- cwd: ctx.cwd,
1684
- encoding: "utf-8",
1685
- timeout: 2e3,
1686
- stdio: ["ignore", "pipe", "ignore"]
1687
- }).trim();
1688
- const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
1689
- if (gitStatus && activeAnchors.length === 0) {
1690
- const home = process.env.HOME || process.env.USERPROFILE || "";
1691
- const pendingPath = import_node_path5.default.join(home, ".anchor", "pending_exit.json");
1692
- const data = {
1693
- cwd: ctx.cwd,
1694
- timestamp: Date.now(),
1695
- modifiedCount: gitStatus.split("\n").filter(Boolean).length
1696
- };
1697
- import_node_fs3.default.writeFileSync(pendingPath, JSON.stringify(data, null, 2), "utf-8");
1698
- }
1699
- } catch (_) {
1700
- }
1815
+ pi.registerCommand("anchors", {
1816
+ description: "Inspect and settle active anchors",
1817
+ handler: async (_args, ctx) => {
1818
+ await openAnchorDashboard(ctx, store);
1701
1819
  }
1702
1820
  });
1703
1821
  }