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
package/templates/CLAUDE.md
CHANGED
|
@@ -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.
|
|
193
|
-
|
|
194
|
-
|
|
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.
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
155
|
-
"**
|
|
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
|
-
|
|
65
|
-
|
|
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
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
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
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
210
|
+
lines.push("\nAfter displaying the warning, continue with the user's request. This tool call will be auto-retried.");
|
|
162
211
|
|
|
163
|
-
|
|
164
|
-
markOverlapDismissed();
|
|
212
|
+
markOverlapWarned();
|
|
165
213
|
|
|
166
|
-
// Audit
|
|
167
214
|
try {
|
|
168
215
|
appendAuditLine({
|
|
169
216
|
type: "overlap_deny",
|
|
170
|
-
user:
|
|
217
|
+
user: currentUser,
|
|
171
218
|
ts: new Date().toISOString(),
|
|
172
|
-
|
|
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:
|
|
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
|
-
//
|
|
199
|
-
const denyResult =
|
|
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
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
//
|
|
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
|
|
599
|
-
const _overlapDismissedPath = () => path.join(_stickyDir(), ".overlap-dismissed");
|
|
598
|
+
const _overlapFlagPath = () => path.join(_stickyDir(), ".overlap-warned");
|
|
600
599
|
|
|
601
|
-
function
|
|
602
|
-
|
|
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
|
|
604
|
+
function isOverlapWarned() {
|
|
605
|
+
const pid = _getCopilotPid();
|
|
606
|
+
if (!pid) return false;
|
|
608
607
|
try {
|
|
609
|
-
const
|
|
610
|
-
|
|
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
|
|
615
|
+
function markOverlapWarned() {
|
|
616
|
+
const pid = _getCopilotPid();
|
|
617
|
+
if (!pid) return;
|
|
618
|
+
let data = {};
|
|
628
619
|
try {
|
|
629
|
-
|
|
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
|
-
//
|
|
634
|
-
|
|
635
|
-
|
|
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
|
|