sticky-note-cli 2.6.10 → 2.6.12

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.12",
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": {
@@ -189,9 +189,15 @@ if set up by `npx sticky-note init`.
189
189
  ### ⚠️ Overlap warnings (CRITICAL — always surface these)
190
190
 
191
191
  Sticky-note hooks detect when your files overlap with other teammates'
192
- open or stuck threads. When you see `[STICKY-NOTE] ⚠️ OVERLAP DETECTED`
193
- in your injected context, you **MUST** start your response with this
194
- exact banner format before doing ANYTHING else:
192
+ open or stuck threads. Overlap warnings arrive through TWO channels:
193
+
194
+ 1. **Injected context** — `[STICKY-NOTE] ⚠️ OVERLAP DETECTED` appears in
195
+ your hook output. This fires on every user prompt.
196
+ 2. **Tool call deny** — Your first tool call may be denied with a reason
197
+ containing "CRITICAL INSTRUCTION" and the overlap details.
198
+
199
+ When you receive an overlap warning through EITHER channel, you **MUST**
200
+ start your response with this exact banner format before doing ANYTHING else:
195
201
 
196
202
  ```
197
203
  ⚠️ Overlap detected: [user] is working on [files] — [narrative summary]
@@ -202,6 +208,7 @@ Rules:
202
208
  - This banner MUST be the **very first thing** in your response
203
209
  - Do NOT bury it at the end or in a footnote
204
210
  - Do NOT rephrase it as "Also noted..." or "By the way..."
211
+ - If a tool call was denied with overlap info, display the warning then proceed
205
212
  - Then continue with the user's actual request normally
206
213
 
207
214
  This pattern exists to prevent two teammates from wasting hours on the
@@ -167,9 +167,16 @@ if set up by `npx sticky-note init`.
167
167
  ### ⚠️ Overlap warnings (CRITICAL — always surface these)
168
168
 
169
169
  Sticky-note hooks detect when your files overlap with other teammates'
170
- open or stuck threads. When you see `[STICKY-NOTE] ⚠️ OVERLAP DETECTED`
171
- in your injected context, you **MUST** start your response with this
172
- exact banner format before doing ANYTHING else:
170
+ open or stuck threads. Overlap warnings arrive through TWO channels:
171
+
172
+ 1. **Injected context** — `[STICKY-NOTE] ⚠️ OVERLAP DETECTED` appears in
173
+ your `additionalContext`. This fires on every user prompt.
174
+ 2. **Tool call deny** — Your first tool call may be denied with a
175
+ `permissionDecisionReason` containing "CRITICAL INSTRUCTION" and the
176
+ overlap details. After the deny, the tool is auto-retried.
177
+
178
+ When you receive an overlap warning through EITHER channel, you **MUST**
179
+ start your response with this exact banner format before doing ANYTHING else:
173
180
 
174
181
  ```
175
182
  ⚠️ Overlap detected: [user] is working on [files] — [narrative summary]
@@ -187,6 +194,7 @@ Rules:
187
194
  - Show it BEFORE your thinking indicators (◐, ●, etc.)
188
195
  - Do NOT bury it at the end or in a footnote
189
196
  - Do NOT rephrase it as "Also noted..." or "By the way..."
197
+ - If a tool call was denied with overlap info, display the warning then proceed
190
198
  - Then continue with the user's actual request normally
191
199
 
192
200
  This pattern exists to prevent two teammates from wasting hours on the
@@ -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 ───────────────────────────────────────────
@@ -151,8 +150,9 @@ function detectAndFormatOverlaps(threads, currentUser) {
151
150
  });
152
151
 
153
152
  const lines = [
154
- "## [STICKY-NOTE] ⚠️ OVERLAP DETECTED — TELL THE USER IMMEDIATELY\n",
155
- "**IMPORTANT: You MUST surface this warning to the user before doing anything else.**\n",
153
+ "## [STICKY-NOTE] ⚠️ OVERLAP DETECTED\n",
154
+ "**CRITICAL INSTRUCTION: Display the overlap warning below as the FIRST line of your response.",
155
+ "Do NOT skip this. Do NOT rephrase it. Do NOT bury it.**\n",
156
156
  "Someone else is working on files you're touching:\n",
157
157
  ];
158
158
  for (const { thread, overlap } of warnings) {
@@ -529,10 +529,6 @@ function main() {
529
529
  // Even with no scored threads, check for overlaps
530
530
  const overlapWarning = detectAndFormatOverlaps(threads, currentUser);
531
531
  if (overlapWarning) {
532
- // Write signal file for preToolUse deny (user-visible in Copilot CLI)
533
- if (_isCopilotCli() && !isOverlapDismissed()) {
534
- writeOverlapSignal(overlapWarning);
535
- }
536
532
  _auditInject("no_scored_threads", 0);
537
533
  _emit(overlapWarning);
538
534
  return;
@@ -590,10 +586,6 @@ function main() {
590
586
  // Prepend overlap warning if other users' threads touch the same files
591
587
  const overlapWarning = detectAndFormatOverlaps(threads, currentUser);
592
588
  if (overlapWarning) {
593
- // Write signal file for preToolUse deny (user-visible in Copilot CLI)
594
- if (_isCopilotCli() && !isOverlapDismissed()) {
595
- writeOverlapSignal(overlapWarning);
596
- }
597
589
  output = overlapWarning + "\n" + output;
598
590
  }
599
591
 
@@ -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,93 @@ 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;
181
+ }
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 });
156
194
  }
157
- if (denyLines.length === 1) {
158
- // Fallback: just use first 300 chars of signal
159
- denyLines.push(signalContent.substring(0, 300));
195
+
196
+ if (warnings.length === 0) return null;
197
+
198
+ const lines = [
199
+ "CRITICAL INSTRUCTION: Before doing anything else, display the following overlap warning to the user.",
200
+ "This is NOT optional. Show it as the FIRST line of your response.\n",
201
+ ];
202
+ for (const { thread, overlap } of warnings) {
203
+ const user = thread.user || thread.author || "unknown";
204
+ const status = thread.status === "stuck" ? "STUCK" : "OPEN";
205
+ const snip = (thread.narrative || thread.last_note || "").substring(0, 80);
206
+ const threadId = (thread.id || "").substring(0, 8);
207
+ lines.push(`⚠️ Overlap detected: ${user} is working on ${overlap.join(", ")}${snip ? " — " + snip : ""}`);
208
+ lines.push(` Status: ${status} | Resume: npx sticky-note resume ${threadId}`);
160
209
  }
161
- denyLines.push("Consider coordinating before proceeding. This tool call will be auto-retried.");
210
+ lines.push("\nAfter displaying the warning, continue with the user's request. This tool call will be auto-retried.");
162
211
 
163
- // Mark as dismissed so inject-context doesn't re-create signal for 5 minutes
164
- markOverlapDismissed();
212
+ markOverlapWarned();
165
213
 
166
- // Audit
167
214
  try {
168
215
  appendAuditLine({
169
216
  type: "overlap_deny",
170
- user: getUser(),
217
+ user: currentUser,
171
218
  ts: new Date().toISOString(),
172
- session_id: getSessionId({}),
219
+ copilot_pid: process.env.COPILOT_LOADER_PID,
173
220
  });
174
221
  } catch (_) { /* ignore */ }
175
222
 
176
223
  return {
177
224
  permissionDecision: "deny",
178
- permissionDecisionReason: denyLines.join("\n"),
225
+ permissionDecisionReason: lines.join("\n"),
179
226
  };
180
227
  }
181
228
 
@@ -195,8 +242,8 @@ function main() {
195
242
  const sessionId = getSessionId(hookInput);
196
243
 
197
244
  // ── Overlap deny gate (Copilot CLI only) ──
198
- // Check if inject-context.js wrote a pending overlap signal file.
199
- const denyResult = _checkOverlapSignal();
245
+ // Detect overlaps and deny on first tool call, keyed by COPILOT_LOADER_PID.
246
+ const denyResult = _checkOverlapsAndDeny();
200
247
  if (denyResult) {
201
248
  process.stdout.write(JSON.stringify(denyResult) + "\n");
202
249
  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