sticky-note-cli 2.6.7 → 2.6.9
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
|
@@ -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) {
|
|
@@ -590,6 +590,32 @@ 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
|
+
// Session-scoped check: different session → not warned
|
|
598
|
+
if (sessionId && data.session_id && data.session_id !== sessionId) {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
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
|
+
}
|
|
610
|
+
|
|
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);
|
|
617
|
+
}
|
|
618
|
+
|
|
593
619
|
// ── Active resumed thread tracking (V2.5) ─────────────────
|
|
594
620
|
|
|
595
621
|
const _activeResumePath = () => path.join(_stickyDir(), ".sticky-active-resume");
|
|
@@ -673,6 +699,9 @@ module.exports = {
|
|
|
673
699
|
getActiveResumeThreadId,
|
|
674
700
|
setActiveResumeThreadId,
|
|
675
701
|
clearActiveResumeThreadId,
|
|
702
|
+
// V2.6: overlap warning tracking
|
|
703
|
+
isOverlapWarned,
|
|
704
|
+
markOverlapWarned,
|
|
676
705
|
// V2.5: cross-platform path helper
|
|
677
706
|
normalizeSep,
|
|
678
707
|
};
|