sticky-note-cli 2.6.14 → 2.6.16
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/bin/cli.js +188 -0
- package/package.json +1 -1
- package/templates/hooks/on-error.js +5 -21
- package/templates/hooks/on-stop.js +8 -31
- package/templates/hooks/post-commit +36 -0
- package/templates/hooks/session-end.js +13 -1
- package/templates/hooks/session-start.js +43 -1
- package/templates/hooks/sticky-utils.js +85 -0
- package/templates/hooks/track-work.js +35 -1
- package/templates/sticky-note-config.json +2 -0
package/bin/cli.js
CHANGED
|
@@ -91,6 +91,117 @@ function makeExecutable(filePath) {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
const STICKY_PRE_COMMIT_MARKER = "# [sticky-note:auto-stage]";
|
|
95
|
+
const STICKY_PRE_COMMIT_END = "# [/sticky-note:auto-stage]";
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Install a pre-commit git hook that auto-stages .sticky-note/ tracked files.
|
|
99
|
+
* If an existing pre-commit hook is present, our block is prepended (or updated)
|
|
100
|
+
* so it chains cleanly with linters, husky, etc.
|
|
101
|
+
*/
|
|
102
|
+
function installPreCommitHook() {
|
|
103
|
+
try {
|
|
104
|
+
const gitDir = path.join(process.cwd(), ".git");
|
|
105
|
+
if (!fs.existsSync(gitDir)) return false;
|
|
106
|
+
const hooksDir = path.join(gitDir, "hooks");
|
|
107
|
+
mkdirSafe(hooksDir);
|
|
108
|
+
const dest = path.join(hooksDir, "pre-commit");
|
|
109
|
+
|
|
110
|
+
const stickyBlock = [
|
|
111
|
+
STICKY_PRE_COMMIT_MARKER,
|
|
112
|
+
'# Auto-stage .sticky-note/ files so they ride along with every commit.',
|
|
113
|
+
'_sn_root="$(git rev-parse --show-toplevel 2>/dev/null)"',
|
|
114
|
+
'if [ -d "$_sn_root/.sticky-note" ]; then',
|
|
115
|
+
' [ -f "$_sn_root/.sticky-note/sticky-note.json" ] && git add "$_sn_root/.sticky-note/sticky-note.json" 2>/dev/null || true',
|
|
116
|
+
' [ -f "$_sn_root/.sticky-note/merge-driver.js" ] && git add "$_sn_root/.sticky-note/merge-driver.js" 2>/dev/null || true',
|
|
117
|
+
' [ -d "$_sn_root/.sticky-note/audit" ] && git add "$_sn_root/.sticky-note/audit/" 2>/dev/null || true',
|
|
118
|
+
' [ -d "$_sn_root/.sticky-note/presence" ] && git add "$_sn_root/.sticky-note/presence/" 2>/dev/null || true',
|
|
119
|
+
'fi',
|
|
120
|
+
STICKY_PRE_COMMIT_END,
|
|
121
|
+
].join("\n");
|
|
122
|
+
|
|
123
|
+
if (fs.existsSync(dest)) {
|
|
124
|
+
let existing = fs.readFileSync(dest, "utf-8");
|
|
125
|
+
const startIdx = existing.indexOf(STICKY_PRE_COMMIT_MARKER);
|
|
126
|
+
const endIdx = existing.indexOf(STICKY_PRE_COMMIT_END);
|
|
127
|
+
|
|
128
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
129
|
+
// Update our existing block in-place
|
|
130
|
+
existing = existing.slice(0, startIdx) + stickyBlock + existing.slice(endIdx + STICKY_PRE_COMMIT_END.length);
|
|
131
|
+
} else {
|
|
132
|
+
// Prepend our block after the shebang (or at the top)
|
|
133
|
+
const shebangMatch = existing.match(/^#!.*\n/);
|
|
134
|
+
if (shebangMatch) {
|
|
135
|
+
existing = shebangMatch[0] + stickyBlock + "\n" + existing.slice(shebangMatch[0].length);
|
|
136
|
+
} else {
|
|
137
|
+
existing = "#!/bin/sh\n" + stickyBlock + "\n" + existing;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
fs.writeFileSync(dest, existing, "utf-8");
|
|
141
|
+
} else {
|
|
142
|
+
// No existing hook — create a fresh one
|
|
143
|
+
fs.writeFileSync(dest, "#!/bin/sh\n" + stickyBlock + "\n", "utf-8");
|
|
144
|
+
}
|
|
145
|
+
makeExecutable(dest);
|
|
146
|
+
return true;
|
|
147
|
+
} catch (err) {
|
|
148
|
+
debugLog("installPreCommitHook failed: " + (err.message || err));
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const STICKY_POST_COMMIT_MARKER = "# [sticky-note:auto-sync]";
|
|
154
|
+
const STICKY_POST_COMMIT_END = "# [/sticky-note:auto-sync]";
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Install a post-commit git hook that auto-commits leftover .sticky-note/ changes.
|
|
158
|
+
* Uses the same prepend/update strategy as installPreCommitHook.
|
|
159
|
+
*/
|
|
160
|
+
function installPostCommitHook() {
|
|
161
|
+
try {
|
|
162
|
+
const gitDir = path.join(process.cwd(), ".git");
|
|
163
|
+
if (!fs.existsSync(gitDir)) return false;
|
|
164
|
+
const hooksDir = path.join(gitDir, "hooks");
|
|
165
|
+
mkdirSafe(hooksDir);
|
|
166
|
+
const dest = path.join(hooksDir, "post-commit");
|
|
167
|
+
const src = path.join(TEMPLATES_DIR, "hooks", "post-commit");
|
|
168
|
+
const template = fs.existsSync(src) ? fs.readFileSync(src, "utf-8") : "";
|
|
169
|
+
|
|
170
|
+
// Extract just the body (skip shebang + first comment line from template)
|
|
171
|
+
const bodyLines = template.split("\n").filter(l => !l.startsWith("#!/"));
|
|
172
|
+
const stickyBlock = [
|
|
173
|
+
STICKY_POST_COMMIT_MARKER,
|
|
174
|
+
...bodyLines,
|
|
175
|
+
STICKY_POST_COMMIT_END,
|
|
176
|
+
].join("\n");
|
|
177
|
+
|
|
178
|
+
if (fs.existsSync(dest)) {
|
|
179
|
+
let existing = fs.readFileSync(dest, "utf-8");
|
|
180
|
+
const startIdx = existing.indexOf(STICKY_POST_COMMIT_MARKER);
|
|
181
|
+
const endIdx = existing.indexOf(STICKY_POST_COMMIT_END);
|
|
182
|
+
|
|
183
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
184
|
+
existing = existing.slice(0, startIdx) + stickyBlock + existing.slice(endIdx + STICKY_POST_COMMIT_END.length);
|
|
185
|
+
} else {
|
|
186
|
+
const shebangMatch = existing.match(/^#!.*\n/);
|
|
187
|
+
if (shebangMatch) {
|
|
188
|
+
existing = shebangMatch[0] + stickyBlock + "\n" + existing.slice(shebangMatch[0].length);
|
|
189
|
+
} else {
|
|
190
|
+
existing = "#!/bin/sh\n" + stickyBlock + "\n" + existing;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
fs.writeFileSync(dest, existing, "utf-8");
|
|
194
|
+
} else {
|
|
195
|
+
fs.writeFileSync(dest, "#!/bin/sh\n" + stickyBlock + "\n", "utf-8");
|
|
196
|
+
}
|
|
197
|
+
makeExecutable(dest);
|
|
198
|
+
return true;
|
|
199
|
+
} catch (err) {
|
|
200
|
+
debugLog("installPostCommitHook failed: " + (err.message || err));
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
94
205
|
function installGitHook(hookName) {
|
|
95
206
|
try {
|
|
96
207
|
const gitDir = path.join(process.cwd(), ".git");
|
|
@@ -640,6 +751,16 @@ async function cmdInit() {
|
|
|
640
751
|
print(" [OK] .git/hooks/post-rewrite (attribution survives rebase)");
|
|
641
752
|
}
|
|
642
753
|
|
|
754
|
+
// Install pre-commit hook (auto-stage .sticky-note/ files)
|
|
755
|
+
if (installPreCommitHook()) {
|
|
756
|
+
print(" [OK] .git/hooks/pre-commit (auto-stage .sticky-note/ files)");
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Install post-commit hook (auto-commit leftover .sticky-note/ changes)
|
|
760
|
+
if (installPostCommitHook()) {
|
|
761
|
+
print(" [OK] .git/hooks/post-commit (auto-sync .sticky-note/ after commit)");
|
|
762
|
+
}
|
|
763
|
+
|
|
643
764
|
// Force-add .claude/hooks/ and .claude/settings.json so git tracks them
|
|
644
765
|
// (needed if .claude/ was previously in .gitignore)
|
|
645
766
|
try {
|
|
@@ -710,6 +831,12 @@ function cmdUpdate() {
|
|
|
710
831
|
if (installGitHook("post-rewrite")) {
|
|
711
832
|
print(" [OK] .git/hooks/post-rewrite (attribution survives rebase)");
|
|
712
833
|
}
|
|
834
|
+
if (installPreCommitHook()) {
|
|
835
|
+
print(" [OK] .git/hooks/pre-commit (auto-stage .sticky-note/ files)");
|
|
836
|
+
}
|
|
837
|
+
if (installPostCommitHook()) {
|
|
838
|
+
print(" [OK] .git/hooks/post-commit (auto-sync .sticky-note/ after commit)");
|
|
839
|
+
}
|
|
713
840
|
|
|
714
841
|
// Update hook_version in config
|
|
715
842
|
const configPath = path.join(stickyDir, "sticky-note-config.json");
|
|
@@ -2246,6 +2373,63 @@ function cmdGc() {
|
|
|
2246
2373
|
print("");
|
|
2247
2374
|
}
|
|
2248
2375
|
|
|
2376
|
+
// ──────────────────────────────────────────────
|
|
2377
|
+
// sync — commit and optionally push .sticky-note/ files
|
|
2378
|
+
// ──────────────────────────────────────────────
|
|
2379
|
+
|
|
2380
|
+
function cmdSync() {
|
|
2381
|
+
const args = process.argv.slice(3);
|
|
2382
|
+
const doPush = args.includes("--push");
|
|
2383
|
+
const stickyDir = path.join(process.cwd(), ".sticky-note");
|
|
2384
|
+
if (!fs.existsSync(stickyDir)) {
|
|
2385
|
+
print(" [ERR] No .sticky-note/ directory found. Run 'npx sticky-note init' first.");
|
|
2386
|
+
process.exit(1);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
printBanner();
|
|
2390
|
+
print(" Syncing .sticky-note/ files...\n");
|
|
2391
|
+
|
|
2392
|
+
try {
|
|
2393
|
+
const status = execSync(
|
|
2394
|
+
'git status --porcelain -- ".sticky-note/sticky-note.json" ".sticky-note/audit/" ".sticky-note/presence/"',
|
|
2395
|
+
{ encoding: "utf-8", cwd: process.cwd() }
|
|
2396
|
+
).trim();
|
|
2397
|
+
if (!status) {
|
|
2398
|
+
print(" [OK] Nothing to sync — .sticky-note/ files are clean.");
|
|
2399
|
+
print("");
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
// Write recursion guard
|
|
2404
|
+
const guardPath = path.join(stickyDir, ".sticky-syncing");
|
|
2405
|
+
fs.writeFileSync(guardPath, process.pid.toString(), "utf-8");
|
|
2406
|
+
|
|
2407
|
+
try {
|
|
2408
|
+
execFileSync("git", [
|
|
2409
|
+
"add", "--", ".sticky-note/sticky-note.json",
|
|
2410
|
+
".sticky-note/audit/", ".sticky-note/presence/"
|
|
2411
|
+
], { cwd: process.cwd(), timeout: 10000, stdio: ["pipe", "pipe", "pipe"] });
|
|
2412
|
+
|
|
2413
|
+
execFileSync("git", [
|
|
2414
|
+
"commit", "--no-verify", "-m", "chore(sticky-note): sync thread data"
|
|
2415
|
+
], { cwd: process.cwd(), timeout: 15000, stdio: ["pipe", "pipe", "pipe"] });
|
|
2416
|
+
print(" ✅ Committed .sticky-note/ changes.");
|
|
2417
|
+
|
|
2418
|
+
if (doPush) {
|
|
2419
|
+
execFileSync("git", ["push"], {
|
|
2420
|
+
cwd: process.cwd(), timeout: 30000, stdio: ["pipe", "pipe", "pipe"],
|
|
2421
|
+
});
|
|
2422
|
+
print(" ✅ Pushed to remote.");
|
|
2423
|
+
}
|
|
2424
|
+
} finally {
|
|
2425
|
+
try { fs.unlinkSync(guardPath); } catch (_) {}
|
|
2426
|
+
}
|
|
2427
|
+
} catch (err) {
|
|
2428
|
+
print(" ⚠️ Sync failed: " + (err.message || err));
|
|
2429
|
+
}
|
|
2430
|
+
print("");
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2249
2433
|
// ──────────────────────────────────────────────
|
|
2250
2434
|
// Main
|
|
2251
2435
|
// ──────────────────────────────────────────────
|
|
@@ -2300,6 +2484,9 @@ async function main() {
|
|
|
2300
2484
|
case "claim":
|
|
2301
2485
|
cmdClaim();
|
|
2302
2486
|
break;
|
|
2487
|
+
case "sync":
|
|
2488
|
+
cmdSync();
|
|
2489
|
+
break;
|
|
2303
2490
|
case "--version":
|
|
2304
2491
|
case "-v":
|
|
2305
2492
|
print(`sticky-note v${VERSION}`);
|
|
@@ -2325,6 +2512,7 @@ async function main() {
|
|
|
2325
2512
|
print(" checkpoint Set work-topic checkpoint for attribution (--topic, --show, --clear)");
|
|
2326
2513
|
print(" overlap Detect file overlaps with open/stuck threads");
|
|
2327
2514
|
print(" claim Claim ownership of files you're working on (--list, --clear)");
|
|
2515
|
+
print(" sync Commit .sticky-note/ changes to git (--push to also push)");
|
|
2328
2516
|
print("");
|
|
2329
2517
|
print(" Options:");
|
|
2330
2518
|
print(" --version Show version");
|
package/package.json
CHANGED
|
@@ -88,27 +88,10 @@ function main() {
|
|
|
88
88
|
if (existing.failed_approaches.length > 5) {
|
|
89
89
|
existing.failed_approaches = existing.failed_approaches.slice(-5);
|
|
90
90
|
}
|
|
91
|
-
} else {
|
|
92
|
-
const thread = {
|
|
93
|
-
id: crypto.randomUUID(),
|
|
94
|
-
user: user,
|
|
95
|
-
project: memory.project || "",
|
|
96
|
-
status: "stuck",
|
|
97
|
-
branch: "",
|
|
98
|
-
created_at: now,
|
|
99
|
-
closed_at: null,
|
|
100
|
-
last_activity_at: now,
|
|
101
|
-
files_touched: [],
|
|
102
|
-
last_note: errorMsg,
|
|
103
|
-
narrative: "",
|
|
104
|
-
failed_approaches: [{ description: errorMsg.substring(0, 150), error: errorMsg.substring(0, 100) }],
|
|
105
|
-
handoff_summary: "",
|
|
106
|
-
related_session_ids: [],
|
|
107
|
-
tool: toolName,
|
|
108
|
-
session_id: sessionId,
|
|
109
|
-
};
|
|
110
|
-
threads.push(thread);
|
|
111
91
|
}
|
|
92
|
+
// If no thread exists for this session, skip — session-start.js creates the
|
|
93
|
+
// thread now. Creating standalone "stuck" threads from tool failures produced
|
|
94
|
+
// garbage entries (tool="Bash"/"Glob", no branch, no context).
|
|
112
95
|
|
|
113
96
|
appendAuditLine({
|
|
114
97
|
type: "error",
|
|
@@ -131,6 +114,7 @@ function main() {
|
|
|
131
114
|
|
|
132
115
|
try {
|
|
133
116
|
main();
|
|
134
|
-
} catch (
|
|
117
|
+
} catch (err) {
|
|
118
|
+
try { utils.logHookError("on-error", err); } catch (_) {}
|
|
135
119
|
_safeExit();
|
|
136
120
|
}
|
|
@@ -217,7 +217,7 @@ async function main() {
|
|
|
217
217
|
let found = false;
|
|
218
218
|
|
|
219
219
|
for (const thread of threads) {
|
|
220
|
-
if (thread.session_id === sessionId
|
|
220
|
+
if (thread.session_id === sessionId) {
|
|
221
221
|
thread.last_activity_at = now;
|
|
222
222
|
// Enrich with extracted data
|
|
223
223
|
if (allFiles.length) {
|
|
@@ -250,35 +250,9 @@ async function main() {
|
|
|
250
250
|
break;
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const thread = {
|
|
257
|
-
id: crypto.randomUUID(),
|
|
258
|
-
user,
|
|
259
|
-
project: memory.project || "",
|
|
260
|
-
status: "closed",
|
|
261
|
-
branch: getBranch(),
|
|
262
|
-
created_at: now,
|
|
263
|
-
closed_at: now,
|
|
264
|
-
last_activity_at: now,
|
|
265
|
-
files_touched: allFiles,
|
|
266
|
-
last_note: reason ? reason.substring(0, 200) : (narrative || "Session stopped"),
|
|
267
|
-
narrative,
|
|
268
|
-
failed_approaches: [],
|
|
269
|
-
handoff_summary: "",
|
|
270
|
-
related_session_ids: [],
|
|
271
|
-
tool: detectTool(hookInput),
|
|
272
|
-
session_id: sessionId,
|
|
273
|
-
work_type: workType,
|
|
274
|
-
tool_calls: auditData.tools || {},
|
|
275
|
-
prompts: storedPrompts,
|
|
276
|
-
contributors: [user],
|
|
277
|
-
resume_history: [],
|
|
278
|
-
};
|
|
279
|
-
thread.handoff_summary = buildHandoffSummary(thread, reason);
|
|
280
|
-
threads.push(thread);
|
|
281
|
-
}
|
|
253
|
+
// If no thread exists for this session, skip — session-start.js creates the
|
|
254
|
+
// thread now. Creating standalone threads from Stop events produced duplicates
|
|
255
|
+
// when session-end.js had already closed the thread (status filter mismatch).
|
|
282
256
|
|
|
283
257
|
const auditEntry = {
|
|
284
258
|
type: "stop",
|
|
@@ -310,4 +284,7 @@ async function main() {
|
|
|
310
284
|
process.exit(0);
|
|
311
285
|
}
|
|
312
286
|
|
|
313
|
-
main().catch(() =>
|
|
287
|
+
main().catch((err) => {
|
|
288
|
+
try { utils.logHookError("on-stop", err); } catch (_) {}
|
|
289
|
+
safeExit();
|
|
290
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Sticky Note — post-commit hook
|
|
3
|
+
# Auto-commits .sticky-note/ changes that weren't included in the user's commit.
|
|
4
|
+
# Installed by `npx sticky-note init`. Safe to remove if you prefer manual sync.
|
|
5
|
+
|
|
6
|
+
_sn_root="$(git rev-parse --show-toplevel 2>/dev/null)"
|
|
7
|
+
[ -z "$_sn_root" ] && exit 0
|
|
8
|
+
|
|
9
|
+
# Recursion guard — if we're already syncing, bail out
|
|
10
|
+
_sn_guard="$_sn_root/.sticky-note/.sticky-syncing"
|
|
11
|
+
[ -f "$_sn_guard" ] && exit 0
|
|
12
|
+
|
|
13
|
+
# Check for dirty .sticky-note/ files (unstaged changes that weren't in the commit)
|
|
14
|
+
_sn_dirty="$(git status --porcelain -- \
|
|
15
|
+
"$_sn_root/.sticky-note/sticky-note.json" \
|
|
16
|
+
"$_sn_root/.sticky-note/audit/" \
|
|
17
|
+
"$_sn_root/.sticky-note/presence/" 2>/dev/null)"
|
|
18
|
+
[ -z "$_sn_dirty" ] && exit 0
|
|
19
|
+
|
|
20
|
+
# Write guard file
|
|
21
|
+
echo $$ > "$_sn_guard"
|
|
22
|
+
|
|
23
|
+
# Stage and commit
|
|
24
|
+
git add -- \
|
|
25
|
+
"$_sn_root/.sticky-note/sticky-note.json" \
|
|
26
|
+
"$_sn_root/.sticky-note/audit/" \
|
|
27
|
+
"$_sn_root/.sticky-note/presence/" 2>/dev/null
|
|
28
|
+
|
|
29
|
+
git commit --no-verify -m "chore(sticky-note): sync thread data" -- \
|
|
30
|
+
"$_sn_root/.sticky-note/sticky-note.json" \
|
|
31
|
+
"$_sn_root/.sticky-note/audit/" \
|
|
32
|
+
"$_sn_root/.sticky-note/presence/" 2>/dev/null
|
|
33
|
+
|
|
34
|
+
# Clean up guard
|
|
35
|
+
rm -f "$_sn_guard" 2>/dev/null
|
|
36
|
+
exit 0
|
|
@@ -58,6 +58,7 @@ const {
|
|
|
58
58
|
clearActiveResumeThreadId,
|
|
59
59
|
clearInjectedSet,
|
|
60
60
|
normalizeSep,
|
|
61
|
+
syncStickyNote,
|
|
61
62
|
} = utils;
|
|
62
63
|
|
|
63
64
|
// ── Constants ─────────────────────────────────────────────
|
|
@@ -935,6 +936,16 @@ function main() {
|
|
|
935
936
|
}
|
|
936
937
|
saveMemoryMerged(memoryPath, memory);
|
|
937
938
|
|
|
939
|
+
// Auto-sync: commit (and optionally push) .sticky-note/ changes
|
|
940
|
+
const config = loadJson(getConfigPath(), {});
|
|
941
|
+
if (config.auto_sync !== false) {
|
|
942
|
+
try {
|
|
943
|
+
syncStickyNote({ push: config.auto_push === true });
|
|
944
|
+
} catch (_) {
|
|
945
|
+
// Non-fatal — sync failure shouldn't block session-end
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
938
949
|
const fileCount = filesTouched.length;
|
|
939
950
|
const commitCount = commitShas.length;
|
|
940
951
|
const statusLabel = isCopilotCli ? "updated" : "closed";
|
|
@@ -949,6 +960,7 @@ function main() {
|
|
|
949
960
|
|
|
950
961
|
try {
|
|
951
962
|
main();
|
|
952
|
-
} catch (
|
|
963
|
+
} catch (err) {
|
|
964
|
+
try { utils.logHookError("session-end", err); } catch (_) {}
|
|
953
965
|
_safeExit();
|
|
954
966
|
}
|
|
@@ -50,6 +50,7 @@ const {
|
|
|
50
50
|
saveMemoryMerged,
|
|
51
51
|
appendAuditLine,
|
|
52
52
|
getUser,
|
|
53
|
+
getBranch,
|
|
53
54
|
getSessionId,
|
|
54
55
|
getResumeThreadId,
|
|
55
56
|
findThreadById,
|
|
@@ -493,6 +494,46 @@ function main() {
|
|
|
493
494
|
}
|
|
494
495
|
}
|
|
495
496
|
|
|
497
|
+
// ── Create "open" thread for this session ──────────────
|
|
498
|
+
// Ensures a thread exists even if SessionEnd never fires (Ctrl+C, crash).
|
|
499
|
+
// session-end.js, on-stop.js, and on-error.js will find and update this
|
|
500
|
+
// thread by session_id. Copilot CLI fires SessionStart per-turn, so skip
|
|
501
|
+
// creation on subsequent turns (thread already exists from the first turn).
|
|
502
|
+
if (!resumedThread) {
|
|
503
|
+
const threads = (memory.threads || []).filter(Boolean);
|
|
504
|
+
memory.threads = threads;
|
|
505
|
+
const existingForSession = threads.find((t) => t.session_id === sessionId);
|
|
506
|
+
if (!existingForSession) {
|
|
507
|
+
const user = getUser();
|
|
508
|
+
const branch = getBranch();
|
|
509
|
+
threads.push({
|
|
510
|
+
id: crypto.randomUUID(),
|
|
511
|
+
user,
|
|
512
|
+
project: memory.project || "",
|
|
513
|
+
status: "open",
|
|
514
|
+
branch,
|
|
515
|
+
created_at: new Date().toISOString(),
|
|
516
|
+
closed_at: null,
|
|
517
|
+
last_activity_at: new Date().toISOString(),
|
|
518
|
+
files_touched: [],
|
|
519
|
+
last_note: "",
|
|
520
|
+
narrative: "",
|
|
521
|
+
failed_approaches: [],
|
|
522
|
+
handoff_summary: "",
|
|
523
|
+
related_session_ids: [],
|
|
524
|
+
resume_chain: [],
|
|
525
|
+
tool: aiTool,
|
|
526
|
+
session_id: sessionId,
|
|
527
|
+
work_type: "",
|
|
528
|
+
activities: [],
|
|
529
|
+
tool_calls: {},
|
|
530
|
+
prompts: [],
|
|
531
|
+
contributors: [user],
|
|
532
|
+
resume_history: [],
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
496
537
|
// ── Build context pieces ──────────────────────────────
|
|
497
538
|
const threadResult = formatThreadsForInjection((memory.threads || []).filter(Boolean));
|
|
498
539
|
const threadContext = threadResult.text;
|
|
@@ -618,6 +659,7 @@ function main() {
|
|
|
618
659
|
|
|
619
660
|
try {
|
|
620
661
|
main();
|
|
621
|
-
} catch (
|
|
662
|
+
} catch (err) {
|
|
663
|
+
try { utils.logHookError("session-start", err); } catch (_) {}
|
|
622
664
|
_safeExit();
|
|
623
665
|
}
|
|
@@ -42,6 +42,21 @@ function _stickyDir() {
|
|
|
42
42
|
return path.join(scriptDir, "..", "..", ".sticky-note");
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// ── Error logging ─────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
function logHookError(hookName, error) {
|
|
48
|
+
try {
|
|
49
|
+
const logPath = path.join(_stickyDir(), ".sticky-errors.log");
|
|
50
|
+
const ts = new Date().toISOString();
|
|
51
|
+
const msg = error instanceof Error ? error.stack || error.message : String(error);
|
|
52
|
+
const line = `[${ts}] ${hookName}: ${msg}\n`;
|
|
53
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
54
|
+
fs.appendFileSync(logPath, line, "utf-8");
|
|
55
|
+
} catch (_) {
|
|
56
|
+
// Last resort — can't even log the error
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
45
60
|
const _paths = {
|
|
46
61
|
memory: "sticky-note.json",
|
|
47
62
|
config: "sticky-note-config.json",
|
|
@@ -125,6 +140,73 @@ function clearSessionFile() {
|
|
|
125
140
|
}
|
|
126
141
|
}
|
|
127
142
|
|
|
143
|
+
// ── Git sync (auto-commit/push .sticky-note/ files) ──────
|
|
144
|
+
|
|
145
|
+
function _gitRepoRoot() {
|
|
146
|
+
try {
|
|
147
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
148
|
+
encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
|
|
149
|
+
}).trim();
|
|
150
|
+
} catch (_) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Stage, commit, and optionally push .sticky-note/ files.
|
|
157
|
+
* Returns { committed: bool, pushed: bool, skipped: bool, error?: string }
|
|
158
|
+
*/
|
|
159
|
+
function syncStickyNote(opts) {
|
|
160
|
+
const push = opts && opts.push;
|
|
161
|
+
const result = { committed: false, pushed: false, skipped: false };
|
|
162
|
+
try {
|
|
163
|
+
const root = _gitRepoRoot();
|
|
164
|
+
if (!root) { result.skipped = true; return result; }
|
|
165
|
+
|
|
166
|
+
const syncGuard = path.join(_stickyDir(), ".sticky-syncing");
|
|
167
|
+
if (fs.existsSync(syncGuard)) { result.skipped = true; return result; }
|
|
168
|
+
|
|
169
|
+
// Check for dirty .sticky-note/ files (staged or unstaged)
|
|
170
|
+
const status = execFileSync("git", [
|
|
171
|
+
"status", "--porcelain", "--", ".sticky-note/sticky-note.json",
|
|
172
|
+
".sticky-note/audit/", ".sticky-note/presence/"
|
|
173
|
+
], { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
174
|
+
|
|
175
|
+
if (!status) { result.skipped = true; return result; }
|
|
176
|
+
|
|
177
|
+
// Write guard to prevent recursive post-commit triggers
|
|
178
|
+
fs.writeFileSync(syncGuard, process.pid.toString(), "utf-8");
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
// Stage the relevant files
|
|
182
|
+
execFileSync("git", [
|
|
183
|
+
"add", "--", ".sticky-note/sticky-note.json",
|
|
184
|
+
".sticky-note/audit/", ".sticky-note/presence/"
|
|
185
|
+
], { timeout: 10000, stdio: ["pipe", "pipe", "pipe"] });
|
|
186
|
+
|
|
187
|
+
// Commit only what was staged
|
|
188
|
+
execFileSync("git", [
|
|
189
|
+
"commit", "-m", "chore(sticky-note): sync thread data", "--no-verify"
|
|
190
|
+
], { timeout: 15000, stdio: ["pipe", "pipe", "pipe"] });
|
|
191
|
+
result.committed = true;
|
|
192
|
+
|
|
193
|
+
// Push if requested
|
|
194
|
+
if (push) {
|
|
195
|
+
execFileSync("git", ["push"], {
|
|
196
|
+
timeout: 30000, stdio: ["pipe", "pipe", "pipe"],
|
|
197
|
+
});
|
|
198
|
+
result.pushed = true;
|
|
199
|
+
}
|
|
200
|
+
} finally {
|
|
201
|
+
try { fs.unlinkSync(syncGuard); } catch (_) {}
|
|
202
|
+
}
|
|
203
|
+
} catch (err) {
|
|
204
|
+
result.error = err.message;
|
|
205
|
+
logHookError("syncStickyNote", err);
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
|
|
128
210
|
// ── Git HEAD snapshot (for files_touched fallback) ────────
|
|
129
211
|
|
|
130
212
|
function saveHeadSha() {
|
|
@@ -671,6 +753,7 @@ module.exports = {
|
|
|
671
753
|
ERROR_PATTERNS,
|
|
672
754
|
RETRY_PATTERNS,
|
|
673
755
|
FILE_PATH_PATTERN,
|
|
756
|
+
logHookError,
|
|
674
757
|
getMemoryPath,
|
|
675
758
|
getConfigPath,
|
|
676
759
|
getAuditPath,
|
|
@@ -731,5 +814,7 @@ module.exports = {
|
|
|
731
814
|
markOverlapDismissed,
|
|
732
815
|
// V2.5: cross-platform path helper
|
|
733
816
|
normalizeSep,
|
|
817
|
+
// V2.7: auto-sync
|
|
818
|
+
syncStickyNote,
|
|
734
819
|
};
|
|
735
820
|
// tier1 test
|
|
@@ -37,12 +37,16 @@ try {
|
|
|
37
37
|
|
|
38
38
|
const {
|
|
39
39
|
getConfigPath,
|
|
40
|
+
getMemoryPath,
|
|
40
41
|
getUserPresencePath,
|
|
41
42
|
loadJson,
|
|
42
43
|
saveJson,
|
|
44
|
+
saveMemoryMerged,
|
|
43
45
|
appendAuditLine,
|
|
44
46
|
getUser,
|
|
47
|
+
getBranch,
|
|
45
48
|
getSessionId,
|
|
49
|
+
normalizeSep,
|
|
46
50
|
} = utils;
|
|
47
51
|
|
|
48
52
|
const WRITE_TOOLS = new Set([
|
|
@@ -302,6 +306,35 @@ function main() {
|
|
|
302
306
|
}
|
|
303
307
|
}
|
|
304
308
|
|
|
309
|
+
// ── Progressive thread update ────────────────────────────
|
|
310
|
+
// Update the session's open thread with files_touched and branch changes
|
|
311
|
+
// so data survives even if SessionEnd never fires (Ctrl+C, crash).
|
|
312
|
+
if (filePath) {
|
|
313
|
+
try {
|
|
314
|
+
const memoryPath = getMemoryPath();
|
|
315
|
+
const memory = loadJson(memoryPath, { version: "2", project: "", threads: [] });
|
|
316
|
+
const threads = (memory.threads || []).filter(Boolean);
|
|
317
|
+
const sessionThread = threads.find((t) => t.session_id === sessionId);
|
|
318
|
+
if (sessionThread) {
|
|
319
|
+
const normalized = normalizeSep(filePath);
|
|
320
|
+
const touched = sessionThread.files_touched || [];
|
|
321
|
+
if (!touched.includes(normalized)) {
|
|
322
|
+
touched.push(normalized);
|
|
323
|
+
sessionThread.files_touched = touched;
|
|
324
|
+
}
|
|
325
|
+
// Update branch if it changed mid-session
|
|
326
|
+
const currentBranch = getBranch();
|
|
327
|
+
if (currentBranch && currentBranch !== sessionThread.branch) {
|
|
328
|
+
sessionThread.branch = currentBranch;
|
|
329
|
+
}
|
|
330
|
+
sessionThread.last_activity_at = now;
|
|
331
|
+
saveMemoryMerged(memoryPath, memory);
|
|
332
|
+
}
|
|
333
|
+
} catch (_) {
|
|
334
|
+
// Best-effort — don't break the hook if thread update fails
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
305
338
|
// Build status message for transparency
|
|
306
339
|
const linePart = lineRanges && lineRanges.length > 0 ? ` (lines ${lineRanges.map(r => `${r.start}-${r.end}`).join(", ")})` : "";
|
|
307
340
|
const statusMsg = `[STICKY-NOTE] Tracked ${toolName}${filePath ? " on " + filePath : ""}${linePart}`;
|
|
@@ -316,6 +349,7 @@ function main() {
|
|
|
316
349
|
|
|
317
350
|
try {
|
|
318
351
|
main();
|
|
319
|
-
} catch (
|
|
352
|
+
} catch (err) {
|
|
353
|
+
try { utils.logHookError("track-work", err); } catch (_) {}
|
|
320
354
|
safeExit();
|
|
321
355
|
}
|