openwolf-enhanced 1.26.0 → 1.28.0
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/CHANGELOG.md +306 -0
- package/dist/dashboard/assets/{AISuggestions-DZmzksSA.js → AISuggestions-C7WFvzLO.js} +1 -1
- package/dist/dashboard/assets/{ActivityLog-BJ0omuzu.js → ActivityLog-SsRbpBFt.js} +1 -1
- package/dist/dashboard/assets/{ActivityTimeline-BWhxretI.js → ActivityTimeline-BoV30nDY.js} +1 -1
- package/dist/dashboard/assets/{AnatomyBrowser-DNVp6-_r.js → AnatomyBrowser-DwzsHn4z.js} +1 -1
- package/dist/dashboard/assets/{BugLog-DMJTuU9p.js → BugLog-CPl4Q-JG.js} +1 -1
- package/dist/dashboard/assets/{CerebrumViewer-BwcGr6pu.js → CerebrumViewer-_f6fBsNF.js} +1 -1
- package/dist/dashboard/assets/{CronStatus-Dwf_M4-S.js → CronStatus-1iBEPJK5.js} +1 -1
- package/dist/dashboard/assets/{DesignQC-DzHkh94I.js → DesignQC-D4UU7lca.js} +1 -1
- package/dist/dashboard/assets/{LinkGraph-kqQD--Yj.js → LinkGraph-BWtI8Gh3.js} +1 -1
- package/dist/dashboard/assets/{MemoryViewer-BCn9Fr-w.js → MemoryViewer-BYRVG34g.js} +1 -1
- package/dist/dashboard/assets/{NativeMemory-DPMZaP5E.js → NativeMemory-Dphtrqjt.js} +1 -1
- package/dist/dashboard/assets/{ProjectOverview-PWENJiC4.js → ProjectOverview-CTYohvjD.js} +1 -1
- package/dist/dashboard/assets/{ProjectsAggregate-Baj44DVk.js → ProjectsAggregate-CMxNs35H.js} +1 -1
- package/dist/dashboard/assets/{StatusBadge-Db61D51H.js → StatusBadge-CjrFCDOI.js} +1 -1
- package/dist/dashboard/assets/{TokenUsage-UefDh9w4.js → TokenUsage-C5wJpMl8.js} +1 -1
- package/dist/dashboard/assets/index-BNi8lsJE.css +2 -0
- package/dist/dashboard/assets/{index-e2PfOI0b.js → index-C74qneS9.js} +5 -5
- package/dist/dashboard/index.html +2 -2
- package/dist/hooks/post-bash.js +10 -5
- package/dist/hooks/post-read.js +24 -26
- package/dist/hooks/post-write.js +135 -118
- package/dist/hooks/pre-read.js +42 -36
- package/dist/hooks/pre-write.js +3 -3
- package/dist/hooks/precompact.js +2 -2
- package/dist/hooks/session-start.js +15 -6
- package/dist/hooks/shared.js +221 -14
- package/dist/hooks/stop.js +33 -7
- package/dist/src/cli/cron-cmd.js +2 -2
- package/dist/src/cli/cron-cmd.js.map +1 -1
- package/dist/src/cli/daemon-cmd.js +49 -21
- package/dist/src/cli/daemon-cmd.js.map +1 -1
- package/dist/src/cli/doctor-cmd.js +2 -1
- package/dist/src/cli/doctor-cmd.js.map +1 -1
- package/dist/src/cli/export-cmd.js +8 -0
- package/dist/src/cli/export-cmd.js.map +1 -1
- package/dist/src/cli/init.js +34 -8
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/registry.js +68 -33
- package/dist/src/cli/registry.js.map +1 -1
- package/dist/src/cli/update.js +37 -6
- package/dist/src/cli/update.js.map +1 -1
- package/dist/src/daemon/cron-engine.js +70 -8
- package/dist/src/daemon/cron-engine.js.map +1 -1
- package/dist/src/daemon/file-watcher.js +4 -1
- package/dist/src/daemon/file-watcher.js.map +1 -1
- package/dist/src/daemon/llm-provider.js +9 -6
- package/dist/src/daemon/llm-provider.js.map +1 -1
- package/dist/src/daemon/wolf-daemon.js +69 -15
- package/dist/src/daemon/wolf-daemon.js.map +1 -1
- package/dist/src/designqc/designqc-capture.js +116 -6
- package/dist/src/designqc/designqc-capture.js.map +1 -1
- package/dist/src/designqc/designqc-engine.js +9 -3
- package/dist/src/designqc/designqc-engine.js.map +1 -1
- package/dist/src/hooks/post-bash.js +10 -5
- package/dist/src/hooks/post-bash.js.map +1 -1
- package/dist/src/hooks/post-read.js +24 -26
- package/dist/src/hooks/post-read.js.map +1 -1
- package/dist/src/hooks/post-write.js +135 -118
- package/dist/src/hooks/post-write.js.map +1 -1
- package/dist/src/hooks/pre-read.js +42 -36
- package/dist/src/hooks/pre-read.js.map +1 -1
- package/dist/src/hooks/pre-write.js +3 -3
- package/dist/src/hooks/pre-write.js.map +1 -1
- package/dist/src/hooks/precompact.js +2 -2
- package/dist/src/hooks/precompact.js.map +1 -1
- package/dist/src/hooks/session-start.js +15 -6
- package/dist/src/hooks/session-start.js.map +1 -1
- package/dist/src/hooks/shared.js +221 -14
- package/dist/src/hooks/shared.js.map +1 -1
- package/dist/src/hooks/stop.js +33 -7
- package/dist/src/hooks/stop.js.map +1 -1
- package/dist/src/scanner/anatomy-scanner.js +7 -3
- package/dist/src/scanner/anatomy-scanner.js.map +1 -1
- package/dist/src/utils/agent-hooks.js +50 -4
- package/dist/src/utils/agent-hooks.js.map +1 -1
- package/dist/src/utils/daemon-pid.js +56 -0
- package/dist/src/utils/daemon-pid.js.map +1 -0
- package/dist/src/utils/dashboard-auth.js +13 -1
- package/dist/src/utils/dashboard-auth.js.map +1 -1
- package/dist/src/utils/embeddings.js +9 -1
- package/dist/src/utils/embeddings.js.map +1 -1
- package/dist/src/utils/fs-safe.js +69 -11
- package/dist/src/utils/fs-safe.js.map +1 -1
- package/dist/src/utils/logger.js +21 -6
- package/dist/src/utils/logger.js.map +1 -1
- package/dist/src/utils/maintenance.js +100 -6
- package/dist/src/utils/maintenance.js.map +1 -1
- package/dist/src/utils/platform.js +39 -0
- package/dist/src/utils/platform.js.map +1 -1
- package/dist/src/utils/remote.js +2 -2
- package/dist/src/utils/remote.js.map +1 -1
- package/package.json +1 -1
- package/src/templates/config.json +34 -9
- package/dist/dashboard/assets/index-CVfvTpSA.css +0 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pre-write.js","sourceRoot":"","sources":["../../../src/hooks/pre-write.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"pre-write.js","sourceRoot":"","sources":["../../../src/hooks/pre-write.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readStdin, timestamp } from "./shared.js";
|
|
2
|
+
import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readStdin, timestamp, sessionFileFor } from "./shared.js";
|
|
3
3
|
// PreCompact hook — compaction survival.
|
|
4
4
|
//
|
|
5
5
|
// Fires just before Claude Code / Codex compacts the context window. The in-flight session state
|
|
@@ -17,7 +17,7 @@ async function main() {
|
|
|
17
17
|
}
|
|
18
18
|
catch { }
|
|
19
19
|
try {
|
|
20
|
-
const session = readJSON(
|
|
20
|
+
const session = readJSON(sessionFileFor(hooksDir, input.session_id), {});
|
|
21
21
|
writeJSON(path.join(hooksDir, "_precompact-snapshot.json"), {
|
|
22
22
|
at: timestamp(),
|
|
23
23
|
trigger: input.trigger ?? "unknown",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"precompact.js","sourceRoot":"","sources":["../../../src/hooks/precompact.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"precompact.js","sourceRoot":"","sources":["../../../src/hooks/precompact.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEnH,yCAAyC;AACzC,EAAE;AACF,iGAAiG;AACjG,8FAA8F;AAC9F,6FAA6F;AAC7F,2FAA2F;AAC3F,wEAAwE;AAExE,KAAK,UAAU,IAAI;IACjB,aAAa,EAAE,CAAC;IAChB,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAE7C,IAAI,KAAK,GAA8C,EAAE,CAAC;IAC1D,IAAI,CAAC;QACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,SAAS,EAAE,CAAC,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IAEV,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,QAAQ,CAA0B,cAAc,CAAC,QAAQ,EAAE,KAAK,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC;QAClG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,2BAA2B,CAAC,EAAE;YAC1D,EAAE,EAAE,SAAS,EAAE;YACf,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,SAAS;YACnC,OAAO;SACR,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IAEV,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { getWolfDir, ensureWolfDir, writeJSON, appendMarkdown, readJSON, timestamp, timeShort, buildResumeDigest, readStdin,
|
|
3
|
+
import { getWolfDir, ensureWolfDir, writeJSON, appendMarkdown, readJSON, timestamp, timeShort, buildResumeDigest, readStdin, tryWithLock, sessionFileFor, pruneOldSessionFiles, SESSION_SUMMARY_SCAFFOLD, bookInjection } from "./shared.js";
|
|
4
4
|
import { estimateTokens, getTokenRatios } from "./token-estimator.js";
|
|
5
5
|
async function main() {
|
|
6
6
|
ensureWolfDir();
|
|
@@ -19,7 +19,6 @@ async function main() {
|
|
|
19
19
|
}
|
|
20
20
|
catch { }
|
|
21
21
|
const hooksDir = path.join(wolfDir, "hooks");
|
|
22
|
-
const sessionFile = path.join(hooksDir, "_session.json");
|
|
23
22
|
const now = new Date();
|
|
24
23
|
const sessionId = `session-${now.toISOString().slice(0, 10)}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
|
|
25
24
|
// SessionStart fires for startup/resume/clear/compact. Only startup and clear begin a genuinely
|
|
@@ -27,16 +26,26 @@ async function main() {
|
|
|
27
26
|
// would wipe this session's read/write tracking mid-flight and append a spurious memory header
|
|
28
27
|
// on every compaction (compaction survival).
|
|
29
28
|
let source = "startup";
|
|
29
|
+
let harnessSessionId;
|
|
30
30
|
try {
|
|
31
31
|
const hookInput = JSON.parse(await readStdin());
|
|
32
32
|
if (typeof hookInput.source === "string")
|
|
33
33
|
source = hookInput.source;
|
|
34
|
+
if (typeof hookInput.session_id === "string")
|
|
35
|
+
harnessSessionId = hookInput.session_id;
|
|
34
36
|
}
|
|
35
37
|
catch { }
|
|
38
|
+
// One state file per session (see sessionFileFor). Two agents in the same project used to share
|
|
39
|
+
// one, so the second one's startup wiped the first one's in-flight tracking.
|
|
40
|
+
const sessionFile = sessionFileFor(hooksDir, harnessSessionId);
|
|
41
|
+
// One file per session accumulates; a finished session is already folded into the ledger.
|
|
42
|
+
pruneOldSessionFiles(hooksDir);
|
|
36
43
|
const continuing = (source === "compact" || source === "resume") && fs.existsSync(sessionFile);
|
|
37
44
|
if (!continuing) {
|
|
38
|
-
// Create fresh session state
|
|
39
|
-
|
|
45
|
+
// Create fresh session state. Under the lock like every other writer of this file: the reset is
|
|
46
|
+
// a whole-file replacement, so an unlocked write here could land in the middle of another
|
|
47
|
+
// hook's read-modify-write and hand it a half-written file to parse.
|
|
48
|
+
tryWithLock(sessionFile, () => writeJSON(sessionFile, {
|
|
40
49
|
session_id: sessionId,
|
|
41
50
|
started: timestamp(),
|
|
42
51
|
files_read: {},
|
|
@@ -47,7 +56,7 @@ async function main() {
|
|
|
47
56
|
repeated_reads_warned: 0,
|
|
48
57
|
cerebrum_warnings: 0,
|
|
49
58
|
stop_count: 0,
|
|
50
|
-
});
|
|
59
|
+
}));
|
|
51
60
|
// Append session header to memory.md
|
|
52
61
|
const memoryPath = path.join(wolfDir, "memory.md");
|
|
53
62
|
const header = `\n## Session: ${now.toISOString().slice(0, 10)} ${timeShort()}\n\n${SESSION_SUMMARY_SCAFFOLD}\n\n| Time | Action | File(s) | Outcome | ~Tokens |\n|------|--------|---------|---------|--------|\n`;
|
|
@@ -87,7 +96,7 @@ async function main() {
|
|
|
87
96
|
// Same lock as the stop hook's ledger write — a session ending in one process while another
|
|
88
97
|
// starts must not clobber the read-modify-write.
|
|
89
98
|
const ledgerPath = path.join(wolfDir, "token-ledger.json");
|
|
90
|
-
|
|
99
|
+
tryWithLock(ledgerPath, () => {
|
|
91
100
|
const ledger = readJSON(ledgerPath, { version: 1, lifetime: { total_sessions: 0 } });
|
|
92
101
|
ledger.lifetime.total_sessions++;
|
|
93
102
|
writeJSON(ledgerPath, ledger);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-start.js","sourceRoot":"","sources":["../../../src/hooks/session-start.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,iBAAiB,EAAE,SAAS,EAAE,
|
|
1
|
+
{"version":3,"file":"session-start.js","sourceRoot":"","sources":["../../../src/hooks/session-start.ts"],"names":[],"mappings":"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"}
|
package/dist/src/hooks/shared.js
CHANGED
|
@@ -74,10 +74,31 @@ export function ensureWolfDir() {
|
|
|
74
74
|
process.exit(0);
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
77
|
+
// Advisory lock around a read-modify-write cycle so concurrent hook/daemon processes don't clobber
|
|
78
|
+
// each other's updates to the same file (M1). It waits, steals a lock left behind by a dead process
|
|
79
|
+
// (>5s old), and — if it still cannot acquire — FAILS instead of proceeding.
|
|
80
|
+
//
|
|
81
|
+
// [2026-08-31] It used to run the callback unlocked after the wait expired, which defeats the lock
|
|
82
|
+
// for every writer that respects it: the whole point is that exactly one process is inside the
|
|
83
|
+
// critical section, and a participant that proceeds anyway makes the guarantee vacuous for
|
|
84
|
+
// everybody else. Under contention that is not a rare fallback, it is the common path — contention
|
|
85
|
+
// is precisely when the timeout fires. Failing closed loses one update; proceeding unlocked loses
|
|
86
|
+
// somebody else's, and can hand a half-written file to the next reader.
|
|
87
|
+
//
|
|
88
|
+
// The poll also started at a flat 25ms. With seven hooks around one file, a herd needs several
|
|
89
|
+
// rounds to drain and each waiter burned 25ms per attempt whether or not the lock was free a
|
|
90
|
+
// microsecond later. It now starts at 2ms and backs off, so the common case (a lock held for the
|
|
91
|
+
// microseconds of one write) is picked up almost immediately and a herd drains inside the budget.
|
|
92
|
+
export class LockTimeoutError extends Error {
|
|
93
|
+
targetPath;
|
|
94
|
+
waitedMs;
|
|
95
|
+
constructor(targetPath, waitedMs) {
|
|
96
|
+
super(`could not acquire the lock on ${path.basename(targetPath)} within ${waitedMs}ms — update skipped`);
|
|
97
|
+
this.targetPath = targetPath;
|
|
98
|
+
this.waitedMs = waitedMs;
|
|
99
|
+
this.name = "LockTimeoutError";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
81
102
|
function sleepSync(ms) {
|
|
82
103
|
try {
|
|
83
104
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
@@ -86,10 +107,11 @@ function sleepSync(ms) {
|
|
|
86
107
|
}
|
|
87
108
|
export function withLock(targetPath, fn) {
|
|
88
109
|
const lockPath = targetPath + ".lock";
|
|
89
|
-
const MAX_WAIT_MS =
|
|
110
|
+
const MAX_WAIT_MS = 1500; // well inside the tightest hook budget (5s)
|
|
90
111
|
const STALE_MS = 5000;
|
|
91
112
|
const start = Date.now();
|
|
92
113
|
let held = false;
|
|
114
|
+
let backoff = 2;
|
|
93
115
|
while (Date.now() - start < MAX_WAIT_MS) {
|
|
94
116
|
try {
|
|
95
117
|
const fd = fs.openSync(lockPath, "wx");
|
|
@@ -111,21 +133,115 @@ export function withLock(targetPath, fn) {
|
|
|
111
133
|
catch {
|
|
112
134
|
continue;
|
|
113
135
|
} // lock vanished — retry immediately
|
|
114
|
-
sleepSync(
|
|
136
|
+
sleepSync(backoff);
|
|
137
|
+
backoff = Math.min(backoff * 2, 50);
|
|
115
138
|
}
|
|
116
139
|
}
|
|
140
|
+
if (!held)
|
|
141
|
+
throw new LockTimeoutError(targetPath, Date.now() - start);
|
|
117
142
|
try {
|
|
118
143
|
return fn();
|
|
119
144
|
}
|
|
120
145
|
finally {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
146
|
+
try {
|
|
147
|
+
fs.unlinkSync(lockPath);
|
|
148
|
+
}
|
|
149
|
+
catch { /* ignore */ }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* withLock for callers that must never throw — a hook that dies takes the tool call with it.
|
|
154
|
+
* Returns whether the callback ran; a lost update is reported on stderr rather than silently
|
|
155
|
+
* dropped, because "the number is wrong and nobody knows why" is the failure this whole lock
|
|
156
|
+
* exists to prevent.
|
|
157
|
+
*/
|
|
158
|
+
export function tryWithLock(targetPath, fn) {
|
|
159
|
+
try {
|
|
160
|
+
withLock(targetPath, fn);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
if (e instanceof LockTimeoutError) {
|
|
165
|
+
process.stderr.write(`[openwolf] ${e.message}\n`);
|
|
166
|
+
return false;
|
|
126
167
|
}
|
|
168
|
+
throw e;
|
|
127
169
|
}
|
|
128
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Where this session's tracking state lives.
|
|
173
|
+
*
|
|
174
|
+
* There used to be exactly one `_session.json` per project, shared by every session running in it.
|
|
175
|
+
* Two agents in the same repo — two terminals, or an agent plus a scripted run — meant the second
|
|
176
|
+
* one's SessionStart reset the first one's tracking mid-flight, and from then on both wrote into
|
|
177
|
+
* the same file: reads counted against the wrong session, the ledger entry for the first one
|
|
178
|
+
* ending with whatever the second had accumulated.
|
|
179
|
+
*
|
|
180
|
+
* Every harness hands each hook invocation a `session_id`, so the state is keyed by it. When there
|
|
181
|
+
* is none (an agent that does not provide one), the legacy shared path is used exactly as before —
|
|
182
|
+
* no worse than it was, and consistent within that agent.
|
|
183
|
+
*/
|
|
184
|
+
export function sessionFileFor(hooksDir, sessionId) {
|
|
185
|
+
if (typeof sessionId === "string") {
|
|
186
|
+
// Same reasoning as the cron task slug: this string becomes a file name and arrives from
|
|
187
|
+
// outside, so it is reduced to something that can only ever name a file in this directory.
|
|
188
|
+
const safe = sessionId.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 64);
|
|
189
|
+
if (safe)
|
|
190
|
+
return path.join(hooksDir, `_session-${safe}.json`);
|
|
191
|
+
}
|
|
192
|
+
return path.join(hooksDir, "_session.json");
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Delete per-session state files nobody will read again.
|
|
196
|
+
*
|
|
197
|
+
* One file per session means the directory grows with every session, forever. The stop hook has
|
|
198
|
+
* already folded a finished session into token-ledger.json, so the file is spent; a week is
|
|
199
|
+
* generous room for a session that is somehow still live. The legacy shared `_session.json` is
|
|
200
|
+
* never removed — it has no session it belongs to, so age says nothing about it.
|
|
201
|
+
*/
|
|
202
|
+
export function pruneOldSessionFiles(hooksDir, maxAgeDays = 7) {
|
|
203
|
+
const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
|
|
204
|
+
let names;
|
|
205
|
+
try {
|
|
206
|
+
names = fs.readdirSync(hooksDir);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
for (const name of names) {
|
|
212
|
+
if (!/^_session-.+\.json(\.lock)?$/.test(name))
|
|
213
|
+
continue;
|
|
214
|
+
const f = path.join(hooksDir, name);
|
|
215
|
+
try {
|
|
216
|
+
if (fs.statSync(f).mtimeMs < cutoff)
|
|
217
|
+
fs.unlinkSync(f);
|
|
218
|
+
}
|
|
219
|
+
catch { /* vanished or unreadable — nothing to clean */ }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Read-modify-write `_session.json` under the lock, in one place.
|
|
224
|
+
*
|
|
225
|
+
* Every hook in a session mutates this one file, and until now only two of the seven writers took
|
|
226
|
+
* the lock at all: the rest did `readJSON` … `writeJSON` around unrelated work, so a PostToolUse
|
|
227
|
+
* hook finishing while PreToolUse was mid-cycle silently dropped one of the two updates. Reads and
|
|
228
|
+
* writes get counted for the token ledger, so a lost update is a wrong number in a report nobody
|
|
229
|
+
* can audit afterwards.
|
|
230
|
+
*
|
|
231
|
+
* The mutator receives the state as it is ON DISK at the moment the lock is held — not a snapshot
|
|
232
|
+
* taken earlier — and must apply a DELTA (increment, add a key), never assign a whole object it
|
|
233
|
+
* captured before. That is what makes the update safe against a concurrent writer.
|
|
234
|
+
*/
|
|
235
|
+
export function updateSession(sessionFile, fallback, mutate) {
|
|
236
|
+
// tryWithLock, not withLock: a hook that throws takes the tool call with it. Under contention
|
|
237
|
+
// the update is dropped and SAID SO on stderr — the alternative (writing anyway) is what made
|
|
238
|
+
// the lock decorative in the first place.
|
|
239
|
+
return tryWithLock(sessionFile, () => {
|
|
240
|
+
const session = readJSON(sessionFile, fallback);
|
|
241
|
+
mutate(session);
|
|
242
|
+
writeJSON(sessionFile, session);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
129
245
|
// A missing file is normal (first run) and silently yields the fallback. An EXISTING file that
|
|
130
246
|
// fails to parse is a different situation: whatever it held is about to be treated as empty, and
|
|
131
247
|
// a read-modify-write caller would then overwrite it with defaults. That must at least be visible.
|
|
@@ -145,7 +261,7 @@ export function bookInjection(wolfDir, tokens) {
|
|
|
145
261
|
return;
|
|
146
262
|
const ledgerPath = path.join(wolfDir, "token-ledger.json");
|
|
147
263
|
try {
|
|
148
|
-
|
|
264
|
+
tryWithLock(ledgerPath, () => {
|
|
149
265
|
// [2026-08-20, review] The first cut used readJSON with a two-field fallback and then wrote
|
|
150
266
|
// it back. On an UNPARSEABLE ledger that produced a valid-looking file with no `sessions[]`,
|
|
151
267
|
// and the stop hook died on `ledger.sessions.findIndex(...)` from then on — silently, because
|
|
@@ -201,10 +317,24 @@ export function readJSON(filePath, fallback) {
|
|
|
201
317
|
return JSON.parse(raw);
|
|
202
318
|
}
|
|
203
319
|
catch (e) {
|
|
204
|
-
|
|
320
|
+
// The caller is about to treat this file as empty and, in a read-modify-write, overwrite it
|
|
321
|
+
// with defaults — so a corrupt token-ledger.json meant months of usage history vanished on the
|
|
322
|
+
// next Stop hook, with one line on stderr as the only trace. Move it aside first. Recovery is
|
|
323
|
+
// then a possibility rather than a hope, and because the path no longer exists, the next read
|
|
324
|
+
// takes the ordinary "missing file" branch instead of warning forever.
|
|
325
|
+
quarantineCorrupt(filePath);
|
|
326
|
+
process.stderr.write(`[openwolf] ${path.basename(filePath)} was not valid JSON — moved aside, using defaults (${e.message})\n`);
|
|
205
327
|
return fallback;
|
|
206
328
|
}
|
|
207
329
|
}
|
|
330
|
+
/** Rename a corrupt file to <name>.corrupt-<stamp>. Best-effort: never throws, never blocks a read. */
|
|
331
|
+
function quarantineCorrupt(filePath) {
|
|
332
|
+
try {
|
|
333
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
334
|
+
fs.renameSync(filePath, `${filePath}.corrupt-${stamp}`);
|
|
335
|
+
}
|
|
336
|
+
catch { /* unwritable directory, race with another process — defaults still apply */ }
|
|
337
|
+
}
|
|
208
338
|
// Duplicated from utils/fs-safe.ts (separate build roots). Never throws — a hook must not kill a
|
|
209
339
|
// session over a failed journal write — but a total failure is reported on stderr instead of
|
|
210
340
|
// vanishing: a silently dropped write once hid a completely broken persistence path (bug-183).
|
|
@@ -298,12 +428,17 @@ export function makeIgnoreMatcher(patterns) {
|
|
|
298
428
|
if (pat.startsWith("*.") && norm.endsWith(pat.slice(1)))
|
|
299
429
|
return true;
|
|
300
430
|
}
|
|
301
|
-
|
|
431
|
+
// `?` is a gitignore wildcard for exactly one character, and it also has to be handled BEFORE
|
|
432
|
+
// this becomes a regex — it used to be neither escaped nor translated, so it survived into the
|
|
433
|
+
// pattern as a regex quantifier and made the preceding character OPTIONAL. `build?/*.js` then
|
|
434
|
+
// matched `buil/x.js` and missed `build2/x.js`: the rule silently covered the wrong files.
|
|
435
|
+
if (pat.includes("*") || pat.includes("?")) {
|
|
302
436
|
const re = new RegExp("^" +
|
|
303
437
|
pat
|
|
304
438
|
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
305
439
|
.replace(/\*\*/g, "\u0000")
|
|
306
440
|
.replace(/\*/g, "[^/]*")
|
|
441
|
+
.replace(/\?/g, "[^/]")
|
|
307
442
|
.replace(/\u0000/g, ".*") +
|
|
308
443
|
"$");
|
|
309
444
|
if (re.test(norm) || (!anchored && re.test(base)))
|
|
@@ -1054,6 +1189,78 @@ export function readStdin() {
|
|
|
1054
1189
|
export function normalizePath(p) {
|
|
1055
1190
|
return p.replace(/\\/g, "/");
|
|
1056
1191
|
}
|
|
1192
|
+
/**
|
|
1193
|
+
* The file's path relative to the project root, or "" when it is outside.
|
|
1194
|
+
*
|
|
1195
|
+
* Lexical first — cheap, and correct for the overwhelming majority. But a lexical comparison alone
|
|
1196
|
+
* silently drops a whole project whenever the root is reached through a symlink: /home/me/work is
|
|
1197
|
+
* a link to /mnt/data/work, the agent opens /mnt/data/work/src/a.ts, the prefix does not match, and
|
|
1198
|
+
* every read in that project goes untracked with no error anywhere. macOS makes this the default
|
|
1199
|
+
* case for anything under /tmp (-> /private/tmp).
|
|
1200
|
+
*
|
|
1201
|
+
* So a lexical miss gets a second opinion from realpath, on both sides. Resolution only runs when
|
|
1202
|
+
* the cheap answer already said no, so the common path costs nothing.
|
|
1203
|
+
*/
|
|
1204
|
+
export function relativeToProject(absFile, projectDir) {
|
|
1205
|
+
const file = normalizePath(absFile);
|
|
1206
|
+
const root = normalizePath(projectDir);
|
|
1207
|
+
// Require the SEPARATOR after the root — a bare startsWith also matched a sibling directory
|
|
1208
|
+
// sharing the prefix (/root/orderflow2 counted as inside /root/orderflow).
|
|
1209
|
+
if (file.startsWith(root + "/"))
|
|
1210
|
+
return file.slice(root.length + 1);
|
|
1211
|
+
if (file === root)
|
|
1212
|
+
return "";
|
|
1213
|
+
try {
|
|
1214
|
+
const realFile = normalizePath(fs.realpathSync(absFile));
|
|
1215
|
+
const realRoot = normalizePath(fs.realpathSync(projectDir));
|
|
1216
|
+
if (realFile.startsWith(realRoot + "/"))
|
|
1217
|
+
return realFile.slice(realRoot.length + 1);
|
|
1218
|
+
}
|
|
1219
|
+
catch { /* the file may not exist yet, or the root is gone — outside, then */ }
|
|
1220
|
+
return "";
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Does an absolute (forward-slashed) file path correspond to this anatomy.md entry?
|
|
1224
|
+
*
|
|
1225
|
+
* anatomy entries are project-relative ("src/api/client.ts"); the hooks see an absolute path. The
|
|
1226
|
+
* old test was `abs.endsWith(rel) || abs.endsWith("/" + rel)`, and the first half of that had no
|
|
1227
|
+
* path-segment boundary: `/home/me/vendor/src/api/client.ts` and `/tmp/other-src/api/client.ts`
|
|
1228
|
+
* both "matched" an entry for `src/api/client.ts`, so the hook printed another file's description
|
|
1229
|
+
* and token estimate and booked an anatomy HIT for a file anatomy.md had never seen. The second
|
|
1230
|
+
* half of the old expression was already correct, so this keeps that and drops the loose one — an
|
|
1231
|
+
* exact equality covers the case where the path is already relative.
|
|
1232
|
+
*/
|
|
1233
|
+
export function matchesAnatomyEntry(filePath, entryRelPath) {
|
|
1234
|
+
return filePath === entryRelPath || filePath.endsWith("/" + entryRelPath);
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Does this project-relative path point OUTSIDE the project root?
|
|
1238
|
+
*
|
|
1239
|
+
* Input is the output of `normalizePath(path.relative(root, file))`. The obvious test —
|
|
1240
|
+
* "does it start with `..`" — is complete on POSIX and wrong on Windows, because `path.relative`
|
|
1241
|
+
* cannot express a route between two drives and gives up by returning the target ABSOLUTE:
|
|
1242
|
+
*
|
|
1243
|
+
* path.win32.relative("C:\\proj", "F:\\secrets\\x") === "F:\\secrets\\x"
|
|
1244
|
+
*
|
|
1245
|
+
* That has no leading `..`, so the guard passed it and the full foreign path was written into
|
|
1246
|
+
* anatomy.md and memory.md — precisely the leak (upstream #56) the guard exists to prevent, on the
|
|
1247
|
+
* one platform nobody tested it on. An absolute result means "no relative route exists", i.e. the
|
|
1248
|
+
* strongest possible outside-the-root signal.
|
|
1249
|
+
*
|
|
1250
|
+
* The `..` test is also anchored to a whole segment now: a file genuinely named `..config` in the
|
|
1251
|
+
* root is inside the project, and used to be counted as external.
|
|
1252
|
+
*/
|
|
1253
|
+
export function isOutsideProject(relPath) {
|
|
1254
|
+
if (relPath === "")
|
|
1255
|
+
return true; // the path IS the root, not a file in it
|
|
1256
|
+
if (relPath === ".." || relPath.startsWith("../"))
|
|
1257
|
+
return true;
|
|
1258
|
+
if (path.isAbsolute(relPath))
|
|
1259
|
+
return true; // win32 cross-drive ("F:/secrets/x")
|
|
1260
|
+
if (/^[a-zA-Z]:/.test(relPath))
|
|
1261
|
+
return true; // drive-relative ("F:x"), also not ours
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
1057
1264
|
// Count non-mechanical (semantic) entries written to memory.md today. Mechanical rows
|
|
1058
1265
|
// (auto file-op / session-end lines) don't count. Used by the stop hook to detect whether
|
|
1059
1266
|
// a meaningful session summary was written. (upstream #55)
|