@fusengine/harness 0.1.53 → 0.1.55

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/dist/cli/bin.mjs CHANGED
@@ -4,7 +4,7 @@ import { t as detectHarness } from "../harness-Cb9xR8dC.mjs";
4
4
  import { t as claudeHome } from "../home-state-D0RLWP8J.mjs";
5
5
  import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-DZvP_9xB.mjs";
6
6
  import { n as writeInitFile, t as initFor } from "../run-Do2JltgU.mjs";
7
- import { F as runDoctor, I as runningVersion, It as todayUtc, L as versionBanner, t as handleHook } from "../handle-D93-VIGS.mjs";
7
+ import { F as runDoctor, I as runningVersion, L as versionBanner, Lt as todayUtc, t as handleHook } from "../handle-L4ZNmpwN.mjs";
8
8
  import { delimiter, join } from "node:path";
9
9
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
10
10
  import { homedir } from "node:os";
@@ -903,10 +903,10 @@ function trunc(text, limit) {
903
903
  /** Render fresh cache entries as the markdown injection block. */
904
904
  function render(entries) {
905
905
  const lines = [
906
- "# MCP Cache disponible cette session",
907
- "Avant de lancer mcp__context7/exa, verifie si deja cached.",
908
- "Lis le fichier .md via Read pour recuperer le resultat.",
909
- "APEX: Read sur cache MCP compte comme research-expert satisfait.",
906
+ "# MCP cache available this session",
907
+ "Before calling mcp__context7/exa, check if the result is already cached.",
908
+ "Read the .md file via Read to retrieve the result.",
909
+ "APEX: reading the MCP cache via Read satisfies the research-expert step.",
910
910
  "",
911
911
  "| Tool | Query | File |",
912
912
  "| --- | --- | --- |"
@@ -1233,7 +1233,7 @@ function pad(n) {
1233
1233
  return String(n).padStart(2, "0");
1234
1234
  }
1235
1235
  /** Compact local timestamp `YYYYMMDD-HHMMSS` (mirrors Python strftime). */
1236
- function stamp(now) {
1236
+ function stamp$1(now) {
1237
1237
  const d = new Date(now);
1238
1238
  return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
1239
1239
  }
@@ -1250,7 +1250,7 @@ function saveApexState(cwd, now = Date.now()) {
1250
1250
  if (!existsSync(stateFile)) return "";
1251
1251
  const backupDir = join(apexDir, "backups");
1252
1252
  mkdirSync(backupDir, { recursive: true });
1253
- copyFileSync(stateFile, join(backupDir, `task-${stamp(now)}.json`));
1253
+ copyFileSync(stateFile, join(backupDir, `task-${stamp$1(now)}.json`));
1254
1254
  const backups = readdirSync(backupDir).filter((n) => n.startsWith("task-") && n.endsWith(".json")).sort().reverse();
1255
1255
  for (const old of backups.slice(5)) try {
1256
1256
  rmSync(join(backupDir, old), { force: true });
@@ -1292,6 +1292,28 @@ function validateRulesLoaded(data, home = homedir()) {
1292
1292
  } catch {}
1293
1293
  }
1294
1294
  //#endregion
1295
+ //#region src/runtime/burst-window.ts
1296
+ /**
1297
+ * @module burst-window
1298
+ * Single source of truth for the multi-plugin hook fan-out window.
1299
+ *
1300
+ * Every DEPLOYED plugin registers its OWN PreToolUse/PostToolUse hook, so ONE
1301
+ * Claude tool event spawns ~11 sibling harness processes that each record the
1302
+ * same deny / one-shot / sniper reminder within milliseconds. Left unchecked
1303
+ * the deny-loop counter jumped by ~11 per real attempt ([REPEAT] "#9" on the
1304
+ * FIRST try), the one-shot metric inflated ~11×, and the sniper reminder was
1305
+ * injected ~11× (token noise).
1306
+ *
1307
+ * A record landing within this window after an identical prior one (same
1308
+ * operation hash + same `session_id`) is treated as the SAME event and folded
1309
+ * into it instead of re-counted. Two REAL agent retries are always spaced
1310
+ * further apart than the burst, so this never masks a genuine loop. No env var:
1311
+ * the fan-out is a physical property of the installed plugin set, not policy.
1312
+ * @packageDocumentation
1313
+ */
1314
+ /** Fan-out dedup window (ms). The ~11 sibling hooks for one event land in <2s. */
1315
+ const BURST_DEDUP_MS = 2e3;
1316
+ //#endregion
1295
1317
  //#region src/runtime/lifecycle/track-changes.ts
1296
1318
  /** Code-file extensions tracked for sniper (mirrors track-session-changes.py). */
1297
1319
  const CODE_EXT$1 = /\.(ts|tsx|js|jsx|py|go|rs|java|php|cpp|c|rb|swift|kt|vue|svelte|astro)$/;
@@ -1326,6 +1348,10 @@ function trackSessionChanges(sessionIdRaw, filePath, home = homedir(), now = Dat
1326
1348
  lastCheck: new Date(now).toISOString().replace(/\.\d{3}Z$/, "Z")
1327
1349
  };
1328
1350
  saveSessionState(sid, state, home);
1351
+ if (!oncePerWindow(`sniper:${sid}:${filePath}`, 2e3, {
1352
+ now,
1353
+ dir: sessionsDir(home)
1354
+ })) return "";
1329
1355
  return contextResponse("PostToolUse", `SNIPER VALIDATION REQUIRED: Code file '${basename(filePath)}' was modified. You MUST now run the sniper agent (fuse-ai-pilot:sniper) to validate this modification before continuing. This is mandatory per CLAUDE.md rules.`);
1330
1356
  }
1331
1357
  //#endregion
@@ -2136,16 +2162,10 @@ function cartoSessionStart(cwd, now = Date.now()) {
2136
2162
  return ctx ? contextResponse("SessionStart", ctx) : "";
2137
2163
  }
2138
2164
  //#endregion
2139
- //#region src/runtime/lifecycle/aipilot/curate-lessons.ts
2140
- /**
2141
- * Mechanical, LLM-free curation of `MEMORY/LESSON.md` bullets (anti-obesity):
2142
- * strict-dedup near-identical bullets (keep newest, `[TRIGGERS …]` preserved),
2143
- * flag over-cap + stale (>90d, cited path gone) in a report. Only dedup writes.
2144
- */
2145
- const CAP = 50;
2146
- const STALE_DAYS = 90;
2147
- const SIM_THRESHOLD = .8;
2148
- const MIN_TOKENS = 4;
2165
+ //#region src/runtime/lifecycle/aipilot/lesson-parse.ts
2166
+ /** Milliseconds in a day. */
2167
+ const DAY_MS = 864e5;
2168
+ /** Case-sensitive decision-time tag line (`[TRIGGERS …]`) — opus-lessons format. */
2149
2169
  const TRIG = /^\[TRIGGERS\s+.+\]$/;
2150
2170
  /** Epoch ms for a `[YYYY-MM-DD HH:MM]` stamp; `NaN` if absent or out of range. */
2151
2171
  function parseTs$1(line) {
@@ -2172,7 +2192,11 @@ function citedPaths(text) {
2172
2192
  for (const m of text.matchAll(/[\w./@-]+\.\w{1,5}/g)) if (m[0]) out.add(m[0]);
2173
2193
  return [...out].filter((p) => p.includes("/") && /\.\w{1,5}$/.test(p));
2174
2194
  }
2175
- /** Split into a verbatim preamble and one Block per `- ` bullet. */
2195
+ /** True when a block carries a `[TRIGGERS …]` continuation line. */
2196
+ function hasTrigger(b) {
2197
+ return b.raw.some((l) => TRIG.test(l.trim()));
2198
+ }
2199
+ /** Split content into a verbatim preamble and one Block per `- ` bullet. */
2176
2200
  function parse(content) {
2177
2201
  const lines = content.split("\n");
2178
2202
  const blocks = [];
@@ -2193,23 +2217,89 @@ function parse(content) {
2193
2217
  blocks
2194
2218
  };
2195
2219
  }
2220
+ //#endregion
2221
+ //#region src/runtime/lifecycle/aipilot/lesson-archive.ts
2222
+ /**
2223
+ * Stage 1 — cap→archive split for LESSON.md. When deduped bullets exceed CAP the
2224
+ * OLDEST excess is MOVED (never deleted) to LESSON-archive.md, EXCEPT a
2225
+ * `[TRIGGERS …]` bullet younger than STALE_DAYS: archiving it would blind the
2226
+ * PreToolUse trigger index (src/policy/lessons/trigger-index reads LESSON.md), so
2227
+ * it stays even past the cap. Pure: this module decides the partition and renders
2228
+ * the archive block; the fail-safe, archive-first file write is the caller's job.
2229
+ */
2230
+ /** Sort key: undated bullets sort oldest, so malformed entries archive first. */
2231
+ function age(b) {
2232
+ return Number.isNaN(b.ts) ? -Infinity : b.ts;
2233
+ }
2234
+ /** A TRIGGERS bullet is protected from archival until older than STALE_DAYS. */
2235
+ function isProtected(b, staleBefore) {
2236
+ return hasTrigger(b) && !(b.ts <= staleBefore);
2237
+ }
2238
+ /**
2239
+ * Partition deduped `blocks` (newest-first file order) into the bullets that
2240
+ * stay in LESSON.md and the oldest excess to archive. Archives only enough to
2241
+ * reach CAP, skipping protected TRIGGERS bullets (so the file MAY stay slightly
2242
+ * over cap by design). Order is preserved in both halves; `keep ∪ archive` is
2243
+ * exactly `blocks` with no loss and no mutation.
2244
+ * @param blocks - Deduped bullets, newest first.
2245
+ * @param now - Clock (ms) for the STALE_DAYS protection window.
2246
+ * @returns `{ keep, archive }` — a lossless partition of `blocks`.
2247
+ */
2248
+ function splitAtCap(blocks, now) {
2249
+ if (blocks.length <= 50) return {
2250
+ keep: blocks,
2251
+ archive: []
2252
+ };
2253
+ const staleBefore = now - 90 * DAY_MS;
2254
+ const oldestFirst = [...blocks].sort((a, b) => age(a) - age(b));
2255
+ const toArchive = /* @__PURE__ */ new Set();
2256
+ let excess = blocks.length - 50;
2257
+ for (const b of oldestFirst) {
2258
+ if (excess <= 0) break;
2259
+ if (isProtected(b, staleBefore)) continue;
2260
+ toArchive.add(b);
2261
+ excess--;
2262
+ }
2263
+ return {
2264
+ keep: blocks.filter((b) => !toArchive.has(b)),
2265
+ archive: blocks.filter((b) => toArchive.has(b))
2266
+ };
2267
+ }
2268
+ /**
2269
+ * Render `archive` bullets as a dated block to PREPEND to LESSON-archive.md
2270
+ * (newest archive session on top). Bullets are emitted BYTE-IDENTICAL (raw lines
2271
+ * rejoined) — zero mutation, so the move stays reversible/auditable.
2272
+ * @param archive - Bullets chosen by {@link splitAtCap}.
2273
+ * @param now - Clock (ms) for the archival header date.
2274
+ * @returns The block text (trailing newline), or "" when nothing is archived.
2275
+ */
2276
+ function formatArchive(archive, now) {
2277
+ if (archive.length === 0) return "";
2278
+ return `${`<!-- archived ${new Date(now).toISOString().slice(0, 10)}: ${archive.length} bullet(s) moved from LESSON.md at cap 50 -->`}\n${archive.map((b) => b.raw.join("\n")).join("\n\n")}\n`;
2279
+ }
2280
+ //#endregion
2281
+ //#region src/runtime/lifecycle/aipilot/curate-lessons.ts
2282
+ /**
2283
+ * Stage-0 mechanical, LLM-free dedup of MEMORY/LESSON.md bullets + cap→archive
2284
+ * orchestration. Strict-dedup near-identical bullets (keep newest, `[TRIGGERS …]`
2285
+ * preserved), then hand the deduped set to lesson-archive's cap split. Returns the
2286
+ * rewritten LESSON.md content, the archive block to move out, and a human report.
2287
+ * Pure: all file I/O (archive-first, fail-safe) lives in the dispatch caller.
2288
+ */
2289
+ const SIM_THRESHOLD = .8;
2290
+ const MIN_TOKENS = 4;
2196
2291
  /** Report lines for bullets older than STALE_DAYS whose only cited path is gone. */
2197
2292
  function staleReport(blocks, now, root) {
2198
- const cutoff = now - STALE_DAYS * 864e5;
2293
+ const cutoff = now - 90 * DAY_MS;
2199
2294
  return blocks.flatMap((b) => {
2200
2295
  if (!(b.ts <= cutoff)) return [];
2201
2296
  const paths = citedPaths(b.raw.join(" "));
2202
2297
  if (paths.length === 0 || paths.some((p) => existsSync(join(root, p)))) return [];
2203
- return [`[STALE?] ${(b.raw[0] ?? "").slice(0, 90)} — chemin(s) disparu(s): ${paths.join(", ")}`];
2298
+ return [`[STALE?] ${(b.raw[0] ?? "").slice(0, 90)} — missing path(s): ${paths.join(", ")}`];
2204
2299
  });
2205
2300
  }
2206
- /**
2207
- * Strict-dedup LESSON.md bullets (keep newest, its `[TRIGGERS …]` line preserved —
2208
- * or carried over from the dropped twin if the kept one lacks it) and report
2209
- * cap/stale. `content` unchanged unless a dedup occurred. Returns content + report.
2210
- */
2211
- function curateLessons(content, now, root = process.cwd()) {
2212
- const { preamble, blocks } = parse(content);
2301
+ /** Strict-dedup: keep the newest of each near-identical pair (TRIGGERS carried over). Returns kept blocks + merge report lines. */
2302
+ function dedup(blocks) {
2213
2303
  const kept = [];
2214
2304
  const fused = [];
2215
2305
  for (const b of blocks) {
@@ -2224,20 +2314,91 @@ function curateLessons(content, now, root = process.cwd()) {
2224
2314
  const t = drop.raw.find((l) => TRIG.test(l.trim()));
2225
2315
  if (t) win.raw.push(t);
2226
2316
  }
2227
- fused.push(`fusion: gardé ${(win.raw[0] ?? "").slice(0, 60)} · retiré ${(drop.raw[0] ?? "").slice(0, 60)}`);
2317
+ fused.push(`merged: kept ${(win.raw[0] ?? "").slice(0, 60)} · dropped ${(drop.raw[0] ?? "").slice(0, 60)}`);
2228
2318
  }
2229
- const old = [...kept].sort((a, b) => a.ts - b.ts).slice(0, Math.max(0, kept.length - CAP));
2230
- const cap = old.length ? [`${kept.length} bullets (> ${CAP}) — plus anciens candidats à l'archivage:`, ...old.map((b) => ` ${(b.raw[0] ?? "").slice(0, 80)}`)] : [];
2319
+ return {
2320
+ kept,
2321
+ fused
2322
+ };
2323
+ }
2324
+ /**
2325
+ * Dedup LESSON.md bullets, then archive the oldest excess over CAP (via
2326
+ * lesson-archive). `content` is byte-identical to the input when nothing is
2327
+ * deduped or archived. The `archive` block (possibly "") is what the caller must
2328
+ * PREPEND to LESSON-archive.md, archive-first, before writing `content`.
2329
+ * @param content - Raw LESSON.md text.
2330
+ * @param now - Clock (ms) for stale/archival windows.
2331
+ * @param root - Project root, for resolving cited paths in the stale report.
2332
+ * @returns The rewritten content, the archive block, and the report.
2333
+ */
2334
+ function curateLessons(content, now, root = process.cwd()) {
2335
+ const { preamble, blocks } = parse(content);
2336
+ const { kept, fused } = dedup(blocks);
2337
+ const { keep, archive } = splitAtCap(kept, now);
2338
+ const rebuilt = fused.length > 0 || archive.length > 0 ? `${preamble}\n${keep.map((b) => b.raw.join("\n")).join("\n\n")}\n` : content;
2339
+ const capReport = archive.length ? [`${kept.length} bullets (> 50) — ${archive.length} oldest archived → LESSON-archive.md`] : [];
2231
2340
  const report = [
2232
2341
  ...fused,
2233
- ...cap,
2342
+ ...capReport,
2234
2343
  ...staleReport(blocks, now, root)
2235
2344
  ].join("\n");
2236
2345
  return {
2237
- content: fused.length ? `${preamble}\n${kept.map((b) => b.raw.join("\n")).join("\n\n")}\n` : content,
2346
+ content: rebuilt,
2347
+ archive: formatArchive(archive, now),
2238
2348
  report
2239
2349
  };
2240
2350
  }
2351
+ /** The `[YYYY-MM-DD HH:MM]` (or date-only) stamp of a bullet, "" if absent. */
2352
+ function stamp(block) {
2353
+ return (block.raw[0] ?? "").match(/\[(\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2})?)\]/)?.[1] ?? "";
2354
+ }
2355
+ /** Bullet text: raw lines joined, leading "- ", date stamp & TRIGGERS lines stripped. */
2356
+ function bodyText(block) {
2357
+ return block.raw.filter((l) => !/^\s*\[TRIGGERS\s/.test(l)).join(" ").replace(/^-\s*/, "").replace(/\[\d{4}-\d{2}-\d{2}[^\]]*\]\s*/, "").trim();
2358
+ }
2359
+ /** First sentence of `s` (split on a period + whitespace), whole string if none. */
2360
+ function firstSentence(s) {
2361
+ return (s.split(/(?<=\.)\s/)[0] ?? s).trim();
2362
+ }
2363
+ /**
2364
+ * Distil the actionable rule from a bullet body. With no "→" the whole bullet is
2365
+ * the rule → its first sentence. Otherwise the rule is everything after the FIRST
2366
+ * "→"; among its arrow-delimited segments (trimmed, empty dropped) take the first
2367
+ * sentence of the LONGEST — the information-dense clause, not whichever short
2368
+ * aside the author appended last. When that clause is under {@link MIN_RULE}
2369
+ * chars, fall back to the first sentence of the WHOLE rule part (never the
2370
+ * narrative), so a short trailing segment never yields an illegible stub yet a
2371
+ * legitimately terse rule is still shown intact.
2372
+ */
2373
+ function distillRule(text) {
2374
+ const arrow = text.indexOf("→");
2375
+ if (arrow < 0) return firstSentence(text);
2376
+ const rulePart = text.slice(arrow + 1);
2377
+ const rule = firstSentence(rulePart.split("→").map((s) => s.trim()).filter(Boolean).reduce((a, b) => b.length > a.length ? b : a, ""));
2378
+ return rule.length >= 40 ? rule : firstSentence(rulePart);
2379
+ }
2380
+ /** Collapse one older bullet to `- [date] <rule>`: {@link distillRule}, capped. */
2381
+ function compressBullet(block) {
2382
+ let rule = distillRule(bodyText(block));
2383
+ if (rule.length > 200) rule = `${rule.slice(0, 199).trimEnd()}…`;
2384
+ const date = stamp(block);
2385
+ return `- ${date ? `[${date}] ` : ""}${rule}`;
2386
+ }
2387
+ /**
2388
+ * Build the compressed injection body for `content`. The preamble comments are
2389
+ * dropped (format docs, noise for the reader); the `recentFull` newest bullets
2390
+ * stay whole, every older bullet becomes one distilled rule-line.
2391
+ * @param content - Raw LESSON.md text.
2392
+ * @param recentFull - Count of newest bullets to keep verbatim.
2393
+ * @returns The compressed block (bullets only), or the trimmed content when there are no bullets.
2394
+ */
2395
+ function compressInjection(content, recentFull = 10) {
2396
+ const { blocks } = parse(content);
2397
+ if (blocks.length === 0) return content.trim();
2398
+ const full = blocks.slice(0, recentFull).map((b) => b.raw.join("\n"));
2399
+ const rest = blocks.slice(recentFull).map(compressBullet);
2400
+ return [...full, ...rest].join("\n");
2401
+ }
2241
2402
  //#endregion
2242
2403
  //#region src/runtime/lifecycle/lessons/state.ts
2243
2404
  /**
@@ -2251,6 +2412,10 @@ function curateLessons(content, now, root = process.cwd()) {
2251
2412
  function lessonsFileFor(root) {
2252
2413
  return join(root, "MEMORY", "LESSON.md");
2253
2414
  }
2415
+ /** Absolute `<root>/MEMORY/LESSON-archive.md` — cold storage for capped-out bullets. */
2416
+ function lessonsArchiveFileFor(root) {
2417
+ return join(root, "MEMORY", "LESSON-archive.md");
2418
+ }
2254
2419
  /** Absolute `<root>/MEMORY/state.json` — machine-local throttle counter. */
2255
2420
  function lessonsStateFileFor(root) {
2256
2421
  return join(root, "MEMORY", "state.json");
@@ -2419,7 +2584,32 @@ function markWrite(payload, now) {
2419
2584
  * in {@link module:runtime/lifecycle/lessons/reminder}; this module keeps the
2420
2585
  * event router + lesson-file injection. Non-fatal by design.
2421
2586
  */
2422
- /** Inject `MEMORY/LESSON.md` for `event`, after mechanical curation (a strict dedup rewrites the file in place; any report surfaces to the user via systemMessage). */
2587
+ /**
2588
+ * Persist a curation ATOMICALLY and ARCHIVE-FIRST for zero-loss: prepend the
2589
+ * moved bullets to LESSON-archive.md, THEN rewrite LESSON.md. On ANY write error
2590
+ * the original file is left untouched (returns `original`) so a bullet is never
2591
+ * lost — a rare archive-then-trim-fail leaves a duplicate (never a loss), which
2592
+ * the next dedup pass reconciles.
2593
+ */
2594
+ function persistCuration(file, root, curated, archive, original) {
2595
+ try {
2596
+ if (archive) {
2597
+ const af = lessonsArchiveFileFor(root);
2598
+ const prev = existsSync(af) ? readFileSync(af, "utf-8") : "";
2599
+ atomicWrite(af, prev ? `${archive}\n${prev}` : archive);
2600
+ }
2601
+ atomicWrite(file, curated);
2602
+ return curated;
2603
+ } catch {
2604
+ return original;
2605
+ }
2606
+ }
2607
+ /**
2608
+ * Inject `MEMORY/LESSON.md` for `event`. Mechanical curation (dedup + cap→archive)
2609
+ * rewrites the FILE; the injected BLOCK is then COMPRESSED (newest bullets whole,
2610
+ * older ones distilled to their rule) so a growing file never inflates the
2611
+ * SessionStart/SubagentStart context. Any curation report surfaces via systemMessage.
2612
+ */
2423
2613
  function injectMemory(cwd, event, now) {
2424
2614
  const root = projectRoot(cwd);
2425
2615
  const file = lessonsFileFor(root);
@@ -2431,12 +2621,9 @@ function injectMemory(cwd, event, now) {
2431
2621
  return "";
2432
2622
  }
2433
2623
  if (!content) return "";
2434
- const { content: curated, report } = curateLessons(content, now, root);
2435
- if (curated !== content) try {
2436
- atomicWrite(file, curated);
2437
- content = curated;
2438
- } catch {}
2439
- const ctx = `Project lessons — never reproduce these:\n${content}\nYou may append OR refine/merge/dedupe bullets in MEMORY/LESSON.md — keep it terse.`;
2624
+ const { content: curated, archive, report } = curateLessons(content, now, root);
2625
+ if (curated !== content) content = persistCuration(file, root, curated, archive, content);
2626
+ const ctx = `Project lessons — never reproduce these:\n${compressInjection(content)}\nYou may append OR refine/merge/dedupe bullets in MEMORY/LESSON.md — keep it terse.`;
2440
2627
  return report ? attachSystemMessage(contextResponse(event, ctx), `LESSON.md curation:\n${report}`) : contextResponse(event, ctx);
2441
2628
  }
2442
2629
  /**
@@ -2495,14 +2682,32 @@ function denyHash(tool, input) {
2495
2682
  /**
2496
2683
  * Pure loop check: given the already-pruned in-window map, compute the running
2497
2684
  * count for `hash` and whether it repeats (count > 1). No IO — the caller persists.
2498
- * @param hash - {@link denyHash} of the current call.
2685
+ *
2686
+ * When `dedupMs` is set (>0) and an identical prior deny landed within that
2687
+ * window, the current call is a sibling hook echoing the SAME event (see
2688
+ * {@link module:burst-window}): it returns the prior verdict VERBATIM with
2689
+ * `deduped:true` and does NOT bump the count, so all N fan-out processes agree
2690
+ * on one number instead of counting to N. Absent `dedupMs` (mono-process
2691
+ * callers / unit tests) the historical increment-every-time behaviour holds.
2692
+ * @param hash - {@link denyHash}-derived map key of the current call.
2499
2693
  * @param priorDenies - The `{ hash -> DenyEntry }` map, pruned to `now`/`windowMs`.
2500
- * @param opts - Clock + window.
2501
- * @returns `{ isRepeat, count, hash }`.
2694
+ * @param opts - Clock + window, plus an optional burst-dedup window.
2695
+ * @returns `{ isRepeat, count, hash, deduped? }`.
2502
2696
  */
2503
2697
  function denyLoopCheck(hash, priorDenies, opts) {
2504
2698
  const prev = priorDenies[hash];
2505
- const count = (prev && typeof prev.lastTs === "number" && opts.now - prev.lastTs < opts.windowMs ? prev.count : 0) + 1;
2699
+ if (!(prev && typeof prev.lastTs === "number" && opts.now - prev.lastTs < opts.windowMs)) return {
2700
+ isRepeat: false,
2701
+ count: 1,
2702
+ hash
2703
+ };
2704
+ if ((opts.dedupMs ?? 0) > 0 && opts.now - prev.lastTs < (opts.dedupMs ?? 0)) return {
2705
+ isRepeat: prev.count > 1,
2706
+ count: prev.count,
2707
+ hash,
2708
+ deduped: true
2709
+ };
2710
+ const count = prev.count + 1;
2506
2711
  return {
2507
2712
  isRepeat: count > 1,
2508
2713
  count,
@@ -2518,7 +2723,7 @@ function denyLoopCheck(hash, priorDenies, opts) {
2518
2723
  * @returns A block prompt with `[REPEAT]` title, STOP-prefixed reason, forced research action.
2519
2724
  */
2520
2725
  function enrichRepeatDeny(prompt, count) {
2521
- const stop = `Tentative identique n°${count} déjà refusée pour la même raison. STOP: ne retente pas ce même appel. `;
2726
+ const stop = `Identical attempt #${count} already denied for the same reason. STOP: do not retry this same call. `;
2522
2727
  const action = "Launch fuse-ai-pilot:research-expert to find a DIFFERENT approach";
2523
2728
  return {
2524
2729
  ...prompt,
@@ -2636,6 +2841,39 @@ function formatSummary(s) {
2636
2841
  return `gates 7d: ${head}${parts.length ? `; ${parts.join("; ")}` : ""}`;
2637
2842
  }
2638
2843
  //#endregion
2844
+ //#region src/tracking/one-shot-dedup.ts
2845
+ /**
2846
+ * @module one-shot-dedup
2847
+ * Burst-dedup guard for the one-shot metric ({@link module:one-shot}).
2848
+ *
2849
+ * ONE Claude tool event fans out to ~11 sibling plugin-hook processes, each
2850
+ * calling {@link recordOneShot}; without this the metric would count a single
2851
+ * deny/allow ~11×. Reuses the proven {@link oncePerWindow} cooldown sidecar:
2852
+ * the FIRST process in the {@link module:burst-window} window mutates the
2853
+ * metric, the rest skip. The dedup key includes the outcome KIND (deny-title vs
2854
+ * allow) so a deny and its later fix — different kinds — are never folded into
2855
+ * each other. No `sessionId` → always the first (mono-process + unit-test
2856
+ * parity; a burst can only exist when a real session drives the fan-out).
2857
+ * @packageDocumentation
2858
+ */
2859
+ /**
2860
+ * True when this `(op, kind)` is the FIRST of its burst for the session — the
2861
+ * process that should actually mutate the metric. Sibling processes firing the
2862
+ * SAME event within {@link BURST_DEDUP_MS} return false and skip the write.
2863
+ * @param op - Content-free operation key ({@link denyHash}("op", …)).
2864
+ * @param kind - Outcome discriminator (`deny:<title>` or `allow`).
2865
+ * @param opts - Clock + state dir + optional session id.
2866
+ * @returns `true` to apply the record, `false` to skip (already counted).
2867
+ */
2868
+ function burstFirst(op, kind, opts) {
2869
+ const sid = opts.sessionId?.trim();
2870
+ if (!sid) return true;
2871
+ return oncePerWindow(`oneshot:${sid}:${op}:${kind}`, BURST_DEDUP_MS, {
2872
+ now: opts.now,
2873
+ dir: opts.dir
2874
+ });
2875
+ }
2876
+ //#endregion
2639
2877
  //#region src/tracking/one-shot.ts
2640
2878
  /**
2641
2879
  * @module one-shot
@@ -2683,12 +2921,13 @@ function loadState(path) {
2683
2921
  function recordOneShot(prompt, input, opts) {
2684
2922
  try {
2685
2923
  if (prompt && prompt.kind !== "block") return;
2686
- const path = join(opts.dir, SIDECAR$1);
2687
- let s = pruneState(loadState(path), opts.now, WINDOW_MS);
2688
2924
  const op = denyHash("op", {
2689
2925
  filePath: input.filePath,
2690
2926
  command: input.command
2691
2927
  });
2928
+ if (!burstFirst(op, prompt ? `deny:${prompt.title}` : "allow", opts)) return;
2929
+ const path = join(opts.dir, SIDECAR$1);
2930
+ let s = pruneState(loadState(path), opts.now, WINDOW_MS);
2692
2931
  s = prompt ? applyDeny(s, prompt.title, op, opts.now) : applyAllow(s, op, opts.now, input.content != null || input.command != null);
2693
2932
  atomicWrite(path, JSON.stringify(s));
2694
2933
  } catch {}
@@ -5708,17 +5947,28 @@ function prune(map, now, windowMs) {
5708
5947
  */
5709
5948
  function recordDeny(tool, input, opts) {
5710
5949
  const hash = denyHash(tool, input);
5950
+ const sid = opts.sessionId?.trim();
5951
+ const key = sid ? `${hash}::${sid}` : hash;
5711
5952
  const path = join(opts.dir, SIDECAR);
5712
5953
  const map = prune(loadMap(path), opts.now, opts.windowMs);
5713
- const res = denyLoopCheck(hash, map, opts);
5714
- map[hash] = {
5715
- count: res.count,
5716
- lastTs: opts.now
5954
+ const res = denyLoopCheck(key, map, {
5955
+ now: opts.now,
5956
+ windowMs: opts.windowMs,
5957
+ dedupMs: sid ? BURST_DEDUP_MS : 0
5958
+ });
5959
+ if (!res.deduped) {
5960
+ map[key] = {
5961
+ count: res.count,
5962
+ lastTs: opts.now
5963
+ };
5964
+ try {
5965
+ atomicWrite(path, JSON.stringify(map));
5966
+ } catch {}
5967
+ }
5968
+ return {
5969
+ ...res,
5970
+ hash
5717
5971
  };
5718
- try {
5719
- atomicWrite(path, JSON.stringify(map));
5720
- } catch {}
5721
- return res;
5722
5972
  }
5723
5973
  /**
5724
5974
  * Gate tail: record every block deny; on a repeat, return the enriched prompt.
@@ -5763,12 +6013,14 @@ async function gate(input) {
5763
6013
  const dir = dirname(input.trackFile);
5764
6014
  recordOneShot(prompt, op, {
5765
6015
  now: input.now,
5766
- dir
6016
+ dir,
6017
+ sessionId: input.sessionId
5767
6018
  });
5768
6019
  return withDenyLoop(prompt, input.tool, op, {
5769
6020
  now: input.now,
5770
6021
  dir,
5771
- windowMs: input.windowMs ?? 12e4
6022
+ windowMs: input.windowMs ?? 12e4,
6023
+ sessionId: input.sessionId
5772
6024
  });
5773
6025
  }
5774
6026
  /** Stateless guards, then the trivial fast path, then the stateful APEX gates. */
@@ -5894,7 +6146,7 @@ function docSourceOf$1(tool) {
5894
6146
  function cacheHitText(web, hit) {
5895
6147
  const kb = Math.floor(hit.body.length / 1024) + 1;
5896
6148
  const hours = Math.floor(hit.ageMs / 36e5);
5897
- return web ? `CACHE HIT WebFetch (~${kb}KB economise, cached il y a ${hours}h):\n\n${hit.body}\n\nPour forcer un nouveau fetch, modifie l'URL ou la query.` : `CACHE HIT (~${kb}KB economise, cached il y a ${hours}h): ${hit.body}\n\nReformule pour forcer un re-call.`;
6149
+ return web ? `CACHE HIT WebFetch (~${kb}KB saved, cached ${hours}h ago):\n\n${hit.body}\n\nModify the URL or query to force a fresh fetch.` : `CACHE HIT (~${kb}KB saved, cached ${hours}h ago): ${hit.body}\n\nRephrase to force a re-call.`;
5898
6150
  }
5899
6151
  /**
5900
6152
  * Pre-event MCP interception: serve a fresh cache hit (deny + cached content),
@@ -7038,4 +7290,4 @@ async function handleHook(id, payload, opts) {
7038
7290
  });
7039
7291
  }
7040
7292
  //#endregion
7041
- export { trackSessionChanges as $, trackSkillRead as A, normalizeEvent as At, lessonsStateFileFor as B, seoPostToolUse as C, projectContext as Ct, postTrackingSideEffects as D, defaultStateDir as Dt, securityAdvisory as E, taskContext as Et, runDoctor as F, securityStatePath as Ft, isProject as G, generateEcosystemMap as H, runningVersion as I, todayUtc as It, mergeLines as J, writeTree as K, versionBanner as L, dispatchLifecycle as M, loadSecurityState as Mt, aipilotPostToolUse as N, saveSecurityState as Nt, trackWatchResearch as O, projectHash$1 as Ot, dispatchAipilot as P, securityStateDir as Pt, postEditTypescript as Q, dispatchLessons as R, postEditContext as S, gitContext as St, dispatchMemory as T, promptSubmitContext as Tt, writePluginMap as U, cartoSessionStart as V, generateProjectMap as W, getFileDesc as X, countFiles as Y, listChildren as Z, preCommitGate as _, pruneEmptyDirs as _t, recordActivity as a, trackAgentMemory as at, extractSymbols as b, trimLogFile as bt, MCP_TTL_MS as c, validateSolidGate as ct, isMcpTool as d, detectSolidProfile as dt, validateRulesLoaded as et, queryOf as f, solidDetectStart as ft, gate as g, sessionStartCore as gt, TRIVIAL_BUDGET as h, runSessionStartCleanups as ht, respond as i, validateTeammateOutput as it, trackEnrichment as j, isoUtc as jt, trackMcpResearch as k, trackFile as kt, WEBFETCH_TTL_MS as l, checkFileSize as lt, REQUIRED_AGENTS as m, readRules as mt, activityFor as n, saveApexState as nt, mcpPostStore as o, subagentCacheContext as ot, DEFAULT_WINDOW_MS as p, injectRules as pt, loadEnriched as q, handlePre as r, logToolFailure as rt, mcpPreIntercept as s, validateTailwind as st, handleHook as t, cleanupSession as tt, cacheQueryOf as u, countLoc as ut, detectDuplication as v, purgeTtlTree as vt, seoPostToolUseResponse as w, claudeMdKey as wt, lifecycleStdout as x, devContext as xt, dryGate as y, removeOldFiles as yt, lessonsFileFor as z };
7293
+ export { postEditTypescript as $, trackSkillRead as A, trackFile as At, lessonsFileFor as B, seoPostToolUse as C, gitContext as Ct, postTrackingSideEffects as D, taskContext as Dt, securityAdvisory as E, promptSubmitContext as Et, runDoctor as F, securityStateDir as Ft, generateProjectMap as G, cartoSessionStart as H, runningVersion as I, securityStatePath as It, loadEnriched as J, isProject as K, versionBanner as L, todayUtc as Lt, dispatchLifecycle as M, isoUtc as Mt, aipilotPostToolUse as N, loadSecurityState as Nt, trackWatchResearch as O, defaultStateDir as Ot, dispatchAipilot as P, saveSecurityState as Pt, listChildren as Q, dispatchLessons as R, postEditContext as S, devContext as St, dispatchMemory as T, claudeMdKey as Tt, generateEcosystemMap as U, lessonsStateFileFor as V, writePluginMap as W, countFiles as X, mergeLines as Y, getFileDesc as Z, preCommitGate as _, sessionStartCore as _t, recordActivity as a, validateTeammateOutput as at, extractSymbols as b, removeOldFiles as bt, MCP_TTL_MS as c, validateTailwind as ct, isMcpTool as d, countLoc as dt, trackSessionChanges as et, queryOf as f, detectSolidProfile as ft, gate as g, runSessionStartCleanups as gt, TRIVIAL_BUDGET as h, readRules as ht, respond as i, logToolFailure as it, trackEnrichment as j, normalizeEvent as jt, trackMcpResearch as k, projectHash$1 as kt, WEBFETCH_TTL_MS as l, validateSolidGate as lt, REQUIRED_AGENTS as m, injectRules as mt, activityFor as n, cleanupSession as nt, mcpPostStore as o, trackAgentMemory as ot, DEFAULT_WINDOW_MS as p, solidDetectStart as pt, writeTree as q, handlePre as r, saveApexState as rt, mcpPreIntercept as s, subagentCacheContext as st, handleHook as t, validateRulesLoaded as tt, cacheQueryOf as u, checkFileSize as ut, detectDuplication as v, pruneEmptyDirs as vt, seoPostToolUseResponse as w, projectContext as wt, lifecycleStdout as x, trimLogFile as xt, dryGate as y, purgeTtlTree as yt, lessonsArchiveFileFor as z };
@@ -692,6 +692,8 @@ declare function dispatchLessons(event: string, payload: Record<string, unknown>
692
692
  //#region src/runtime/lifecycle/lessons/state.d.ts
693
693
  /** Absolute `<root>/MEMORY/LESSON.md` — the curated, committable lessons file. */
694
694
  declare function lessonsFileFor(root: string): string;
695
+ /** Absolute `<root>/MEMORY/LESSON-archive.md` — cold storage for capped-out bullets. */
696
+ declare function lessonsArchiveFileFor(root: string): string;
695
697
  /** Absolute `<root>/MEMORY/state.json` — machine-local throttle counter. */
696
698
  declare function lessonsStateFileFor(root: string): string;
697
699
  //#endregion
@@ -826,4 +828,4 @@ interface PreContext {
826
828
  */
827
829
  declare function handlePre(ctx: PreContext): Promise<HandleOutcome>;
828
830
  //#endregion
829
- export { Activity, DEFAULT_WINDOW_MS, DuplicationVerdict, type GateInput, HandleOptions, HandleOutcome, LifecycleInput, MCP_TTL_MS, McpIntercept, NormalizedEvent, PluginScope, PreContext, REQUIRED_AGENTS, SolidProfile, TRIVIAL_BUDGET, ToolEvent, WEBFETCH_TTL_MS, activityFor, aipilotPostToolUse, cacheQueryOf, cartoSessionStart, checkFileSize, claudeHome, claudeMdKey, cleanupSession, countFiles, countLoc, defaultStateDir, detectDuplication, detectSolidProfile, devContext, dispatchAipilot, dispatchLessons, dispatchLifecycle, dispatchMemory, dryGate, extractSymbols, fuseHarnessHome, fusengineCache, gate, generateEcosystemMap, generateProjectMap, getFileDesc, gitContext, handleHook, handlePre, harnessStateDir, injectRules, isMcpTool, isProject, isoUtc, lessonsFileFor, lessonsStateFileFor, lifecycleStdout, listChildren, loadEnriched, loadSecurityState, loadSessionState, logToolFailure, mcpPostStore, mcpPreIntercept, mergeLines, normalizeEvent, postEditContext, postEditTypescript, postTrackingSideEffects, preCommitGate, projectContext, projectHash, promptSubmitContext, pruneEmptyDirs, purgeTtlTree, queryOf, readRules, recordActivity, removeOldFiles, respond, runSessionStartCleanups, sanitizeSessionId, saveApexState, saveSecurityState, saveSessionState, securityAdvisory, securityStateDir, securityStatePath, seoPostToolUse, seoPostToolUseResponse, sessionStartCore, sessionStatePath, sessionsDir, solidDetectStart, subagentCacheContext, taskContext, todayUtc, trackAgentMemory, trackEnrichment, trackFile, trackMcpResearch, trackSessionChanges, trackSkillRead, trackWatchResearch, trimLogFile, validateRulesLoaded, validateSolidGate, validateTailwind, validateTeammateOutput, writePluginMap, writeTree };
831
+ export { Activity, DEFAULT_WINDOW_MS, DuplicationVerdict, type GateInput, HandleOptions, HandleOutcome, LifecycleInput, MCP_TTL_MS, McpIntercept, NormalizedEvent, PluginScope, PreContext, REQUIRED_AGENTS, SolidProfile, TRIVIAL_BUDGET, ToolEvent, WEBFETCH_TTL_MS, activityFor, aipilotPostToolUse, cacheQueryOf, cartoSessionStart, checkFileSize, claudeHome, claudeMdKey, cleanupSession, countFiles, countLoc, defaultStateDir, detectDuplication, detectSolidProfile, devContext, dispatchAipilot, dispatchLessons, dispatchLifecycle, dispatchMemory, dryGate, extractSymbols, fuseHarnessHome, fusengineCache, gate, generateEcosystemMap, generateProjectMap, getFileDesc, gitContext, handleHook, handlePre, harnessStateDir, injectRules, isMcpTool, isProject, isoUtc, lessonsArchiveFileFor, lessonsFileFor, lessonsStateFileFor, lifecycleStdout, listChildren, loadEnriched, loadSecurityState, loadSessionState, logToolFailure, mcpPostStore, mcpPreIntercept, mergeLines, normalizeEvent, postEditContext, postEditTypescript, postTrackingSideEffects, preCommitGate, projectContext, projectHash, promptSubmitContext, pruneEmptyDirs, purgeTtlTree, queryOf, readRules, recordActivity, removeOldFiles, respond, runSessionStartCleanups, sanitizeSessionId, saveApexState, saveSecurityState, saveSessionState, securityAdvisory, securityStateDir, securityStatePath, seoPostToolUse, seoPostToolUseResponse, sessionStartCore, sessionStatePath, sessionsDir, solidDetectStart, subagentCacheContext, taskContext, todayUtc, trackAgentMemory, trackEnrichment, trackFile, trackMcpResearch, trackSessionChanges, trackSkillRead, trackWatchResearch, trimLogFile, validateRulesLoaded, validateSolidGate, validateTailwind, validateTeammateOutput, writePluginMap, writeTree };
@@ -1,6 +1,6 @@
1
1
  import { r as projectLayout } from "../layout-C0jaaCQC.mjs";
2
2
  import { a as sanitizeSessionId, c as sessionsDir, i as loadSessionState, n as fuseHarnessHome, o as saveSessionState, r as fusengineCache, s as sessionStatePath, t as claudeHome } from "../home-state-D0RLWP8J.mjs";
3
- import { $ as trackSessionChanges, A as trackSkillRead, At as normalizeEvent, B as lessonsStateFileFor, C as seoPostToolUse, Ct as projectContext, D as postTrackingSideEffects, Dt as defaultStateDir, E as securityAdvisory, Et as taskContext, Ft as securityStatePath, G as isProject, H as generateEcosystemMap, It as todayUtc, J as mergeLines, K as writeTree, M as dispatchLifecycle, Mt as loadSecurityState, N as aipilotPostToolUse, Nt as saveSecurityState, O as trackWatchResearch, Ot as projectHash, P as dispatchAipilot, Pt as securityStateDir, Q as postEditTypescript, R as dispatchLessons, S as postEditContext, St as gitContext, T as dispatchMemory, Tt as promptSubmitContext, U as writePluginMap, V as cartoSessionStart, W as generateProjectMap, X as getFileDesc, Y as countFiles, Z as listChildren, _ as preCommitGate, _t as pruneEmptyDirs, a as recordActivity, at as trackAgentMemory, b as extractSymbols, bt as trimLogFile, c as MCP_TTL_MS, ct as validateSolidGate, d as isMcpTool, dt as detectSolidProfile, et as validateRulesLoaded, f as queryOf, ft as solidDetectStart, g as gate, gt as sessionStartCore, h as TRIVIAL_BUDGET, ht as runSessionStartCleanups, i as respond, it as validateTeammateOutput, j as trackEnrichment, jt as isoUtc, k as trackMcpResearch, kt as trackFile, l as WEBFETCH_TTL_MS, lt as checkFileSize, m as REQUIRED_AGENTS, mt as readRules, n as activityFor, nt as saveApexState, o as mcpPostStore, ot as subagentCacheContext, p as DEFAULT_WINDOW_MS, pt as injectRules, q as loadEnriched, r as handlePre, rt as logToolFailure, s as mcpPreIntercept, st as validateTailwind, t as handleHook, tt as cleanupSession, u as cacheQueryOf, ut as countLoc, v as detectDuplication, vt as purgeTtlTree, w as seoPostToolUseResponse, wt as claudeMdKey, x as lifecycleStdout, xt as devContext, y as dryGate, yt as removeOldFiles, z as lessonsFileFor } from "../handle-D93-VIGS.mjs";
3
+ import { $ as postEditTypescript, A as trackSkillRead, At as trackFile, B as lessonsFileFor, C as seoPostToolUse, Ct as gitContext, D as postTrackingSideEffects, Dt as taskContext, E as securityAdvisory, Et as promptSubmitContext, Ft as securityStateDir, G as generateProjectMap, H as cartoSessionStart, It as securityStatePath, J as loadEnriched, K as isProject, Lt as todayUtc, M as dispatchLifecycle, Mt as isoUtc, N as aipilotPostToolUse, Nt as loadSecurityState, O as trackWatchResearch, Ot as defaultStateDir, P as dispatchAipilot, Pt as saveSecurityState, Q as listChildren, R as dispatchLessons, S as postEditContext, St as devContext, T as dispatchMemory, Tt as claudeMdKey, U as generateEcosystemMap, V as lessonsStateFileFor, W as writePluginMap, X as countFiles, Y as mergeLines, Z as getFileDesc, _ as preCommitGate, _t as sessionStartCore, a as recordActivity, at as validateTeammateOutput, b as extractSymbols, bt as removeOldFiles, c as MCP_TTL_MS, ct as validateTailwind, d as isMcpTool, dt as countLoc, et as trackSessionChanges, f as queryOf, ft as detectSolidProfile, g as gate, gt as runSessionStartCleanups, h as TRIVIAL_BUDGET, ht as readRules, i as respond, it as logToolFailure, j as trackEnrichment, jt as normalizeEvent, k as trackMcpResearch, kt as projectHash, l as WEBFETCH_TTL_MS, lt as validateSolidGate, m as REQUIRED_AGENTS, mt as injectRules, n as activityFor, nt as cleanupSession, o as mcpPostStore, ot as trackAgentMemory, p as DEFAULT_WINDOW_MS, pt as solidDetectStart, q as writeTree, r as handlePre, rt as saveApexState, s as mcpPreIntercept, st as subagentCacheContext, t as handleHook, tt as validateRulesLoaded, u as cacheQueryOf, ut as checkFileSize, v as detectDuplication, vt as pruneEmptyDirs, w as seoPostToolUseResponse, wt as projectContext, x as lifecycleStdout, xt as trimLogFile, y as dryGate, yt as purgeTtlTree, z as lessonsArchiveFileFor } from "../handle-L4ZNmpwN.mjs";
4
4
  //#region src/runtime/storage.ts
5
5
  /**
6
6
  * The project's single state dir (`<root>/.harness`) — neutral + harness-agnostic,
@@ -10,4 +10,4 @@ function harnessStateDir(root) {
10
10
  return projectLayout(root).stateDir;
11
11
  }
12
12
  //#endregion
13
- export { DEFAULT_WINDOW_MS, MCP_TTL_MS, REQUIRED_AGENTS, TRIVIAL_BUDGET, WEBFETCH_TTL_MS, activityFor, aipilotPostToolUse, cacheQueryOf, cartoSessionStart, checkFileSize, claudeHome, claudeMdKey, cleanupSession, countFiles, countLoc, defaultStateDir, detectDuplication, detectSolidProfile, devContext, dispatchAipilot, dispatchLessons, dispatchLifecycle, dispatchMemory, dryGate, extractSymbols, fuseHarnessHome, fusengineCache, gate, generateEcosystemMap, generateProjectMap, getFileDesc, gitContext, handleHook, handlePre, harnessStateDir, injectRules, isMcpTool, isProject, isoUtc, lessonsFileFor, lessonsStateFileFor, lifecycleStdout, listChildren, loadEnriched, loadSecurityState, loadSessionState, logToolFailure, mcpPostStore, mcpPreIntercept, mergeLines, normalizeEvent, postEditContext, postEditTypescript, postTrackingSideEffects, preCommitGate, projectContext, projectHash, promptSubmitContext, pruneEmptyDirs, purgeTtlTree, queryOf, readRules, recordActivity, removeOldFiles, respond, runSessionStartCleanups, sanitizeSessionId, saveApexState, saveSecurityState, saveSessionState, securityAdvisory, securityStateDir, securityStatePath, seoPostToolUse, seoPostToolUseResponse, sessionStartCore, sessionStatePath, sessionsDir, solidDetectStart, subagentCacheContext, taskContext, todayUtc, trackAgentMemory, trackEnrichment, trackFile, trackMcpResearch, trackSessionChanges, trackSkillRead, trackWatchResearch, trimLogFile, validateRulesLoaded, validateSolidGate, validateTailwind, validateTeammateOutput, writePluginMap, writeTree };
13
+ export { DEFAULT_WINDOW_MS, MCP_TTL_MS, REQUIRED_AGENTS, TRIVIAL_BUDGET, WEBFETCH_TTL_MS, activityFor, aipilotPostToolUse, cacheQueryOf, cartoSessionStart, checkFileSize, claudeHome, claudeMdKey, cleanupSession, countFiles, countLoc, defaultStateDir, detectDuplication, detectSolidProfile, devContext, dispatchAipilot, dispatchLessons, dispatchLifecycle, dispatchMemory, dryGate, extractSymbols, fuseHarnessHome, fusengineCache, gate, generateEcosystemMap, generateProjectMap, getFileDesc, gitContext, handleHook, handlePre, harnessStateDir, injectRules, isMcpTool, isProject, isoUtc, lessonsArchiveFileFor, lessonsFileFor, lessonsStateFileFor, lifecycleStdout, listChildren, loadEnriched, loadSecurityState, loadSessionState, logToolFailure, mcpPostStore, mcpPreIntercept, mergeLines, normalizeEvent, postEditContext, postEditTypescript, postTrackingSideEffects, preCommitGate, projectContext, projectHash, promptSubmitContext, pruneEmptyDirs, purgeTtlTree, queryOf, readRules, recordActivity, removeOldFiles, respond, runSessionStartCleanups, sanitizeSessionId, saveApexState, saveSecurityState, saveSessionState, securityAdvisory, securityStateDir, securityStatePath, seoPostToolUse, seoPostToolUseResponse, sessionStartCore, sessionStatePath, sessionsDir, solidDetectStart, subagentCacheContext, taskContext, todayUtc, trackAgentMemory, trackEnrichment, trackFile, trackMcpResearch, trackSessionChanges, trackSkillRead, trackWatchResearch, trimLogFile, validateRulesLoaded, validateSolidGate, validateTailwind, validateTeammateOutput, writePluginMap, writeTree };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusengine/harness",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
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",