sticky-note-cli 2.6.10 → 2.6.11
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
|
@@ -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 ───────────────────────────────────────────
|
|
@@ -529,10 +528,6 @@ function main() {
|
|
|
529
528
|
// Even with no scored threads, check for overlaps
|
|
530
529
|
const overlapWarning = detectAndFormatOverlaps(threads, currentUser);
|
|
531
530
|
if (overlapWarning) {
|
|
532
|
-
// Write signal file for preToolUse deny (user-visible in Copilot CLI)
|
|
533
|
-
if (_isCopilotCli() && !isOverlapDismissed()) {
|
|
534
|
-
writeOverlapSignal(overlapWarning);
|
|
535
|
-
}
|
|
536
531
|
_auditInject("no_scored_threads", 0);
|
|
537
532
|
_emit(overlapWarning);
|
|
538
533
|
return;
|
|
@@ -590,10 +585,6 @@ function main() {
|
|
|
590
585
|
// Prepend overlap warning if other users' threads touch the same files
|
|
591
586
|
const overlapWarning = detectAndFormatOverlaps(threads, currentUser);
|
|
592
587
|
if (overlapWarning) {
|
|
593
|
-
// Write signal file for preToolUse deny (user-visible in Copilot CLI)
|
|
594
|
-
if (_isCopilotCli() && !isOverlapDismissed()) {
|
|
595
|
-
writeOverlapSignal(overlapWarning);
|
|
596
|
-
}
|
|
597
588
|
output = overlapWarning + "\n" + output;
|
|
598
589
|
}
|
|
599
590
|
|
|
@@ -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,88 @@ 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
|
-
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;
|
|
156
181
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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 });
|
|
160
194
|
}
|
|
161
|
-
denyLines.push("Consider coordinating before proceeding. This tool call will be auto-retried.");
|
|
162
195
|
|
|
163
|
-
|
|
164
|
-
|
|
196
|
+
if (warnings.length === 0) return null;
|
|
197
|
+
|
|
198
|
+
const lines = ["⚠️ OVERLAP DETECTED — someone else is working on files you're touching:"];
|
|
199
|
+
for (const { thread, overlap } of warnings) {
|
|
200
|
+
const user = thread.user || thread.author || "unknown";
|
|
201
|
+
const status = thread.status === "stuck" ? "STUCK" : "OPEN";
|
|
202
|
+
const snip = (thread.narrative || thread.last_note || "").substring(0, 80);
|
|
203
|
+
lines.push(` [${status}] ${user}: ${overlap.join(", ")}${snip ? " — " + snip : ""}`);
|
|
204
|
+
}
|
|
205
|
+
lines.push("Consider coordinating before proceeding. This tool call will be auto-retried.");
|
|
206
|
+
|
|
207
|
+
markOverlapWarned();
|
|
165
208
|
|
|
166
|
-
// Audit
|
|
167
209
|
try {
|
|
168
210
|
appendAuditLine({
|
|
169
211
|
type: "overlap_deny",
|
|
170
|
-
user:
|
|
212
|
+
user: currentUser,
|
|
171
213
|
ts: new Date().toISOString(),
|
|
172
|
-
|
|
214
|
+
copilot_pid: process.env.COPILOT_LOADER_PID,
|
|
173
215
|
});
|
|
174
216
|
} catch (_) { /* ignore */ }
|
|
175
217
|
|
|
176
218
|
return {
|
|
177
219
|
permissionDecision: "deny",
|
|
178
|
-
permissionDecisionReason:
|
|
220
|
+
permissionDecisionReason: lines.join("\n"),
|
|
179
221
|
};
|
|
180
222
|
}
|
|
181
223
|
|
|
@@ -195,8 +237,8 @@ function main() {
|
|
|
195
237
|
const sessionId = getSessionId(hookInput);
|
|
196
238
|
|
|
197
239
|
// ── Overlap deny gate (Copilot CLI only) ──
|
|
198
|
-
//
|
|
199
|
-
const denyResult =
|
|
240
|
+
// Detect overlaps and deny on first tool call, keyed by COPILOT_LOADER_PID.
|
|
241
|
+
const denyResult = _checkOverlapsAndDeny();
|
|
200
242
|
if (denyResult) {
|
|
201
243
|
process.stdout.write(JSON.stringify(denyResult) + "\n");
|
|
202
244
|
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
|
|