sticky-note-cli 2.6.10 → 2.6.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sticky-note-cli",
3
- "version": "2.6.10",
3
+ "version": "2.6.11",
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": {
@@ -54,7 +54,6 @@ const {
54
54
  getResumeThreadId, findThreadById, getSessionId,
55
55
  appendAuditLine, detectTool, getConfigPath,
56
56
  isThreadInjected, markThreadInjected, normalizeSep,
57
- writeOverlapSignal, isOverlapDismissed,
58
57
  } = utils;
59
58
 
60
59
  // ── Git helpers ───────────────────────────────────────────
@@ -529,10 +528,6 @@ function main() {
529
528
  // Even with no scored threads, check for overlaps
530
529
  const overlapWarning = detectAndFormatOverlaps(threads, currentUser);
531
530
  if (overlapWarning) {
532
- // Write signal file for preToolUse deny (user-visible in Copilot CLI)
533
- if (_isCopilotCli() && !isOverlapDismissed()) {
534
- writeOverlapSignal(overlapWarning);
535
- }
536
531
  _auditInject("no_scored_threads", 0);
537
532
  _emit(overlapWarning);
538
533
  return;
@@ -590,10 +585,6 @@ function main() {
590
585
  // Prepend overlap warning if other users' threads touch the same files
591
586
  const overlapWarning = detectAndFormatOverlaps(threads, currentUser);
592
587
  if (overlapWarning) {
593
- // Write signal file for preToolUse deny (user-visible in Copilot CLI)
594
- if (_isCopilotCli() && !isOverlapDismissed()) {
595
- writeOverlapSignal(overlapWarning);
596
- }
597
588
  output = overlapWarning + "\n" + output;
598
589
  }
599
590
 
@@ -61,8 +61,11 @@ const {
61
61
  appendAuditLine,
62
62
  isThreadInjected,
63
63
  markThreadInjected,
64
- readAndDeleteOverlapSignal,
65
- markOverlapDismissed,
64
+ isOverlapWarned,
65
+ markOverlapWarned,
66
+ getMemoryPath,
67
+ loadJson,
68
+ normalizeSep,
66
69
  } = utils;
67
70
 
68
71
  // ── File path extraction from tool input ──────────────────
@@ -133,49 +136,88 @@ function formatThreadForInjection(threadData, file) {
133
136
  return lines.join("\n");
134
137
  }
135
138
 
136
- // ── Overlap deny via signal file (Copilot CLI only) ───────
137
- // inject-context.js writes .overlap-pending with the warning text.
138
- // We read+delete it atomically and return a deny. No shared state,
139
- // no TTL, no session-ID issues.
139
+ // ── Overlap deny via COPILOT_LOADER_PID (Copilot CLI only) ──
140
+ // Each Copilot CLI session has a unique COPILOT_LOADER_PID env var.
141
+ // On the first tool call, if overlaps exist, deny with the warning.
142
+ // The warned state is keyed by PID so concurrent sessions don't interfere.
143
+
144
+ const { execSync } = require("child_process");
145
+
146
+ function _getRecentlyModifiedFiles() {
147
+ const files = new Set();
148
+ for (const target of ["HEAD~5", "HEAD~1"]) {
149
+ try {
150
+ const result = execSync(`git diff --name-only ${target}`, {
151
+ encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
152
+ });
153
+ for (const f of result.trim().split(/\r?\n/)) {
154
+ if (f.trim()) files.add(f.trim());
155
+ }
156
+ break;
157
+ } catch (_) { /* target doesn't exist */ }
158
+ }
159
+ try {
160
+ const result = execSync("git diff --name-only", {
161
+ encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
162
+ });
163
+ for (const f of result.trim().split(/\r?\n/)) {
164
+ if (f.trim()) files.add(f.trim());
165
+ }
166
+ } catch (_) { /* ignore */ }
167
+ return files;
168
+ }
140
169
 
141
- function _checkOverlapSignal() {
170
+ function _checkOverlapsAndDeny() {
142
171
  if (!_isCopilotCli()) return null;
172
+ if (!process.env.COPILOT_LOADER_PID) return null;
173
+ if (isOverlapWarned()) return null;
143
174
 
144
- const signalContent = readAndDeleteOverlapSignal();
145
- if (!signalContent) return null;
146
-
147
- // Extract a concise version for the deny reason
148
- // The signal file contains the full markdown warning from detectAndFormatOverlaps
149
- const lines = signalContent.split("\n").filter((l) => l.trim());
150
- const denyLines = ["⚠️ OVERLAP DETECTED — someone else is working on files you're touching:"];
151
- for (const line of lines) {
152
- // Include lines that show user/file info (start with "- [")
153
- if (line.trimStart().startsWith("- [")) {
154
- denyLines.push(" " + line.trim().replace(/^- /, ""));
155
- }
175
+ let threads;
176
+ try {
177
+ const data = loadJson(getMemoryPath());
178
+ threads = data.threads || [];
179
+ } catch (_) {
180
+ return null;
156
181
  }
157
- if (denyLines.length === 1) {
158
- // Fallback: just use first 300 chars of signal
159
- denyLines.push(signalContent.substring(0, 300));
182
+
183
+ const currentUser = getUser();
184
+ const modifiedFiles = _getRecentlyModifiedFiles();
185
+ if (modifiedFiles.size === 0) return null;
186
+
187
+ const warnings = [];
188
+ for (const thread of threads.filter((t) => t.status === "open" || t.status === "stuck")) {
189
+ const threadUser = thread.user || thread.author || "";
190
+ if (threadUser === currentUser) continue;
191
+ const threadFiles = (thread.files_touched || []).map(normalizeSep);
192
+ const overlap = threadFiles.filter((f) => modifiedFiles.has(f));
193
+ if (overlap.length > 0) warnings.push({ thread, overlap });
160
194
  }
161
- denyLines.push("Consider coordinating before proceeding. This tool call will be auto-retried.");
162
195
 
163
- // Mark as dismissed so inject-context doesn't re-create signal for 5 minutes
164
- markOverlapDismissed();
196
+ if (warnings.length === 0) return null;
197
+
198
+ const lines = ["⚠️ OVERLAP DETECTED — someone else is working on files you're touching:"];
199
+ for (const { thread, overlap } of warnings) {
200
+ const user = thread.user || thread.author || "unknown";
201
+ const status = thread.status === "stuck" ? "STUCK" : "OPEN";
202
+ const snip = (thread.narrative || thread.last_note || "").substring(0, 80);
203
+ lines.push(` [${status}] ${user}: ${overlap.join(", ")}${snip ? " — " + snip : ""}`);
204
+ }
205
+ lines.push("Consider coordinating before proceeding. This tool call will be auto-retried.");
206
+
207
+ markOverlapWarned();
165
208
 
166
- // Audit
167
209
  try {
168
210
  appendAuditLine({
169
211
  type: "overlap_deny",
170
- user: getUser(),
212
+ user: currentUser,
171
213
  ts: new Date().toISOString(),
172
- session_id: getSessionId({}),
214
+ copilot_pid: process.env.COPILOT_LOADER_PID,
173
215
  });
174
216
  } catch (_) { /* ignore */ }
175
217
 
176
218
  return {
177
219
  permissionDecision: "deny",
178
- permissionDecisionReason: denyLines.join("\n"),
220
+ permissionDecisionReason: lines.join("\n"),
179
221
  };
180
222
  }
181
223
 
@@ -195,8 +237,8 @@ function main() {
195
237
  const sessionId = getSessionId(hookInput);
196
238
 
197
239
  // ── Overlap deny gate (Copilot CLI only) ──
198
- // Check if inject-context.js wrote a pending overlap signal file.
199
- const denyResult = _checkOverlapSignal();
240
+ // Detect overlaps and deny on first tool call, keyed by COPILOT_LOADER_PID.
241
+ const denyResult = _checkOverlapsAndDeny();
200
242
  if (denyResult) {
201
243
  process.stdout.write(JSON.stringify(denyResult) + "\n");
202
244
  return;
@@ -590,49 +590,53 @@ function markThreadInjected(threadId, sessionId) {
590
590
  saveInjectedSet(data);
591
591
  }
592
592
 
593
- // ── Overlap signal file (V2.6.10) ─────────────────────────
594
- // Signal file approach: inject-context.js writes .overlap-pending with the
595
- // warning text. pre-tool-use.js reads+deletes it atomically and returns a
596
- // deny with the content. This avoids shared-state / TTL / session-ID issues.
593
+ // ── Overlap warning tracking (V2.6.11) ────────────────────
594
+ // Uses COPILOT_LOADER_PID to identify each Copilot CLI session uniquely.
595
+ // Each session gets its own overlap-warned flag, preventing cross-session
596
+ // poisoning. The flag file stores a JSON object keyed by PID.
597
597
 
598
- const _overlapPendingPath = () => path.join(_stickyDir(), ".overlap-pending");
599
- const _overlapDismissedPath = () => path.join(_stickyDir(), ".overlap-dismissed");
598
+ const _overlapFlagPath = () => path.join(_stickyDir(), ".overlap-warned");
600
599
 
601
- function writeOverlapSignal(text) {
602
- try {
603
- fs.writeFileSync(_overlapPendingPath(), text, "utf-8");
604
- } catch (_) { /* ignore */ }
600
+ function _getCopilotPid() {
601
+ return process.env.COPILOT_LOADER_PID || null;
605
602
  }
606
603
 
607
- function readAndDeleteOverlapSignal() {
604
+ function isOverlapWarned() {
605
+ const pid = _getCopilotPid();
606
+ if (!pid) return false;
608
607
  try {
609
- const content = fs.readFileSync(_overlapPendingPath(), "utf-8");
610
- fs.unlinkSync(_overlapPendingPath());
611
- return content || null;
612
- } catch (_) {
613
- return null;
614
- }
615
- }
616
-
617
- function isOverlapDismissed(ttlMs) {
618
- try {
619
- const raw = fs.readFileSync(_overlapDismissedPath(), "utf-8").trim();
620
- const elapsed = Date.now() - new Date(raw).getTime();
621
- return elapsed < (ttlMs || 300000); // default 5 minutes
608
+ const data = JSON.parse(fs.readFileSync(_overlapFlagPath(), "utf-8"));
609
+ return !!data[pid];
622
610
  } catch (_) {
623
611
  return false;
624
612
  }
625
613
  }
626
614
 
627
- function markOverlapDismissed() {
615
+ function markOverlapWarned() {
616
+ const pid = _getCopilotPid();
617
+ if (!pid) return;
618
+ let data = {};
628
619
  try {
629
- fs.writeFileSync(_overlapDismissedPath(), new Date().toISOString(), "utf-8");
620
+ data = JSON.parse(fs.readFileSync(_overlapFlagPath(), "utf-8"));
621
+ } catch (_) { /* empty or missing */ }
622
+ // Clean up stale PIDs (older than 1 hour)
623
+ const now = Date.now();
624
+ for (const [k, v] of Object.entries(data)) {
625
+ if (now - v > 3600000) delete data[k];
626
+ }
627
+ data[pid] = now;
628
+ try {
629
+ fs.writeFileSync(_overlapFlagPath(), JSON.stringify(data), "utf-8");
630
630
  } catch (_) { /* ignore */ }
631
631
  }
632
632
 
633
- // Legacy stubs — kept for API compat
634
- function isOverlapWarned() { return false; }
635
- function markOverlapWarned() { /* no-op */ }
633
+ // Signal file helpers — kept for backward compat but no longer primary
634
+ const _overlapPendingPath = () => path.join(_stickyDir(), ".overlap-pending");
635
+ const _overlapDismissedPath = () => path.join(_stickyDir(), ".overlap-dismissed");
636
+ function writeOverlapSignal(text) { try { fs.writeFileSync(_overlapPendingPath(), text, "utf-8"); } catch (_) {} }
637
+ function readAndDeleteOverlapSignal() { try { const c = fs.readFileSync(_overlapPendingPath(), "utf-8"); fs.unlinkSync(_overlapPendingPath()); return c || null; } catch (_) { return null; } }
638
+ function isOverlapDismissed(ttlMs) { try { const e = Date.now() - new Date(fs.readFileSync(_overlapDismissedPath(), "utf-8").trim()).getTime(); return e < (ttlMs || 300000); } catch (_) { return false; } }
639
+ function markOverlapDismissed() { try { fs.writeFileSync(_overlapDismissedPath(), new Date().toISOString(), "utf-8"); } catch (_) {} }
636
640
 
637
641
  // ── Active resumed thread tracking (V2.5) ─────────────────
638
642