sticky-note-cli 2.6.9 → 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.9",
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": {
@@ -136,12 +136,10 @@ function formatThreadForInjection(threadData, file) {
136
136
  return lines.join("\n");
137
137
  }
138
138
 
139
- // ── Overlap detection via preToolUse deny (Copilot CLI only) ──
140
- // Copilot CLI ignores output from all hooks except preToolUse "deny".
141
- // On the FIRST tool call of a session, if we detect file overlaps with
142
- // other users' threads, we deny the tool call with the overlap warning
143
- // as the reason. The AI reports the denial to the user, then retries.
144
- // Subsequent tool calls proceed normally.
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.
145
143
 
146
144
  const { execSync } = require("child_process");
147
145
 
@@ -169,17 +167,14 @@ function _getRecentlyModifiedFiles() {
169
167
  return files;
170
168
  }
171
169
 
172
- function _checkOverlapsAndDeny(hookInput) {
170
+ function _checkOverlapsAndDeny() {
173
171
  if (!_isCopilotCli()) return null;
172
+ if (!process.env.COPILOT_LOADER_PID) return null;
173
+ if (isOverlapWarned()) return null;
174
174
 
175
- const sessionId = getSessionId(hookInput);
176
- if (isOverlapWarned(sessionId)) return null;
177
-
178
- // Load threads
179
175
  let threads;
180
176
  try {
181
- const memPath = getMemoryPath();
182
- const data = loadJson(memPath);
177
+ const data = loadJson(getMemoryPath());
183
178
  threads = data.threads || [];
184
179
  } catch (_) {
185
180
  return null;
@@ -189,48 +184,34 @@ function _checkOverlapsAndDeny(hookInput) {
189
184
  const modifiedFiles = _getRecentlyModifiedFiles();
190
185
  if (modifiedFiles.size === 0) return null;
191
186
 
192
- const activeThreads = threads.filter(
193
- (t) => t.status === "open" || t.status === "stuck"
194
- );
195
-
196
187
  const warnings = [];
197
- for (const thread of activeThreads) {
188
+ for (const thread of threads.filter((t) => t.status === "open" || t.status === "stuck")) {
198
189
  const threadUser = thread.user || thread.author || "";
199
190
  if (threadUser === currentUser) continue;
200
191
  const threadFiles = (thread.files_touched || []).map(normalizeSep);
201
192
  const overlap = threadFiles.filter((f) => modifiedFiles.has(f));
202
- if (overlap.length === 0) continue;
203
- warnings.push({ thread, overlap });
193
+ if (overlap.length > 0) warnings.push({ thread, overlap });
204
194
  }
205
195
 
206
196
  if (warnings.length === 0) return null;
207
197
 
208
- // Build a concise deny reason
209
198
  const lines = ["⚠️ OVERLAP DETECTED — someone else is working on files you're touching:"];
210
199
  for (const { thread, overlap } of warnings) {
211
200
  const user = thread.user || thread.author || "unknown";
212
201
  const status = thread.status === "stuck" ? "STUCK" : "OPEN";
213
- const narrative = thread.narrative || thread.last_note || "";
214
- const snip = narrative.length > 80 ? narrative.substring(0, 80) + "…" : narrative;
202
+ const snip = (thread.narrative || thread.last_note || "").substring(0, 80);
215
203
  lines.push(` [${status}] ${user}: ${overlap.join(", ")}${snip ? " — " + snip : ""}`);
216
204
  }
217
205
  lines.push("Consider coordinating before proceeding. This tool call will be auto-retried.");
218
206
 
219
- // Mark as warned so subsequent tool calls aren't blocked
220
- markOverlapWarned(sessionId);
207
+ markOverlapWarned();
221
208
 
222
- // Audit the overlap deny
223
209
  try {
224
210
  appendAuditLine({
225
211
  type: "overlap_deny",
226
212
  user: currentUser,
227
213
  ts: new Date().toISOString(),
228
- session_id: sessionId,
229
- warnings_count: warnings.length,
230
- threads: warnings.map((w) => ({
231
- user: w.thread.user || w.thread.author,
232
- files: w.overlap,
233
- })),
214
+ copilot_pid: process.env.COPILOT_LOADER_PID,
234
215
  });
235
216
  } catch (_) { /* ignore */ }
236
217
 
@@ -256,8 +237,8 @@ function main() {
256
237
  const sessionId = getSessionId(hookInput);
257
238
 
258
239
  // ── Overlap deny gate (Copilot CLI only) ──
259
- // On first tool call, deny with overlap warning if applicable.
260
- const denyResult = _checkOverlapsAndDeny(hookInput);
240
+ // Detect overlaps and deny on first tool call, keyed by COPILOT_LOADER_PID.
241
+ const denyResult = _checkOverlapsAndDeny();
261
242
  if (denyResult) {
262
243
  process.stdout.write(JSON.stringify(denyResult) + "\n");
263
244
  return;
@@ -590,32 +590,54 @@ function markThreadInjected(threadId, sessionId) {
590
590
  saveInjectedSet(data);
591
591
  }
592
592
 
593
- // ── Overlap-warned tracking (V2.6) ────────────────────────
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.
594
597
 
595
- function isOverlapWarned(sessionId) {
596
- const data = loadInjectedSet();
597
- // Session-scoped check: different session → not warned
598
- if (sessionId && data.session_id && data.session_id !== sessionId) {
598
+ const _overlapFlagPath = () => path.join(_stickyDir(), ".overlap-warned");
599
+
600
+ function _getCopilotPid() {
601
+ return process.env.COPILOT_LOADER_PID || null;
602
+ }
603
+
604
+ function isOverlapWarned() {
605
+ const pid = _getCopilotPid();
606
+ if (!pid) return false;
607
+ try {
608
+ const data = JSON.parse(fs.readFileSync(_overlapFlagPath(), "utf-8"));
609
+ return !!data[pid];
610
+ } catch (_) {
599
611
  return false;
600
612
  }
601
- // Time-based fallback: if warned more than 60s ago, treat as new session.
602
- // Copilot CLI sessions share session IDs (via .sticky-session file), so
603
- // we can't rely on session_id alone to distinguish separate sessions.
604
- if (data.overlap_warned_at) {
605
- const elapsed = Date.now() - new Date(data.overlap_warned_at).getTime();
606
- if (elapsed > 60000) return false;
607
- }
608
- return !!data.overlap_warned;
609
613
  }
610
614
 
611
- function markOverlapWarned(sessionId) {
612
- const data = loadInjectedSet();
613
- data.session_id = sessionId || data.session_id;
614
- data.overlap_warned = true;
615
- data.overlap_warned_at = new Date().toISOString();
616
- saveInjectedSet(data);
615
+ function markOverlapWarned() {
616
+ const pid = _getCopilotPid();
617
+ if (!pid) return;
618
+ let data = {};
619
+ try {
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
+ } catch (_) { /* ignore */ }
617
631
  }
618
632
 
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 (_) {} }
640
+
619
641
  // ── Active resumed thread tracking (V2.5) ─────────────────
620
642
 
621
643
  const _activeResumePath = () => path.join(_stickyDir(), ".sticky-active-resume");
@@ -702,6 +724,11 @@ module.exports = {
702
724
  // V2.6: overlap warning tracking
703
725
  isOverlapWarned,
704
726
  markOverlapWarned,
727
+ // V2.6.10: overlap signal file
728
+ writeOverlapSignal,
729
+ readAndDeleteOverlapSignal,
730
+ isOverlapDismissed,
731
+ markOverlapDismissed,
705
732
  // V2.5: cross-platform path helper
706
733
  normalizeSep,
707
734
  };