sticky-note-cli 2.6.9 → 2.6.10

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.10",
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,6 +54,7 @@ const {
54
54
  getResumeThreadId, findThreadById, getSessionId,
55
55
  appendAuditLine, detectTool, getConfigPath,
56
56
  isThreadInjected, markThreadInjected, normalizeSep,
57
+ writeOverlapSignal, isOverlapDismissed,
57
58
  } = utils;
58
59
 
59
60
  // ── Git helpers ───────────────────────────────────────────
@@ -528,6 +529,10 @@ function main() {
528
529
  // Even with no scored threads, check for overlaps
529
530
  const overlapWarning = detectAndFormatOverlaps(threads, currentUser);
530
531
  if (overlapWarning) {
532
+ // Write signal file for preToolUse deny (user-visible in Copilot CLI)
533
+ if (_isCopilotCli() && !isOverlapDismissed()) {
534
+ writeOverlapSignal(overlapWarning);
535
+ }
531
536
  _auditInject("no_scored_threads", 0);
532
537
  _emit(overlapWarning);
533
538
  return;
@@ -585,6 +590,10 @@ function main() {
585
590
  // Prepend overlap warning if other users' threads touch the same files
586
591
  const overlapWarning = detectAndFormatOverlaps(threads, currentUser);
587
592
  if (overlapWarning) {
593
+ // Write signal file for preToolUse deny (user-visible in Copilot CLI)
594
+ if (_isCopilotCli() && !isOverlapDismissed()) {
595
+ writeOverlapSignal(overlapWarning);
596
+ }
588
597
  output = overlapWarning + "\n" + output;
589
598
  }
590
599
 
@@ -61,11 +61,8 @@ const {
61
61
  appendAuditLine,
62
62
  isThreadInjected,
63
63
  markThreadInjected,
64
- isOverlapWarned,
65
- markOverlapWarned,
66
- getMemoryPath,
67
- loadJson,
68
- normalizeSep,
64
+ readAndDeleteOverlapSignal,
65
+ markOverlapDismissed,
69
66
  } = utils;
70
67
 
71
68
  // ── File path extraction from tool input ──────────────────
@@ -136,107 +133,49 @@ function formatThreadForInjection(threadData, file) {
136
133
  return lines.join("\n");
137
134
  }
138
135
 
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.
145
-
146
- const { execSync } = require("child_process");
147
-
148
- function _getRecentlyModifiedFiles() {
149
- const files = new Set();
150
- for (const target of ["HEAD~5", "HEAD~1"]) {
151
- try {
152
- const result = execSync(`git diff --name-only ${target}`, {
153
- encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
154
- });
155
- for (const f of result.trim().split(/\r?\n/)) {
156
- if (f.trim()) files.add(f.trim());
157
- }
158
- break;
159
- } catch (_) { /* target doesn't exist */ }
160
- }
161
- try {
162
- const result = execSync("git diff --name-only", {
163
- encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
164
- });
165
- for (const f of result.trim().split(/\r?\n/)) {
166
- if (f.trim()) files.add(f.trim());
167
- }
168
- } catch (_) { /* ignore */ }
169
- return files;
170
- }
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.
171
140
 
172
- function _checkOverlapsAndDeny(hookInput) {
141
+ function _checkOverlapSignal() {
173
142
  if (!_isCopilotCli()) return null;
174
143
 
175
- const sessionId = getSessionId(hookInput);
176
- if (isOverlapWarned(sessionId)) return null;
177
-
178
- // Load threads
179
- let threads;
180
- try {
181
- const memPath = getMemoryPath();
182
- const data = loadJson(memPath);
183
- threads = data.threads || [];
184
- } catch (_) {
185
- return null;
186
- }
187
-
188
- const currentUser = getUser();
189
- const modifiedFiles = _getRecentlyModifiedFiles();
190
- if (modifiedFiles.size === 0) return null;
191
-
192
- const activeThreads = threads.filter(
193
- (t) => t.status === "open" || t.status === "stuck"
194
- );
195
-
196
- const warnings = [];
197
- for (const thread of activeThreads) {
198
- const threadUser = thread.user || thread.author || "";
199
- if (threadUser === currentUser) continue;
200
- const threadFiles = (thread.files_touched || []).map(normalizeSep);
201
- const overlap = threadFiles.filter((f) => modifiedFiles.has(f));
202
- if (overlap.length === 0) continue;
203
- warnings.push({ thread, overlap });
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
+ }
204
156
  }
205
-
206
- if (warnings.length === 0) return null;
207
-
208
- // Build a concise deny reason
209
- const lines = ["⚠️ OVERLAP DETECTED — someone else is working on files you're touching:"];
210
- for (const { thread, overlap } of warnings) {
211
- const user = thread.user || thread.author || "unknown";
212
- 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;
215
- lines.push(` [${status}] ${user}: ${overlap.join(", ")}${snip ? " — " + snip : ""}`);
157
+ if (denyLines.length === 1) {
158
+ // Fallback: just use first 300 chars of signal
159
+ denyLines.push(signalContent.substring(0, 300));
216
160
  }
217
- lines.push("Consider coordinating before proceeding. This tool call will be auto-retried.");
161
+ denyLines.push("Consider coordinating before proceeding. This tool call will be auto-retried.");
218
162
 
219
- // Mark as warned so subsequent tool calls aren't blocked
220
- markOverlapWarned(sessionId);
163
+ // Mark as dismissed so inject-context doesn't re-create signal for 5 minutes
164
+ markOverlapDismissed();
221
165
 
222
- // Audit the overlap deny
166
+ // Audit
223
167
  try {
224
168
  appendAuditLine({
225
169
  type: "overlap_deny",
226
- user: currentUser,
170
+ user: getUser(),
227
171
  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
- })),
172
+ session_id: getSessionId({}),
234
173
  });
235
174
  } catch (_) { /* ignore */ }
236
175
 
237
176
  return {
238
177
  permissionDecision: "deny",
239
- permissionDecisionReason: lines.join("\n"),
178
+ permissionDecisionReason: denyLines.join("\n"),
240
179
  };
241
180
  }
242
181
 
@@ -256,8 +195,8 @@ function main() {
256
195
  const sessionId = getSessionId(hookInput);
257
196
 
258
197
  // ── Overlap deny gate (Copilot CLI only) ──
259
- // On first tool call, deny with overlap warning if applicable.
260
- const denyResult = _checkOverlapsAndDeny(hookInput);
198
+ // Check if inject-context.js wrote a pending overlap signal file.
199
+ const denyResult = _checkOverlapSignal();
261
200
  if (denyResult) {
262
201
  process.stdout.write(JSON.stringify(denyResult) + "\n");
263
202
  return;
@@ -590,32 +590,50 @@ function markThreadInjected(threadId, sessionId) {
590
590
  saveInjectedSet(data);
591
591
  }
592
592
 
593
- // ── Overlap-warned tracking (V2.6) ────────────────────────
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.
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) {
599
- return false;
598
+ const _overlapPendingPath = () => path.join(_stickyDir(), ".overlap-pending");
599
+ const _overlapDismissedPath = () => path.join(_stickyDir(), ".overlap-dismissed");
600
+
601
+ function writeOverlapSignal(text) {
602
+ try {
603
+ fs.writeFileSync(_overlapPendingPath(), text, "utf-8");
604
+ } catch (_) { /* ignore */ }
605
+ }
606
+
607
+ function readAndDeleteOverlapSignal() {
608
+ try {
609
+ const content = fs.readFileSync(_overlapPendingPath(), "utf-8");
610
+ fs.unlinkSync(_overlapPendingPath());
611
+ return content || null;
612
+ } catch (_) {
613
+ return null;
600
614
  }
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;
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
622
+ } catch (_) {
623
+ return false;
607
624
  }
608
- return !!data.overlap_warned;
609
625
  }
610
626
 
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);
627
+ function markOverlapDismissed() {
628
+ try {
629
+ fs.writeFileSync(_overlapDismissedPath(), new Date().toISOString(), "utf-8");
630
+ } catch (_) { /* ignore */ }
617
631
  }
618
632
 
633
+ // Legacy stubs — kept for API compat
634
+ function isOverlapWarned() { return false; }
635
+ function markOverlapWarned() { /* no-op */ }
636
+
619
637
  // ── Active resumed thread tracking (V2.5) ─────────────────
620
638
 
621
639
  const _activeResumePath = () => path.join(_stickyDir(), ".sticky-active-resume");
@@ -702,6 +720,11 @@ module.exports = {
702
720
  // V2.6: overlap warning tracking
703
721
  isOverlapWarned,
704
722
  markOverlapWarned,
723
+ // V2.6.10: overlap signal file
724
+ writeOverlapSignal,
725
+ readAndDeleteOverlapSignal,
726
+ isOverlapDismissed,
727
+ markOverlapDismissed,
705
728
  // V2.5: cross-platform path helper
706
729
  normalizeSep,
707
730
  };