sticky-note-cli 2.6.6 → 2.6.8
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
|
@@ -181,6 +181,25 @@ function detectAndFormatOverlaps(threads, currentUser) {
|
|
|
181
181
|
lines.push(
|
|
182
182
|
"\n**Consider coordinating with these teammates before starting work.**\n"
|
|
183
183
|
);
|
|
184
|
+
|
|
185
|
+
// Write a visible banner directly to stderr so the user sees it in
|
|
186
|
+
// their terminal regardless of whether the AI surfaces it.
|
|
187
|
+
const stderrLines = ["\n⚠️ OVERLAP DETECTED — someone else is touching your files:"];
|
|
188
|
+
for (const { thread, overlap } of warnings) {
|
|
189
|
+
const user = thread.user || thread.author || "unknown";
|
|
190
|
+
const status = thread.status === "stuck" ? "STUCK" : "OPEN";
|
|
191
|
+
const narrative = thread.narrative || thread.last_note || "";
|
|
192
|
+
const narrativeSnip = narrative.length > 80
|
|
193
|
+
? narrative.substring(0, 80) + "…"
|
|
194
|
+
: narrative;
|
|
195
|
+
stderrLines.push(
|
|
196
|
+
` [${status}] ${user}: ${overlap.join(", ")}` +
|
|
197
|
+
(narrativeSnip ? ` — ${narrativeSnip}` : "")
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
stderrLines.push("");
|
|
201
|
+
process.stderr.write(stderrLines.join("\n") + "\n");
|
|
202
|
+
|
|
184
203
|
return lines.join("\n");
|
|
185
204
|
}
|
|
186
205
|
|
|
@@ -61,6 +61,11 @@ const {
|
|
|
61
61
|
appendAuditLine,
|
|
62
62
|
isThreadInjected,
|
|
63
63
|
markThreadInjected,
|
|
64
|
+
isOverlapWarned,
|
|
65
|
+
markOverlapWarned,
|
|
66
|
+
getMemoryPath,
|
|
67
|
+
loadJson,
|
|
68
|
+
normalizeSep,
|
|
64
69
|
} = utils;
|
|
65
70
|
|
|
66
71
|
// ── File path extraction from tool input ──────────────────
|
|
@@ -131,6 +136,110 @@ function formatThreadForInjection(threadData, file) {
|
|
|
131
136
|
return lines.join("\n");
|
|
132
137
|
}
|
|
133
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.
|
|
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
|
+
}
|
|
171
|
+
|
|
172
|
+
function _checkOverlapsAndDeny(hookInput) {
|
|
173
|
+
if (!_isCopilotCli()) return null;
|
|
174
|
+
|
|
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 });
|
|
204
|
+
}
|
|
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 : ""}`);
|
|
216
|
+
}
|
|
217
|
+
lines.push("Consider coordinating before proceeding. This tool call will be auto-retried.");
|
|
218
|
+
|
|
219
|
+
// Mark as warned so subsequent tool calls aren't blocked
|
|
220
|
+
markOverlapWarned(sessionId);
|
|
221
|
+
|
|
222
|
+
// Audit the overlap deny
|
|
223
|
+
try {
|
|
224
|
+
appendAuditLine({
|
|
225
|
+
type: "overlap_deny",
|
|
226
|
+
user: currentUser,
|
|
227
|
+
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
|
+
})),
|
|
234
|
+
});
|
|
235
|
+
} catch (_) { /* ignore */ }
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
permissionDecision: "deny",
|
|
239
|
+
permissionDecisionReason: lines.join("\n"),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
134
243
|
// ── Main ──────────────────────────────────────────────────
|
|
135
244
|
|
|
136
245
|
function main() {
|
|
@@ -146,6 +255,14 @@ function main() {
|
|
|
146
255
|
|
|
147
256
|
const sessionId = getSessionId(hookInput);
|
|
148
257
|
|
|
258
|
+
// ── Overlap deny gate (Copilot CLI only) ──
|
|
259
|
+
// On first tool call, deny with overlap warning if applicable.
|
|
260
|
+
const denyResult = _checkOverlapsAndDeny(hookInput);
|
|
261
|
+
if (denyResult) {
|
|
262
|
+
process.stdout.write(JSON.stringify(denyResult) + "\n");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
149
266
|
// Extract file path from tool input
|
|
150
267
|
const filePath = extractFilePath(hookInput);
|
|
151
268
|
if (!filePath) {
|
|
@@ -394,6 +394,24 @@ function formatOverlapWarnings(threads, currentUser, memory) {
|
|
|
394
394
|
lines.push(
|
|
395
395
|
"\n**Consider coordinating with these teammates before starting work.**"
|
|
396
396
|
);
|
|
397
|
+
|
|
398
|
+
// Write a visible banner directly to stderr so the user sees it
|
|
399
|
+
const stderrLines = ["\n⚠️ OVERLAP DETECTED — someone else is touching your files:"];
|
|
400
|
+
for (const { thread, overlap } of warnings) {
|
|
401
|
+
const user = thread.user || thread.author || "unknown";
|
|
402
|
+
const status = thread.status === "stuck" ? "STUCK" : "OPEN";
|
|
403
|
+
const narrative = thread.narrative || thread.last_note || "";
|
|
404
|
+
const narrativeSnip = narrative.length > 80
|
|
405
|
+
? narrative.substring(0, 80) + "…"
|
|
406
|
+
: narrative;
|
|
407
|
+
stderrLines.push(
|
|
408
|
+
` [${status}] ${user}: ${overlap.join(", ")}` +
|
|
409
|
+
(narrativeSnip ? ` — ${narrativeSnip}` : "")
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
stderrLines.push("");
|
|
413
|
+
process.stderr.write(stderrLines.join("\n") + "\n");
|
|
414
|
+
|
|
397
415
|
return lines.join("\n");
|
|
398
416
|
}
|
|
399
417
|
|
|
@@ -590,6 +590,23 @@ function markThreadInjected(threadId, sessionId) {
|
|
|
590
590
|
saveInjectedSet(data);
|
|
591
591
|
}
|
|
592
592
|
|
|
593
|
+
// ── Overlap-warned tracking (V2.6) ────────────────────────
|
|
594
|
+
|
|
595
|
+
function isOverlapWarned(sessionId) {
|
|
596
|
+
const data = loadInjectedSet();
|
|
597
|
+
if (sessionId && data.session_id && data.session_id !== sessionId) {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
return !!data.overlap_warned;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function markOverlapWarned(sessionId) {
|
|
604
|
+
const data = loadInjectedSet();
|
|
605
|
+
data.session_id = sessionId || data.session_id;
|
|
606
|
+
data.overlap_warned = true;
|
|
607
|
+
saveInjectedSet(data);
|
|
608
|
+
}
|
|
609
|
+
|
|
593
610
|
// ── Active resumed thread tracking (V2.5) ─────────────────
|
|
594
611
|
|
|
595
612
|
const _activeResumePath = () => path.join(_stickyDir(), ".sticky-active-resume");
|
|
@@ -673,6 +690,9 @@ module.exports = {
|
|
|
673
690
|
getActiveResumeThreadId,
|
|
674
691
|
setActiveResumeThreadId,
|
|
675
692
|
clearActiveResumeThreadId,
|
|
693
|
+
// V2.6: overlap warning tracking
|
|
694
|
+
isOverlapWarned,
|
|
695
|
+
markOverlapWarned,
|
|
676
696
|
// V2.5: cross-platform path helper
|
|
677
697
|
normalizeSep,
|
|
678
698
|
};
|