@fusengine/harness 0.1.81 → 0.1.83

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.
@@ -1,28 +1,29 @@
1
1
  import { a as recordAgent, c as recordRefRead, d as trivialCount, i as emptyTrack, l as recordTarget, n as SessionTrack, o as recordBrainstormRequired, r as agentsFresh, s as recordDoc, t as AgentQuality, u as recordTrivialEdit } from "../session-state-D5gLr66m.mjs";
2
2
  //#region src/tracking/store.d.ts
3
+ /** Journal mode is the default; `FUSE_TRACK_JOURNAL=0` reverts to the legacy locked RMW (see the module NOTE: =0 ignores the uncompacted journal — recoverable, keep the `.log`). */
4
+ declare function trackJournalEnabled(): boolean;
3
5
  /**
4
- * Load and verify a session track from a signed envelope file.
5
- *
6
- * Returns {@link emptyTrack} (fail closed) when the file is absent, corrupt, or
7
- * fails MAC validation — so the gates re-require real agents rather than trust a
8
- * forged track. Only a MAC mismatch triggers fail-closed; the nonce is advisory
9
- * and is never checked during load.
6
+ * Load a session track: the verified legacy snapshot (the migration base — an
7
+ * old `track-<sid>.json` with no log reads exactly as before) with the journal
8
+ * folded on top. Fail closed on tampering (envelope MAC, per-line MAC).
10
9
  */
11
10
  declare function loadTrack(file: string): Promise<SessionTrack>;
12
11
  /**
13
- * Sign and persist a session track as a tamper-evident envelope, then write the
12
+ * Persist a session track. Journal mode appends the diff as signed events;
13
+ * legacy mode signs and persists the tamper-evident envelope, then writes the
14
14
  * advisory nonce watermark.
15
15
  */
16
16
  declare function saveTrack(file: string, track: SessionTrack): Promise<void>;
17
17
  /**
18
- * Locked read-modify-write on a session track (the fan-out-safe way to mutate
19
- * it): loads under the lock, applies `mutate`, persists atomically. On lock
20
- * contention the write is SKIPPED (fail-open, stderr already logged) and
21
- * `false` is returned a named lost write, never a crash.
18
+ * Mutate a session track. Journal mode: load (snapshot log), run `mutate`,
19
+ * append the resulting events lock-free, so a write is only ever lost to a
20
+ * genuine I/O error (reported as `false`). Legacy mode: locked RMW; on lock
21
+ * contention the write is SKIPPED (fail-open, stderr logged) and `false` is
22
+ * returned — a named lost write, never a crash.
22
23
  * @param file - The session track file.
23
24
  * @param mutate - Pure mutation over the freshly loaded track.
24
25
  * @returns True when the write landed.
25
26
  */
26
27
  declare function withTrack(file: string, mutate: (track: SessionTrack) => SessionTrack | Promise<SessionTrack>): Promise<boolean>;
27
28
  //#endregion
28
- export { AgentQuality, SessionTrack, agentsFresh, emptyTrack, loadTrack, recordAgent, recordBrainstormRequired, recordDoc, recordRefRead, recordTarget, recordTrivialEdit, saveTrack, trivialCount, withTrack };
29
+ export { AgentQuality, SessionTrack, agentsFresh, emptyTrack, loadTrack, recordAgent, recordBrainstormRequired, recordDoc, recordRefRead, recordTarget, recordTrivialEdit, saveTrack, trackJournalEnabled, trivialCount, withTrack };
@@ -1,2 +1,2 @@
1
- import { _ as trivialCount, d as recordAgent, f as recordBrainstormRequired, g as recordTrivialEdit, h as recordTarget, l as agentsFresh, m as recordRefRead, n as saveTrack, p as recordDoc, r as withTrack, t as loadTrack, u as emptyTrack } from "../store-yr_QFsZY.mjs";
2
- export { agentsFresh, emptyTrack, loadTrack, recordAgent, recordBrainstormRequired, recordDoc, recordRefRead, recordTarget, recordTrivialEdit, saveTrack, trivialCount, withTrack };
1
+ import { S as trivialCount, _ as recordBrainstormRequired, b as recordTarget, g as recordAgent, h as emptyTrack, i as withTrack, m as agentsFresh, n as saveTrack, r as trackJournalEnabled, t as loadTrack, v as recordDoc, x as recordTrivialEdit, y as recordRefRead } from "../store-Dw08Amqh.mjs";
2
+ export { agentsFresh, emptyTrack, loadTrack, recordAgent, recordBrainstormRequired, recordDoc, recordRefRead, recordTarget, recordTrivialEdit, saveTrack, trackJournalEnabled, trivialCount, withTrack };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusengine/harness",
3
- "version": "0.1.81",
3
+ "version": "0.1.83",
4
4
  "description": "Harness-agnostic toolkit for AI coding agents: runtime harness detection (Claude Code, Codex, Cursor, Cline, Gemini, Aider...), pure policy core (env config, project/framework detection, SOLID/file-size limits, APEX freshness, guard patterns, portable prompts), cache, project memory, ref routing, state/locks, statusline, per-harness adapters (Claude/Cursor/Cline/Gemini) and a cli-mode harness-check binary. Bun-native, with a built dist for Node + bundlers.",
5
5
  "type": "module",
6
6
  "module": "src/index.ts",
@@ -1,437 +0,0 @@
1
- import { c as ttlLabel } from "./dotenv-DoqNlALG.mjs";
2
- import { n as fuseHarnessHome } from "./home-state-oUGFB4ds.mjs";
3
- import { a as writeJsonFile, i as readJsonFile } from "./json-io-DisYd2fb.mjs";
4
- import { i as resolveSessions } from "./doc-helpers-CWZegVdR.mjs";
5
- import { t as routeReferences } from "./router-PKVNBHge.mjs";
6
- import { t as resolveSkillPath } from "./skill-path-DhBGlQe3.mjs";
7
- import { dirname, join } from "node:path";
8
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
9
- import { createHmac, randomBytes } from "node:crypto";
10
- //#region src/policy/skill-source.ts
11
- /**
12
- * Framework -> SKILL.md doc source for the APEX Check-1 non-routed deny —
13
- * parity with the Python plugin's enforce-helpers.ts `getSkillSource()`
14
- * (10-entry map, fallback "mcp__context7__query-docs"). The map is ported
15
- * faithfully as skill NAMES only: disk paths are resolved DYNAMICALLY via
16
- * {@link resolveSkillPath} (proprietary rule — no static path table; installed
17
- * plugin dir names differ from upstream's `react-expert/...`, and a hardcoded
18
- * table drifts from what is actually installed).
19
- */
20
- /**
21
- * Fallback doc source when no SKILL.md can be promised: the consigne to query
22
- * context7 (a TOOL name, not a path) — parity enforce-helpers.ts:51.
23
- */
24
- const CONTEXT7_SOURCE = "mcp__context7__query-docs";
25
- /**
26
- * Framework -> sub-skill dir name, faithful to the Python
27
- * `getSkillSource` map (enforce-helpers.ts:39-50, all 10 entries).
28
- */
29
- const SKILL_NAMES = {
30
- react: "react-19",
31
- nextjs: "nextjs-16",
32
- swift: "swiftui-components",
33
- laravel: "laravel-eloquent",
34
- tailwind: "tailwindcss-v4",
35
- generic: "solid-generic",
36
- java: "solid-java",
37
- go: "solid-go",
38
- ruby: "solid-ruby",
39
- rust: "solid-rust"
40
- };
41
- /**
42
- * The doc source a Check-1 deny may promise for `framework` (parity
43
- * `getSkillSource`): the REAL installed SKILL.md absolute path when it exists
44
- * on disk, else {@link CONTEXT7_SOURCE}. `resolveSkillPath` returns a
45
- * `/SKILL.md`-suffixed path ONLY on an `existsSync` hit; its
46
- * `${PLUGINS_DIR}/<name>` not-found fallback (no marketplace installed, or a
47
- * mapped skill absent on disk — e.g. upstream's `swiftui-components`) is
48
- * converted to the context7 consigne here, because a deny must never promise
49
- * a nonexistent path (every option a deny offers must actually unblock).
50
- * @param framework - Detected framework (the `detectFramework()` domain).
51
- * @param pluginsDirOverride - Marketplace plugins dir override (tests).
52
- */
53
- function getSkillSource(framework, pluginsDirOverride) {
54
- const name = SKILL_NAMES[framework];
55
- if (!name) return CONTEXT7_SOURCE;
56
- const path = resolveSkillPath(name, pluginsDirOverride);
57
- return path.endsWith("/SKILL.md") ? path : CONTEXT7_SOURCE;
58
- }
59
- //#endregion
60
- //#region src/policy/apex-authorization.ts
61
- /**
62
- * Credit a doc consultation onto one framework's auth entry (parity
63
- * track-doc-consultation.py): the once-per-session Check-2 fields
64
- * (`doc_sessions` + `sources`) plus the per-framework Check-1 stamp
65
- * (`sessions` + `doc_consulted`). Pure.
66
- */
67
- function creditDocConsultation(prev, sessionId, source, nowIso) {
68
- const p = prev ?? {};
69
- const add = (list, v) => v && !list.includes(v) ? [...list, v] : list;
70
- return {
71
- ...p,
72
- sessions: add(resolveSessions(p), sessionId),
73
- doc_sessions: add(p.doc_sessions ?? [], sessionId),
74
- sources: add(p.sources ?? (p.source ? [p.source] : []), source),
75
- doc_consulted: nowIso
76
- };
77
- }
78
- /**
79
- * Check 1 (parity enforce-apex-phases.ts isAuthorized): the framework's doc or
80
- * skill was consulted in THIS session (resolveSessions) AND the stamp is
81
- * younger than the TTL (FUSE_ENFORCE_TTL_SEC via src/config/ttl.ts).
82
- */
83
- function isAuthorized(auth, sessionId, now, ttlMs) {
84
- if (!auth?.doc_consulted || !resolveSessions(auth).includes(sessionId)) return false;
85
- const readEpoch = Date.parse(auth.doc_consulted);
86
- return !Number.isNaN(readEpoch) && now - readEpoch < ttlMs;
87
- }
88
- /**
89
- * A fresh same-session `skills/**.md` read whose path names the framework also
90
- * satisfies Check 1 (parity track-doc-consultation.py, which stamps
91
- * `doc_consulted` on `skills/.*\.md` Reads; the harness records those Reads in
92
- * `refsRead`/`refsReadAt` instead, and the track file is per-session).
93
- */
94
- function skillReadAuthorized(ctx, now, ttlMs) {
95
- const nameRe = new RegExp(`\\b(${{
96
- laravel: "laravel|php",
97
- tailwind: "tailwindcss?"
98
- }[ctx.framework] ?? ctx.framework})\\b`, "i");
99
- return (ctx.refsRead ?? []).some((p) => {
100
- if (!/skills\/.*\.md$/i.test(p) || !nameRe.test(p)) return false;
101
- const at = ctx.refsReadAt?.[p];
102
- return at !== void 0 && now - at < ttlMs;
103
- });
104
- }
105
- /**
106
- * Gate (Check 1): deny until the framework's doc/skill is freshly consulted.
107
- * The deny lists the routed SOLID refs when refs are loaded (parity
108
- * formatRoutedDeny), else the framework's skill source (parity getSkillSource:
109
- * a real installed SKILL.md path, or the context7 consigne). The RUNTIME
110
- * caller persists `state.target` on deny (recordTarget, parity :80-81) so
111
- * recordDoc can cross-credit this framework on the next doc consultation.
112
- */
113
- const apexAuthorizationGate = (ctx) => {
114
- const windowMs = ctx.windowMs ?? 120 * 1e3;
115
- const now = ctx.now ?? Date.now();
116
- if (isAuthorized(ctx.authorizations?.[ctx.framework], ctx.sessionId, now, windowMs)) return null;
117
- if (skillReadAuthorized(ctx, now, windowMs)) return null;
118
- const ttl = ttlLabel(Math.round(windowMs / 1e3));
119
- const title = `APEX: ${ctx.framework} documentation required`;
120
- const routed = ctx.refs?.length ? routeReferences(ctx.refs, ctx.filePath, ctx.content) : null;
121
- if (!routed) {
122
- const src = getSkillSource(ctx.framework);
123
- return {
124
- kind: "block",
125
- title,
126
- reason: `APEX: Read doc first (expires every ${ttl}) for ${ctx.framework}! Source: ${src}`,
127
- actions: [src === "mcp__context7__query-docs" ? `Query ${CONTEXT7_SOURCE} for ${ctx.framework} docs` : `Read ${src}`]
128
- };
129
- }
130
- const optional = routed.optional.map((r) => r.meta.filePath);
131
- return {
132
- kind: "block",
133
- title,
134
- reason: [
135
- `APEX: Read ALL SOLID references below (including Optional), then retry (expires every ${ttl}) for ${ctx.framework}.`,
136
- `Editing: ${ctx.filePath}`,
137
- ...optional.length ? [`Optional: ${optional.join(", ")}`] : [],
138
- `Full skill: ${routed.skillPath}`
139
- ].join("\n"),
140
- actions: routed.required.map((r) => r.meta.filePath)
141
- };
142
- };
143
- //#endregion
144
- //#region src/tracking/session-state.ts
145
- /** A fresh, empty track. */
146
- function emptyTrack() {
147
- return {
148
- authorizations: {},
149
- refsRead: [],
150
- agents: [],
151
- trivialEdits: []
152
- };
153
- }
154
- /**
155
- * Record a doc consultation (Context7/Exa/web) for a framework in this session:
156
- * the Check-2 credit (`doc_sessions` + `sources`) plus the Check-1 stamp
157
- * (`sessions` + `doc_consulted` = ISO of `now`). PARITY track-doc-consultation.py
158
- * :62-70 — a `target` left by a Check-1 deny (enforce-apex-phases.ts:80) is
159
- * cross-credited on EVERY consultation, with NO TTL on `target.set_at`, and the
160
- * target PERSISTS (only the next deny replaces it via {@link recordTarget});
161
- * single-shot clearing or TTL-gating the target re-opened the deny loop when a
162
- * consultation landed > TTL after the deny. Immutable.
163
- */
164
- function recordDoc(track, framework, sessionId, source, now = Date.now()) {
165
- const nowIso = new Date(now).toISOString();
166
- const authorizations = {
167
- ...track.authorizations,
168
- [framework]: creditDocConsultation(track.authorizations[framework], sessionId, source, nowIso)
169
- };
170
- const t = track.target;
171
- if (t && t.framework !== framework) authorizations[t.framework] = creditDocConsultation(track.authorizations[t.framework], sessionId, source, nowIso);
172
- return {
173
- ...track,
174
- authorizations
175
- };
176
- }
177
- /** Set the pending doc-credit target (written by the runtime on a Check-1 deny). Immutable. */
178
- function recordTarget(track, target) {
179
- return {
180
- ...track,
181
- target
182
- };
183
- }
184
- /**
185
- * Record that a SOLID reference file was read (deduped). Immutable. When `now`
186
- * (epoch ms — the tool event's own timestamp, never a fresh `Date.now()`) is
187
- * supplied, the read is stamped in `refsReadAt`, refreshed on re-reads so the
188
- * LATEST read drives the SOLID TTL (parity track-solid-reads.py, which appends
189
- * a timestamped entry per read; require-solid-read.py checks the most recent).
190
- * Callers that omit `now` keep the legacy untimestamped behavior.
191
- */
192
- function recordRefRead(track, path, now) {
193
- const refsRead = track.refsRead.includes(path) ? track.refsRead : [...track.refsRead, path];
194
- if (now === void 0) return refsRead === track.refsRead ? track : {
195
- ...track,
196
- refsRead
197
- };
198
- return {
199
- ...track,
200
- refsRead,
201
- refsReadAt: {
202
- ...track.refsReadAt,
203
- [path]: now
204
- }
205
- };
206
- }
207
- /** Record an agent/tool call with a timestamp + optional quality. Immutable. */
208
- function recordAgent(track, name, ts, quality) {
209
- const entry = quality ? {
210
- name,
211
- ts,
212
- quality
213
- } : {
214
- name,
215
- ts
216
- };
217
- return {
218
- ...track,
219
- agents: [...track.agents, entry]
220
- };
221
- }
222
- /** True when ALL of `names` ran within `windowMs` with non-insufficient quality. */
223
- function agentsFresh(track, names, windowMs, now) {
224
- const cutoff = now - windowMs;
225
- return names.every((n) => track.agents.some((a) => a.name === n && a.ts > cutoff && a.quality !== "insufficient"));
226
- }
227
- /** Record a trivial edit timestamp (sliding window; old evicted). Immutable. */
228
- function recordTrivialEdit(track, ts, windowMs, now) {
229
- const cutoff = now - windowMs;
230
- return {
231
- ...track,
232
- trivialEdits: [...(track.trivialEdits ?? []).filter((t) => t > cutoff), ts]
233
- };
234
- }
235
- /** Count trivial edits within the sliding window. */
236
- function trivialCount(track, windowMs, now) {
237
- const cutoff = now - windowMs;
238
- return (track.trivialEdits ?? []).filter((t) => t > cutoff).length;
239
- }
240
- /** Set the brainstorm-required flag (from creation-intent detection). Immutable. */
241
- function recordBrainstormRequired(track, required) {
242
- return {
243
- ...track,
244
- brainstormRequired: required
245
- };
246
- }
247
- //#endregion
248
- //#region src/tracking/integrity.ts
249
- /**
250
- * @module integrity
251
- * HMAC-SHA256 tamper-evident wrapping for {@link SessionTrack}.
252
- *
253
- * Failure policy: ONLY a MAC mismatch causes fail-closed (returns null).
254
- * The nonce is included in the signed payload and written to disk for advisory
255
- * diagnostics, but is NOT checked during verification — concurrent hook
256
- * invocations (PostToolUse, SubagentStop, …) legitimately load the same
257
- * envelope after the nonce watermark advances; a monotonic check would trigger
258
- * spurious fail-closed behaviour mid-session.
259
- *
260
- * The machine key is readable by the same agent process, so this deters naive
261
- * out-of-band tampering — not a determined re-sign. Item B (transcript-grounded
262
- * freshness) is the primary guarantee.
263
- * @packageDocumentation
264
- */
265
- const HARNESS_DIR = fuseHarnessHome();
266
- const KEY_PATH = join(HARNESS_DIR, ".key");
267
- const NONCE_PATH = join(HARNESS_DIR, ".nonce");
268
- /** Load (or create on first use) the per-machine HMAC key stored at mode 0600. */
269
- function loadOrCreateKey() {
270
- mkdirSync(HARNESS_DIR, { recursive: true });
271
- if (existsSync(KEY_PATH)) return readFileSync(KEY_PATH, "utf8").trim();
272
- const candidate = randomBytes(32).toString("hex");
273
- try {
274
- writeFileSync(KEY_PATH, candidate, {
275
- encoding: "utf8",
276
- mode: 384,
277
- flag: "wx"
278
- });
279
- return candidate;
280
- } catch (e) {
281
- if (e.code !== "EEXIST") throw e;
282
- }
283
- for (let i = 0; i < 5; i++) {
284
- const existing = readFileSync(KEY_PATH, "utf8").trim();
285
- if (existing) return existing;
286
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
287
- }
288
- const key = readFileSync(KEY_PATH, "utf8").trim();
289
- if (!key) throw new Error("fuse: empty .key after write contention");
290
- return key;
291
- }
292
- /**
293
- * Persist the last-seen nonce for advisory diagnostics (mode 0600).
294
- * Never read during {@link verifyTrack} — see module JSDoc for rationale.
295
- */
296
- function writeLastNonce(nonce) {
297
- mkdirSync(HARNESS_DIR, { recursive: true });
298
- writeFileSync(NONCE_PATH, String(nonce), {
299
- encoding: "utf8",
300
- mode: 384
301
- });
302
- }
303
- /** Compute HMAC-SHA256 over the canonical message `"${nonce}:${data}"`. */
304
- function computeMac(key, data, nonce) {
305
- return createHmac("sha256", key).update(`${nonce}:${data}`).digest("hex");
306
- }
307
- /**
308
- * Sign a {@link SessionTrack} into a tamper-evident envelope. The nonce
309
- * (`Date.now()`) is embedded in the MAC to bind the timestamp to the payload.
310
- */
311
- function signTrack(track) {
312
- const key = loadOrCreateKey();
313
- const data = JSON.stringify(track);
314
- const nonce = Date.now();
315
- return {
316
- data,
317
- nonce,
318
- mac: computeMac(key, data, nonce)
319
- };
320
- }
321
- /**
322
- * Verify a {@link TrackEnvelope}. Returns the parsed {@link SessionTrack} on
323
- * success, or `null` ONLY on MAC mismatch / parse failure (fail-closed on
324
- * tampering). The nonce is NOT checked — see module JSDoc.
325
- */
326
- function verifyTrack(envelope) {
327
- try {
328
- const key = loadOrCreateKey();
329
- if (envelope.mac !== computeMac(key, envelope.data, envelope.nonce)) return null;
330
- return JSON.parse(envelope.data);
331
- } catch {
332
- return null;
333
- }
334
- }
335
- //#endregion
336
- //#region src/tracking/track-lock.ts
337
- /**
338
- * Cross-process lock for session-track read-modify-write. The ~11-hook
339
- * fan-out races plain `loadTrack → mutate → saveTrack` (writes silently
340
- * lost — `refsRead` was the only mitigated channel, via its journal). This
341
- * lockfile (`wx`, atomic create) serializes the whole RMW:
342
- * - retry is bounded in TOTAL time (400 ms, far under any hook timeout);
343
- * - after the budget, NEVER crash: stderr is logged and the caller skips the
344
- * write — a named lost write beats a fail-open crash (hook fail-open
345
- * semantics preserved);
346
- * - an orphaned lockfile (dead process) is reclaimed after a 10 s TTL, so a
347
- * crash mid-write never blocks a project forever.
348
- */
349
- /** Orphaned lockfile lifetime before reclamation. */
350
- const LOCK_TTL_MS = 1e4;
351
- /** Delay between two lock acquisition attempts. */
352
- const RETRY_STEP_MS = 8;
353
- /** Sentinel returned by {@link withTrackLock} when the lock could not be taken. */
354
- const LOCK_FAILED = Symbol("track-lock-failed");
355
- /** True when the lockfile is older than the TTL (orphaned by a dead process). */
356
- function isStale(lock) {
357
- try {
358
- return Date.now() - statSync(lock).mtimeMs > LOCK_TTL_MS;
359
- } catch {
360
- return false;
361
- }
362
- }
363
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
364
- /**
365
- * Run `fn` under the track lock of `dir`, or skip with a stderr note.
366
- * @param dir - The track state directory (lockfile lives inside).
367
- * @param fn - The protected read-modify-write.
368
- * @returns `fn`'s result, or {@link LOCK_FAILED} when the lock stayed busy.
369
- */
370
- async function withTrackLock(dir, fn) {
371
- mkdirSync(dir, { recursive: true });
372
- const lock = join(dir, "track.lock");
373
- const deadline = Date.now() + 400;
374
- for (;;) try {
375
- closeSync(openSync(lock, "wx"));
376
- break;
377
- } catch {
378
- if (isStale(lock)) {
379
- try {
380
- unlinkSync(lock);
381
- } catch {}
382
- continue;
383
- }
384
- if (Date.now() >= deadline) {
385
- process.stderr.write(`harness: track lock busy, write skipped (${lock})\n`);
386
- return LOCK_FAILED;
387
- }
388
- await sleep(RETRY_STEP_MS);
389
- }
390
- try {
391
- return await fn();
392
- } finally {
393
- try {
394
- unlinkSync(lock);
395
- } catch {}
396
- }
397
- }
398
- //#endregion
399
- //#region src/tracking/store.ts
400
- /**
401
- * Load and verify a session track from a signed envelope file.
402
- *
403
- * Returns {@link emptyTrack} (fail closed) when the file is absent, corrupt, or
404
- * fails MAC validation — so the gates re-require real agents rather than trust a
405
- * forged track. Only a MAC mismatch triggers fail-closed; the nonce is advisory
406
- * and is never checked during load.
407
- */
408
- async function loadTrack(file) {
409
- const envelope = await readJsonFile(file);
410
- if (!envelope) return emptyTrack();
411
- return verifyTrack(envelope) ?? emptyTrack();
412
- }
413
- /**
414
- * Sign and persist a session track as a tamper-evident envelope, then write the
415
- * advisory nonce watermark.
416
- */
417
- async function saveTrack(file, track) {
418
- const envelope = signTrack(track);
419
- await writeJsonFile(file, envelope);
420
- writeLastNonce(envelope.nonce);
421
- }
422
- /**
423
- * Locked read-modify-write on a session track (the fan-out-safe way to mutate
424
- * it): loads under the lock, applies `mutate`, persists atomically. On lock
425
- * contention the write is SKIPPED (fail-open, stderr already logged) and
426
- * `false` is returned — a named lost write, never a crash.
427
- * @param file - The session track file.
428
- * @param mutate - Pure mutation over the freshly loaded track.
429
- * @returns True when the write landed.
430
- */
431
- async function withTrack(file, mutate) {
432
- return await withTrackLock(dirname(file), async () => {
433
- await saveTrack(file, await mutate(await loadTrack(file)));
434
- }) !== LOCK_FAILED;
435
- }
436
- //#endregion
437
- export { trivialCount as _, LOCK_TTL_MS as a, writeLastNonce as c, recordAgent as d, recordBrainstormRequired as f, recordTrivialEdit as g, recordTarget as h, LOCK_FAILED as i, agentsFresh as l, recordRefRead as m, saveTrack as n, signTrack as o, recordDoc as p, withTrack as r, verifyTrack as s, loadTrack as t, emptyTrack as u, apexAuthorizationGate as v };