sticky-note-cli 2.5.4 → 2.6.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/bin/cli.js CHANGED
@@ -1372,6 +1372,282 @@ function _relativeTime(tsStr) {
1372
1372
  }
1373
1373
  }
1374
1374
 
1375
+ // ──────────────────────────────────────────────
1376
+ // OVERLAP command — detect file overlaps with open/stuck threads
1377
+ // ──────────────────────────────────────────────
1378
+
1379
+ function cmdOverlap() {
1380
+ const args = process.argv.slice(3);
1381
+ const stickyDir = path.join(process.cwd(), ".sticky-note");
1382
+ const memoryPath = path.join(stickyDir, "sticky-note.json");
1383
+
1384
+ if (!fs.existsSync(memoryPath)) {
1385
+ print(" [ERR] No sticky-note.json found. Run `npx sticky-note init` first.");
1386
+ process.exit(1);
1387
+ }
1388
+
1389
+ printBanner();
1390
+
1391
+ // Get files to check: either --files flag or git diff
1392
+ let filesToCheck = new Set();
1393
+ const filesIdx = args.indexOf("--files");
1394
+ if (filesIdx !== -1 && args.length > filesIdx + 1) {
1395
+ for (const f of args.slice(filesIdx + 1)) {
1396
+ if (f.startsWith("--")) break;
1397
+ filesToCheck.add(_normSep(f));
1398
+ }
1399
+ } else {
1400
+ // Use git diff (uncommitted + recent commits)
1401
+ try {
1402
+ const uncommitted = execSync("git diff --name-only", {
1403
+ encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
1404
+ }).trim();
1405
+ for (const f of uncommitted.split(/\r?\n/)) {
1406
+ if (f.trim()) filesToCheck.add(_normSep(f.trim()));
1407
+ }
1408
+ } catch (_) {}
1409
+ try {
1410
+ const staged = execSync("git diff --name-only --cached", {
1411
+ encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
1412
+ }).trim();
1413
+ for (const f of staged.split(/\r?\n/)) {
1414
+ if (f.trim()) filesToCheck.add(_normSep(f.trim()));
1415
+ }
1416
+ } catch (_) {}
1417
+ const diffTargets = ["HEAD~5", "HEAD~1"];
1418
+ for (const target of diffTargets) {
1419
+ try {
1420
+ const result = execSync(`git diff --name-only ${target}`, {
1421
+ encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
1422
+ }).trim();
1423
+ for (const f of result.split(/\r?\n/)) {
1424
+ if (f.trim()) filesToCheck.add(_normSep(f.trim()));
1425
+ }
1426
+ break;
1427
+ } catch (_) {}
1428
+ }
1429
+ }
1430
+
1431
+ if (filesToCheck.size === 0) {
1432
+ print(" No modified files detected. Use --files <file1> <file2> to check specific files.\n");
1433
+ return;
1434
+ }
1435
+
1436
+ const currentUserNames = _getCurrentUserNames();
1437
+ const isMe = (user) => currentUserNames.has(user || "");
1438
+ const memory = readJsonSafe(memoryPath, { threads: [] });
1439
+ const threads = (memory.threads || []).filter(
1440
+ (t) => t.status === "open" || t.status === "stuck"
1441
+ );
1442
+
1443
+ // Find overlaps
1444
+ const overlaps = [];
1445
+ for (const thread of threads) {
1446
+ const threadFiles = (thread.files_touched || []).map(_normSep);
1447
+ const threadFileSet = new Set(threadFiles);
1448
+ const matching = [];
1449
+ for (const f of filesToCheck) {
1450
+ if (threadFileSet.has(f)) matching.push(f);
1451
+ }
1452
+ if (matching.length > 0) {
1453
+ overlaps.push({ thread, matching });
1454
+ }
1455
+ }
1456
+
1457
+ print(` Checking ${filesToCheck.size} file(s) against ${threads.length} open/stuck thread(s):\n`);
1458
+
1459
+ if (overlaps.length === 0) {
1460
+ print(" ✅ No overlaps found. You're clear to work.\n");
1461
+ return;
1462
+ }
1463
+
1464
+ // Sort: other users first, then stuck before open, then by overlap count
1465
+ overlaps.sort((a, b) => {
1466
+ const aOther = !isMe(a.thread.user) ? 1 : 0;
1467
+ const bOther = !isMe(b.thread.user) ? 1 : 0;
1468
+ if (aOther !== bOther) return bOther - aOther;
1469
+ if (a.thread.status !== b.thread.status) {
1470
+ return a.thread.status === "stuck" ? -1 : 1;
1471
+ }
1472
+ return b.matching.length - a.matching.length;
1473
+ });
1474
+
1475
+ for (const { thread, matching } of overlaps) {
1476
+ const user = thread.user || thread.author || "unknown";
1477
+ const icon = statusIcon(thread.status);
1478
+ const isSelf = isMe(user);
1479
+ const prefix = isSelf ? " " : " ⚠️ ";
1480
+ const suffix = isSelf ? " (you)" : "";
1481
+ const narrative = thread.narrative || thread.last_note || "";
1482
+ const narrativeSnip = narrative.length > 80
1483
+ ? narrative.substring(0, 80) + "…"
1484
+ : narrative;
1485
+ const branch = thread.branch ? ` on ${thread.branch}` : "";
1486
+ const ago = thread.last_activity_at ? _relativeTime(thread.last_activity_at) : "";
1487
+ const agoLabel = ago ? ` (${ago})` : "";
1488
+
1489
+ print(`${prefix}${icon} ${user}${suffix}${branch}${agoLabel}`);
1490
+ if (narrativeSnip) {
1491
+ print(` "${narrativeSnip}"`);
1492
+ }
1493
+ print(` overlapping: ${matching.join(", ")}`);
1494
+ if (thread.failed_approaches && thread.failed_approaches.length > 0) {
1495
+ print(` ⚠️ ${thread.failed_approaches.length} failed approach(es)`);
1496
+ }
1497
+ const threadId = (thread.id || "").substring(0, 8);
1498
+ if (!isSelf && threadId) {
1499
+ print(` → npx sticky-note resume ${threadId}`);
1500
+ }
1501
+ print("");
1502
+ }
1503
+
1504
+ const otherUserOverlaps = overlaps.filter(
1505
+ (o) => !isMe(o.thread.user)
1506
+ );
1507
+ if (otherUserOverlaps.length > 0) {
1508
+ print(` ⚠️ ${otherUserOverlaps.length} thread(s) from other users overlap with your files.`);
1509
+ print(" Consider coordinating before diving in.\n");
1510
+ }
1511
+ }
1512
+
1513
+ function _normSep(p) {
1514
+ return typeof p === "string" ? p.replace(/\\/g, "/") : p;
1515
+ }
1516
+
1517
+ function _getGitUserName() {
1518
+ try {
1519
+ return execSync("git config user.name", {
1520
+ encoding: "utf-8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"],
1521
+ }).trim();
1522
+ } catch (_) {
1523
+ return process.env.USER || process.env.USERNAME || "unknown";
1524
+ }
1525
+ }
1526
+
1527
+ function _getCurrentUserNames() {
1528
+ const names = new Set();
1529
+ const envUser = process.env.USER || process.env.USERNAME || "";
1530
+ if (envUser) names.add(envUser);
1531
+ try {
1532
+ const gitName = execSync("git config user.name", {
1533
+ encoding: "utf-8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"],
1534
+ }).trim();
1535
+ if (gitName) names.add(gitName);
1536
+ } catch (_) {}
1537
+ try {
1538
+ const gitEmail = execSync("git config user.email", {
1539
+ encoding: "utf-8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"],
1540
+ }).trim();
1541
+ if (gitEmail) names.add(gitEmail);
1542
+ } catch (_) {}
1543
+ return names;
1544
+ }
1545
+
1546
+ // ──────────────────────────────────────────────
1547
+ // CLAIM command — declare intent to work on files
1548
+ // ──────────────────────────────────────────────
1549
+
1550
+ function cmdClaim() {
1551
+ const args = process.argv.slice(3);
1552
+ const stickyDir = path.join(process.cwd(), ".sticky-note");
1553
+ const memoryPath = path.join(stickyDir, "sticky-note.json");
1554
+
1555
+ if (!fs.existsSync(memoryPath)) {
1556
+ print(" [ERR] No sticky-note.json found. Run `npx sticky-note init` first.");
1557
+ process.exit(1);
1558
+ }
1559
+
1560
+ // Parse: npx sticky-note claim <file1> [file2...] "description"
1561
+ // or: npx sticky-note claim --clear
1562
+ if (args.includes("--clear")) {
1563
+ const memory = readJsonSafe(memoryPath, { threads: [], claims: [] });
1564
+ const userNames = _getCurrentUserNames();
1565
+ const before = (memory.claims || []).length;
1566
+ memory.claims = (memory.claims || []).filter((c) => !userNames.has(c.user));
1567
+ const removed = before - memory.claims.length;
1568
+ fs.writeFileSync(memoryPath, JSON.stringify(memory, null, 2));
1569
+ print(` [OK] Cleared ${removed} claim(s).`);
1570
+ return;
1571
+ }
1572
+
1573
+ if (args.includes("--list")) {
1574
+ const memory = readJsonSafe(memoryPath, { threads: [], claims: [] });
1575
+ const claims = memory.claims || [];
1576
+ if (claims.length === 0) {
1577
+ print(" No active claims.");
1578
+ return;
1579
+ }
1580
+ printBanner();
1581
+ print(" Active Claims:\n");
1582
+ for (const c of claims) {
1583
+ const ago = c.claimed_at ? _relativeTime(c.claimed_at) : "";
1584
+ const agoLabel = ago ? ` (${ago})` : "";
1585
+ print(` 📌 ${c.user}${agoLabel}: ${c.files.join(", ")}`);
1586
+ if (c.description) print(` "${c.description}"`);
1587
+ }
1588
+ print("");
1589
+ return;
1590
+ }
1591
+
1592
+ if (args.length === 0) {
1593
+ print(" Usage: npx sticky-note claim <file1> [file2...] \"description\"");
1594
+ print(" npx sticky-note claim --list");
1595
+ print(" npx sticky-note claim --clear");
1596
+ return;
1597
+ }
1598
+
1599
+ // Parse files and description (last quoted arg or last arg is description)
1600
+ const files = [];
1601
+ let description = "";
1602
+ for (const a of args) {
1603
+ if (a.startsWith("--")) continue;
1604
+ // If it looks like a file path (has a dot or slash), treat as file
1605
+ if (a.includes("/") || a.includes("\\") || a.includes(".")) {
1606
+ files.push(_normSep(a));
1607
+ } else {
1608
+ // Treat as part of description
1609
+ description += (description ? " " : "") + a;
1610
+ }
1611
+ }
1612
+
1613
+ if (files.length === 0) {
1614
+ print(" [ERR] No files specified. Usage: npx sticky-note claim <file> \"description\"");
1615
+ return;
1616
+ }
1617
+
1618
+ const currentUser = process.env.USER || process.env.USERNAME || _getGitUserName();
1619
+ const userNames = _getCurrentUserNames();
1620
+ const memory = readJsonSafe(memoryPath, { threads: [], claims: [] });
1621
+ if (!memory.claims) memory.claims = [];
1622
+
1623
+ // Remove existing claims from this user for these files
1624
+ memory.claims = memory.claims.filter(
1625
+ (c) => !userNames.has(c.user) || !c.files.some((f) => files.includes(f))
1626
+ );
1627
+
1628
+ memory.claims.push({
1629
+ user: currentUser,
1630
+ files,
1631
+ description,
1632
+ claimed_at: new Date().toISOString(),
1633
+ branch: _getCurrentBranch(),
1634
+ });
1635
+
1636
+ fs.writeFileSync(memoryPath, JSON.stringify(memory, null, 2));
1637
+ print(` ✅ Claimed ${files.join(", ")}${description ? ` — "${description}"` : ""}`);
1638
+ print(` Others will see this at session start. Commit + push to share.`);
1639
+ }
1640
+
1641
+ function _getCurrentBranch() {
1642
+ try {
1643
+ return execSync("git rev-parse --abbrev-ref HEAD", {
1644
+ encoding: "utf-8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"],
1645
+ }).trim();
1646
+ } catch (_) {
1647
+ return "";
1648
+ }
1649
+ }
1650
+
1375
1651
  // ──────────────────────────────────────────────
1376
1652
  // SWITCH command — safe branch switching
1377
1653
  // ──────────────────────────────────────────────
@@ -1954,6 +2230,12 @@ async function main() {
1954
2230
  case "checkpoint":
1955
2231
  cmdCheckpoint();
1956
2232
  break;
2233
+ case "overlap":
2234
+ cmdOverlap();
2235
+ break;
2236
+ case "claim":
2237
+ cmdClaim();
2238
+ break;
1957
2239
  case "--version":
1958
2240
  case "-v":
1959
2241
  print(`sticky-note v${VERSION}`);
@@ -1977,6 +2259,8 @@ async function main() {
1977
2259
  print(" reset Wipe all threads and start fresh (--force, --keep-audit)");
1978
2260
  print(" get-line-attribution File→thread attribution with line ranges (--file, --lines)");
1979
2261
  print(" checkpoint Set work-topic checkpoint for attribution (--topic, --show, --clear)");
2262
+ print(" overlap Detect file overlaps with open/stuck threads");
2263
+ print(" claim Claim ownership of files you're working on (--list, --clear)");
1980
2264
  print("");
1981
2265
  print(" Options:");
1982
2266
  print(" --version Show version");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sticky-note-cli",
3
- "version": "2.5.4",
3
+ "version": "2.6.0",
4
4
  "description": "Human-to-human handoff for AI coding assistants. Git-backed shared memory for Claude Code and Copilot CLI.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -47,6 +47,7 @@ const {
47
47
  markThreadInjected,
48
48
  clearActiveResumeThreadId,
49
49
  getActiveResumeThreadId,
50
+ normalizeSep,
50
51
  } = utils;
51
52
 
52
53
  // ── Stale-thread ageing ───────────────────────────────────
@@ -75,6 +76,34 @@ function ageStaleThreads(memory, staleDays) {
75
76
  return changed;
76
77
  }
77
78
 
79
+ // Auto-close Copilot CLI threads that have been inactive beyond a threshold.
80
+ // Copilot CLI has no session-end signal, so threads stay open indefinitely.
81
+ // This closes them after configurable inactivity (default 24h).
82
+ function autoCloseCopilotCliThreads(memory, autoCloseHours) {
83
+ const now = Date.now();
84
+ let changed = false;
85
+ for (const thread of memory.threads || []) {
86
+ if (thread.status !== "open") continue;
87
+ if (thread.tool !== "copilot-cli") continue;
88
+ const tsField =
89
+ thread.last_activity_at || thread.updated_at || thread.created_at || "";
90
+ if (!tsField) continue;
91
+ try {
92
+ const ts = new Date(tsField).getTime();
93
+ if (isNaN(ts)) continue;
94
+ const diffHours = (now - ts) / (1000 * 60 * 60);
95
+ if (diffHours >= autoCloseHours) {
96
+ thread.status = "closed";
97
+ thread.closed_at = new Date().toISOString();
98
+ changed = true;
99
+ }
100
+ } catch (_) {
101
+ continue;
102
+ }
103
+ }
104
+ return changed;
105
+ }
106
+
78
107
  // ── Formatting helpers ────────────────────────────────────
79
108
 
80
109
  function formatThreadsForInjection(threads, maxThreads, maxTokens) {
@@ -181,9 +210,26 @@ function loadAllPresence() {
181
210
  return data;
182
211
  }
183
212
 
184
- function formatPresence(presenceData) {
213
+ function formatPresence(presenceData, threads) {
185
214
  const now = Date.now();
186
215
  const active = [];
216
+
217
+ // Build a map of user → most recent open/stuck thread
218
+ const userThreads = {};
219
+ for (const t of (threads || [])) {
220
+ if (t.status !== "open" && t.status !== "stuck") continue;
221
+ const user = t.user || t.author || "";
222
+ if (!user) continue;
223
+ const existing = userThreads[user];
224
+ if (!existing) {
225
+ userThreads[user] = t;
226
+ } else {
227
+ const existingTs = existing.last_activity_at || existing.created_at || "";
228
+ const thisTs = t.last_activity_at || t.created_at || "";
229
+ if (thisTs > existingTs) userThreads[user] = t;
230
+ }
231
+ }
232
+
187
233
  for (const [user, info] of Object.entries(presenceData || {})) {
188
234
  const lastSeen = info.last_seen || "";
189
235
  if (!lastSeen) continue;
@@ -192,7 +238,20 @@ function formatPresence(presenceData) {
192
238
  if (isNaN(ts)) continue;
193
239
  if (now - ts < 15 * 60 * 1000) {
194
240
  const files = (info.active_files || []).slice(0, 3).join(", ");
195
- active.push(`- **${user}** active on: ${files}`);
241
+ const thread = userThreads[user];
242
+ let line = `- **${user}**`;
243
+ if (thread) {
244
+ const narrative = thread.narrative || thread.last_note || "";
245
+ const snip = narrative.length > 60
246
+ ? narrative.substring(0, 60) + "…"
247
+ : narrative;
248
+ const status = thread.status === "stuck" ? " [STUCK]" : "";
249
+ line += `${status}: ${snip || files}`;
250
+ if (snip && files) line += ` (${files})`;
251
+ } else {
252
+ line += ` active on: ${files}`;
253
+ }
254
+ active.push(line);
196
255
  }
197
256
  } catch (_) {
198
257
  continue;
@@ -202,6 +261,128 @@ function formatPresence(presenceData) {
202
261
  return "\n## Active Now\n" + active.join("\n");
203
262
  }
204
263
 
264
+ // ── Overlap detection ─────────────────────────────────────
265
+
266
+ function getRecentlyModifiedFiles() {
267
+ const files = new Set();
268
+ const diffTargets = ["HEAD~5", "HEAD~1"];
269
+ for (const target of diffTargets) {
270
+ try {
271
+ const result = require("child_process").execSync(
272
+ `git diff --name-only ${target}`,
273
+ { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }
274
+ );
275
+ for (const f of result.trim().split(/\r?\n/)) {
276
+ const trimmed = f.trim();
277
+ if (trimmed) files.add(normalizeSep(trimmed));
278
+ }
279
+ break;
280
+ } catch (_) {}
281
+ }
282
+ try {
283
+ const result = require("child_process").execSync("git diff --name-only", {
284
+ encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
285
+ });
286
+ for (const f of result.trim().split(/\r?\n/)) {
287
+ const trimmed = f.trim();
288
+ if (trimmed) files.add(normalizeSep(trimmed));
289
+ }
290
+ } catch (_) {}
291
+ try {
292
+ const result = require("child_process").execSync(
293
+ "git diff --name-only --cached",
294
+ { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }
295
+ );
296
+ for (const f of result.trim().split(/\r?\n/)) {
297
+ const trimmed = f.trim();
298
+ if (trimmed) files.add(normalizeSep(trimmed));
299
+ }
300
+ } catch (_) {}
301
+ return files;
302
+ }
303
+
304
+ function detectOverlaps(threads, currentUser) {
305
+ const modifiedFiles = getRecentlyModifiedFiles();
306
+ if (modifiedFiles.size === 0) return { warnings: [], claims: [] };
307
+
308
+ const activeThreads = (threads || []).filter(
309
+ (t) => t.status === "open" || t.status === "stuck"
310
+ );
311
+
312
+ const warnings = [];
313
+ for (const thread of activeThreads) {
314
+ const threadUser = thread.user || thread.author || "";
315
+ if (threadUser === currentUser) continue; // skip own threads
316
+ const threadFiles = (thread.files_touched || []).map(normalizeSep);
317
+ const overlap = threadFiles.filter((f) => modifiedFiles.has(f));
318
+ if (overlap.length === 0) continue;
319
+ warnings.push({ thread, overlap });
320
+ }
321
+
322
+ // Sort: stuck first, then by overlap count
323
+ warnings.sort((a, b) => {
324
+ if (a.thread.status !== b.thread.status) {
325
+ return a.thread.status === "stuck" ? -1 : 1;
326
+ }
327
+ return b.overlap.length - a.overlap.length;
328
+ });
329
+
330
+ return { warnings, modifiedFiles };
331
+ }
332
+
333
+ function formatOverlapWarnings(threads, currentUser, memory) {
334
+ const { warnings, modifiedFiles } = detectOverlaps(threads, currentUser);
335
+ const claims = (memory.claims || []).filter((c) => {
336
+ if (c.user === currentUser) return false;
337
+ if (!modifiedFiles || modifiedFiles.size === 0) return false;
338
+ return (c.files || []).some((f) => modifiedFiles.has(normalizeSep(f)));
339
+ });
340
+
341
+ if (warnings.length === 0 && claims.length === 0) return "";
342
+
343
+ const lines = [
344
+ "## [STICKY-NOTE] ⚠️ OVERLAP DETECTED\n",
345
+ "Someone else is working on files you're touching:\n",
346
+ ];
347
+
348
+ for (const { thread, overlap } of warnings) {
349
+ const user = thread.user || thread.author || "unknown";
350
+ const status = thread.status === "stuck" ? "[STUCK]" : "[OPEN]";
351
+ const narrative = thread.narrative || thread.last_note || "";
352
+ const narrativeSnip = narrative.length > 100
353
+ ? narrative.substring(0, 100) + "…"
354
+ : narrative;
355
+ const branch = thread.branch ? ` on \`${thread.branch}\`` : "";
356
+ const threadId = (thread.id || "").substring(0, 8);
357
+
358
+ lines.push(`- ${status} **${user}**${branch}: ${overlap.join(", ")}`);
359
+ if (narrativeSnip) lines.push(` _${narrativeSnip}_`);
360
+ const failed = thread.failed_approaches || [];
361
+ if (failed.length > 0) {
362
+ lines.push(` ⚠️ ${failed.length} failed approach(es):`);
363
+ for (const fa of failed.slice(0, 2)) {
364
+ lines.push(` - ${(fa.description || "").substring(0, 80)}`);
365
+ }
366
+ }
367
+ if (threadId) {
368
+ lines.push(` → Resume: \`npx sticky-note resume ${threadId}\``);
369
+ }
370
+ }
371
+
372
+ for (const claim of claims) {
373
+ const ago = claim.claimed_at || "";
374
+ lines.push(
375
+ `- 📌 **${claim.user}** claimed: ${(claim.files || []).join(", ")}` +
376
+ (claim.description ? ` — "${claim.description}"` : "")
377
+ );
378
+ }
379
+
380
+ lines.push(
381
+ "\n**Consider coordinating with these teammates before starting work.**"
382
+ );
383
+ return lines.join("\n");
384
+ }
385
+
205
386
  // ── Main ──────────────────────────────────────────────────
206
387
 
207
388
  function main() {
@@ -243,8 +424,13 @@ function main() {
243
424
  });
244
425
  const config = loadJson(getConfigPath(), { stale_days: 14 });
245
426
  const staleDays = config.stale_days != null ? config.stale_days : 14;
427
+ const autoCloseHours =
428
+ config.copilot_cli_auto_close_hours != null
429
+ ? config.copilot_cli_auto_close_hours
430
+ : 24;
246
431
 
247
432
  ageStaleThreads(memory, staleDays);
433
+ autoCloseCopilotCliThreads(memory, autoCloseHours);
248
434
 
249
435
  // ── Resume handling ───────────────────────────────────
250
436
  const resumeThreadId = getResumeThreadId();
@@ -280,7 +466,10 @@ function main() {
280
466
  const threadContext = threadResult.text;
281
467
  const configContext = formatConfigForInjection(config);
282
468
  const presenceData = loadAllPresence();
283
- const presenceContext = formatPresence(presenceData);
469
+ const presenceContext = formatPresence(presenceData, memory.threads || []);
470
+ const overlapContext = formatOverlapWarnings(
471
+ memory.threads || [], getUser(), memory
472
+ );
284
473
 
285
474
  // V2.5: Mark eagerly-injected stuck threads so PreToolUse won't re-inject
286
475
  for (const threadId of threadResult.threadIds) {
@@ -378,6 +567,9 @@ function main() {
378
567
  parts.push(resumeLines.join("\n"));
379
568
  }
380
569
 
570
+ // Overlap warning comes first — most urgent signal
571
+ if (overlapContext) parts.push(overlapContext);
572
+
381
573
  if (threadContext) parts.push(threadContext);
382
574
  if (configContext) parts.push(configContext);
383
575
  if (presenceContext) parts.push(presenceContext);
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "stale_days": 14,
3
+ "copilot_cli_auto_close_hours": 24,
3
4
  "inject_token_budget": 1000,
4
5
  "mcp_servers": [],
5
6
  "skills": [],