sticky-note-cli 2.6.9 → 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
|
@@ -136,12 +136,10 @@ function formatThreadForInjection(threadData, file) {
|
|
|
136
136
|
return lines.join("\n");
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
// ── Overlap
|
|
140
|
-
// Copilot CLI
|
|
141
|
-
// On the
|
|
142
|
-
//
|
|
143
|
-
// as the reason. The AI reports the denial to the user, then retries.
|
|
144
|
-
// Subsequent tool calls proceed normally.
|
|
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.
|
|
145
143
|
|
|
146
144
|
const { execSync } = require("child_process");
|
|
147
145
|
|
|
@@ -169,17 +167,14 @@ function _getRecentlyModifiedFiles() {
|
|
|
169
167
|
return files;
|
|
170
168
|
}
|
|
171
169
|
|
|
172
|
-
function _checkOverlapsAndDeny(
|
|
170
|
+
function _checkOverlapsAndDeny() {
|
|
173
171
|
if (!_isCopilotCli()) return null;
|
|
172
|
+
if (!process.env.COPILOT_LOADER_PID) return null;
|
|
173
|
+
if (isOverlapWarned()) return null;
|
|
174
174
|
|
|
175
|
-
const sessionId = getSessionId(hookInput);
|
|
176
|
-
if (isOverlapWarned(sessionId)) return null;
|
|
177
|
-
|
|
178
|
-
// Load threads
|
|
179
175
|
let threads;
|
|
180
176
|
try {
|
|
181
|
-
const
|
|
182
|
-
const data = loadJson(memPath);
|
|
177
|
+
const data = loadJson(getMemoryPath());
|
|
183
178
|
threads = data.threads || [];
|
|
184
179
|
} catch (_) {
|
|
185
180
|
return null;
|
|
@@ -189,48 +184,34 @@ function _checkOverlapsAndDeny(hookInput) {
|
|
|
189
184
|
const modifiedFiles = _getRecentlyModifiedFiles();
|
|
190
185
|
if (modifiedFiles.size === 0) return null;
|
|
191
186
|
|
|
192
|
-
const activeThreads = threads.filter(
|
|
193
|
-
(t) => t.status === "open" || t.status === "stuck"
|
|
194
|
-
);
|
|
195
|
-
|
|
196
187
|
const warnings = [];
|
|
197
|
-
for (const thread of
|
|
188
|
+
for (const thread of threads.filter((t) => t.status === "open" || t.status === "stuck")) {
|
|
198
189
|
const threadUser = thread.user || thread.author || "";
|
|
199
190
|
if (threadUser === currentUser) continue;
|
|
200
191
|
const threadFiles = (thread.files_touched || []).map(normalizeSep);
|
|
201
192
|
const overlap = threadFiles.filter((f) => modifiedFiles.has(f));
|
|
202
|
-
if (overlap.length
|
|
203
|
-
warnings.push({ thread, overlap });
|
|
193
|
+
if (overlap.length > 0) warnings.push({ thread, overlap });
|
|
204
194
|
}
|
|
205
195
|
|
|
206
196
|
if (warnings.length === 0) return null;
|
|
207
197
|
|
|
208
|
-
// Build a concise deny reason
|
|
209
198
|
const lines = ["⚠️ OVERLAP DETECTED — someone else is working on files you're touching:"];
|
|
210
199
|
for (const { thread, overlap } of warnings) {
|
|
211
200
|
const user = thread.user || thread.author || "unknown";
|
|
212
201
|
const status = thread.status === "stuck" ? "STUCK" : "OPEN";
|
|
213
|
-
const
|
|
214
|
-
const snip = narrative.length > 80 ? narrative.substring(0, 80) + "…" : narrative;
|
|
202
|
+
const snip = (thread.narrative || thread.last_note || "").substring(0, 80);
|
|
215
203
|
lines.push(` [${status}] ${user}: ${overlap.join(", ")}${snip ? " — " + snip : ""}`);
|
|
216
204
|
}
|
|
217
205
|
lines.push("Consider coordinating before proceeding. This tool call will be auto-retried.");
|
|
218
206
|
|
|
219
|
-
|
|
220
|
-
markOverlapWarned(sessionId);
|
|
207
|
+
markOverlapWarned();
|
|
221
208
|
|
|
222
|
-
// Audit the overlap deny
|
|
223
209
|
try {
|
|
224
210
|
appendAuditLine({
|
|
225
211
|
type: "overlap_deny",
|
|
226
212
|
user: currentUser,
|
|
227
213
|
ts: new Date().toISOString(),
|
|
228
|
-
|
|
229
|
-
warnings_count: warnings.length,
|
|
230
|
-
threads: warnings.map((w) => ({
|
|
231
|
-
user: w.thread.user || w.thread.author,
|
|
232
|
-
files: w.overlap,
|
|
233
|
-
})),
|
|
214
|
+
copilot_pid: process.env.COPILOT_LOADER_PID,
|
|
234
215
|
});
|
|
235
216
|
} catch (_) { /* ignore */ }
|
|
236
217
|
|
|
@@ -256,8 +237,8 @@ function main() {
|
|
|
256
237
|
const sessionId = getSessionId(hookInput);
|
|
257
238
|
|
|
258
239
|
// ── Overlap deny gate (Copilot CLI only) ──
|
|
259
|
-
//
|
|
260
|
-
const denyResult = _checkOverlapsAndDeny(
|
|
240
|
+
// Detect overlaps and deny on first tool call, keyed by COPILOT_LOADER_PID.
|
|
241
|
+
const denyResult = _checkOverlapsAndDeny();
|
|
261
242
|
if (denyResult) {
|
|
262
243
|
process.stdout.write(JSON.stringify(denyResult) + "\n");
|
|
263
244
|
return;
|
|
@@ -590,32 +590,54 @@ function markThreadInjected(threadId, sessionId) {
|
|
|
590
590
|
saveInjectedSet(data);
|
|
591
591
|
}
|
|
592
592
|
|
|
593
|
-
// ── Overlap
|
|
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.
|
|
594
597
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
598
|
+
const _overlapFlagPath = () => path.join(_stickyDir(), ".overlap-warned");
|
|
599
|
+
|
|
600
|
+
function _getCopilotPid() {
|
|
601
|
+
return process.env.COPILOT_LOADER_PID || null;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function isOverlapWarned() {
|
|
605
|
+
const pid = _getCopilotPid();
|
|
606
|
+
if (!pid) return false;
|
|
607
|
+
try {
|
|
608
|
+
const data = JSON.parse(fs.readFileSync(_overlapFlagPath(), "utf-8"));
|
|
609
|
+
return !!data[pid];
|
|
610
|
+
} catch (_) {
|
|
599
611
|
return false;
|
|
600
612
|
}
|
|
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
613
|
}
|
|
610
614
|
|
|
611
|
-
function markOverlapWarned(
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
data
|
|
615
|
-
|
|
616
|
-
|
|
615
|
+
function markOverlapWarned() {
|
|
616
|
+
const pid = _getCopilotPid();
|
|
617
|
+
if (!pid) return;
|
|
618
|
+
let data = {};
|
|
619
|
+
try {
|
|
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
|
+
} catch (_) { /* ignore */ }
|
|
617
631
|
}
|
|
618
632
|
|
|
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 (_) {} }
|
|
640
|
+
|
|
619
641
|
// ── Active resumed thread tracking (V2.5) ─────────────────
|
|
620
642
|
|
|
621
643
|
const _activeResumePath = () => path.join(_stickyDir(), ".sticky-active-resume");
|
|
@@ -702,6 +724,11 @@ module.exports = {
|
|
|
702
724
|
// V2.6: overlap warning tracking
|
|
703
725
|
isOverlapWarned,
|
|
704
726
|
markOverlapWarned,
|
|
727
|
+
// V2.6.10: overlap signal file
|
|
728
|
+
writeOverlapSignal,
|
|
729
|
+
readAndDeleteOverlapSignal,
|
|
730
|
+
isOverlapDismissed,
|
|
731
|
+
markOverlapDismissed,
|
|
705
732
|
// V2.5: cross-platform path helper
|
|
706
733
|
normalizeSep,
|
|
707
734
|
};
|