@fusengine/harness 0.1.49 → 0.1.50

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 { Mt as todayUtc, t as handleHook } from "../handle-D1hHqHvh.mjs";
7
+ import { Nt as todayUtc, t as handleHook } from "../handle-DzPjaPS-.mjs";
8
8
  import { delimiter, dirname, join } from "node:path";
9
9
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
10
10
  import { homedir } from "node:os";
@@ -327,18 +327,81 @@ function designLifecycle(payload, cacheDir, cwd, stamp, now) {
327
327
  }
328
328
  return false;
329
329
  }
330
+ /** Sidecar basename under the per-project state dir. */
331
+ const SIDECAR = "inject-dedup.json";
332
+ /** Load the `{ key -> epochMs }` map, or `{}` when missing/corrupt. */
333
+ function loadMap(path) {
334
+ try {
335
+ if (!existsSync(path)) return {};
336
+ const data = JSON.parse(readFileSync(path, "utf8"));
337
+ return data && typeof data === "object" && !Array.isArray(data) ? data : {};
338
+ } catch {
339
+ return {};
340
+ }
341
+ }
342
+ /** Keep only entries newer than `windowMs` before `now` (bounds sidecar size). */
343
+ function prune(map, now, windowMs) {
344
+ const out = {};
345
+ for (const [k, t] of Object.entries(map)) if (typeof t === "number" && now - t < windowMs) out[k] = t;
346
+ return out;
347
+ }
348
+ /**
349
+ * Cooldown gate. Returns `true` when `key` has NOT been recorded within the last
350
+ * `windowMs` (the caller MAY emit — and the emission is recorded now), or `false`
351
+ * when it was (the caller SHOULD suppress). The first call in a window wins;
352
+ * subsequent identical keys are throttled until the window elapses.
353
+ *
354
+ * Fails open: if the sidecar is unwritable, the emission is allowed rather than
355
+ * silently dropping context.
356
+ * @param key - Stable identity of the block (e.g. a content hash, or `lesson:<id>`).
357
+ * @param windowMs - Suppression window in ms.
358
+ * @param opts - Optional clock + state-dir overrides (for tests).
359
+ * @returns `true` to proceed/emit, `false` to suppress.
360
+ */
361
+ function oncePerWindow(key, windowMs, opts = {}) {
362
+ const now = opts.now ?? Date.now();
363
+ const path = join(opts.dir ?? defaultStateDir(), SIDECAR);
364
+ const map = prune(loadMap(path), now, windowMs);
365
+ const last = map[key];
366
+ if (typeof last === "number" && now - last < windowMs) return false;
367
+ map[key] = now;
368
+ try {
369
+ atomicWrite(path, JSON.stringify(map));
370
+ } catch {}
371
+ return true;
372
+ }
330
373
  //#endregion
331
374
  //#region src/runtime/inject-context.ts
332
375
  /**
376
+ * Build the {@link oncePerWindow} key for the CLAUDE.md preamble gate. The
377
+ * prompt hash keeps two distinct legitimate turns from colliding — even non-dev
378
+ * prompts, whose block is prompt-independent (just CLAUDE.md) and would
379
+ * otherwise hash-collide within the window — while the content hash still lets a
380
+ * same-turn double-fire of an identical block be suppressed. Single source of
381
+ * truth so the owner invariant test guards the real production key.
382
+ * @param prompt - The raw user prompt.
383
+ * @param ctx - The rendered CLAUDE.md (+ optional APEX) block.
384
+ * @returns The namespaced dedup key.
385
+ */
386
+ function claudeMdKey(prompt, ctx) {
387
+ return `claude-md:${hashText(prompt)}:${hashText(ctx)}`;
388
+ }
389
+ /**
333
390
  * UserPromptSubmit context injection: render the CLAUDE.md (+ optional APEX)
334
391
  * preamble as a Claude `additionalContext` response, or "" when nothing to emit.
392
+ * Guarded by {@link oncePerWindow} via {@link claudeMdKey}: only a
393
+ * near-simultaneous double-fire of the SAME turn (identical prompt AND identical
394
+ * block, within {@link DEDUP_WINDOW_MS}) is suppressed. The invariant "CLAUDE.md
395
+ * is emitted on EVERY message" is thus preserved.
335
396
  * @param prompt - The raw user prompt.
336
397
  * @param cwd - Project root (for project-type detection).
337
398
  * @returns The native hook stdout (possibly empty).
338
399
  */
339
400
  function promptSubmitContext(prompt, cwd) {
340
401
  const ctx = buildClaudeMdContext(prompt, cwd);
341
- return ctx ? contextResponse("UserPromptSubmit", ctx) : "";
402
+ if (!ctx) return "";
403
+ if (!oncePerWindow(claudeMdKey(prompt, ctx), 3e3)) return "";
404
+ return contextResponse("UserPromptSubmit", ctx);
342
405
  }
343
406
  /**
344
407
  * PreToolUse Task context injection: render the APEX sub-agent context as a
@@ -867,6 +930,87 @@ function subagentCacheContext(sessionIdRaw, home = homedir(), env = process.env,
867
930
  return fresh.length ? contextResponse("SubagentStart", render(fresh)) : "";
868
931
  }
869
932
  //#endregion
933
+ //#region src/runtime/lifecycle/agent-files.ts
934
+ /**
935
+ * Per-agent file attribution for SubagentStop. Parses a sub-agent's OWN
936
+ * transcript (`agent_transcript_path` — the clean JSONL the Claude Code platform
937
+ * writes for that specific sub-agent, added in CLI v2.0.42) to recover the exact
938
+ * set of files it wrote via Write/Edit/MultiEdit/NotebookEdit. This lets the
939
+ * sniper reminder attribute only the files THIS agent touched, instead of every
940
+ * file changed in the whole session (which cross-attributes other teammates'
941
+ * work — the bug this fixes). Per LESSON.md, the SubagentStop transcript is the
942
+ * reliable anchor; sidechain PostToolUse hooks are not (issues #43612/#34692).
943
+ */
944
+ /** Tools whose `input.file_path`/`notebook_path` names a file the agent authored. */
945
+ const WRITE_TOOLS = /* @__PURE__ */ new Set([
946
+ "Write",
947
+ "Edit",
948
+ "MultiEdit",
949
+ "NotebookEdit"
950
+ ]);
951
+ /**
952
+ * Return the distinct list of file paths written by the sub-agent whose own
953
+ * transcript is at `transcriptPath`, by scanning its Write/Edit/MultiEdit/
954
+ * NotebookEdit `tool_use` blocks. Distinguishes "no writes" from "cannot read":
955
+ *
956
+ * - `null` — the path is absent, or the transcript is unreadable. The caller
957
+ * MUST fall back to the session-wide list (fail-open: never drop the sniper
958
+ * reminder just because a transcript could not be parsed — no regression vs.
959
+ * the pre-fix behavior).
960
+ * - `string[]` (possibly empty) — the transcript was read; every write was
961
+ * collected. An empty array means this agent authored no files, so it should
962
+ * NOT be told to validate files other agents changed.
963
+ *
964
+ * @param transcriptPath - Absolute path to the sub-agent's own `.jsonl`
965
+ * transcript (SubagentStop payload field `agent_transcript_path`).
966
+ * @returns Distinct written paths (order-preserving), or `null` when unreadable.
967
+ */
968
+ function filesWrittenByAgent(transcriptPath) {
969
+ if (!transcriptPath) return null;
970
+ let text;
971
+ try {
972
+ text = readText(transcriptPath);
973
+ } catch {
974
+ return null;
975
+ }
976
+ const out = [];
977
+ const seen = /* @__PURE__ */ new Set();
978
+ for (const line of text.split("\n")) {
979
+ if (!line.trim()) continue;
980
+ let entry;
981
+ try {
982
+ entry = JSON.parse(line);
983
+ } catch {
984
+ continue;
985
+ }
986
+ const content = entry.message?.content;
987
+ if (!Array.isArray(content)) continue;
988
+ for (const block of content) {
989
+ if (block?.type !== "tool_use" || !block.name || !WRITE_TOOLS.has(block.name)) continue;
990
+ const fp = block.input?.file_path ?? block.input?.notebook_path;
991
+ if (typeof fp === "string" && fp && !seen.has(fp)) {
992
+ seen.add(fp);
993
+ out.push(fp);
994
+ }
995
+ }
996
+ }
997
+ return out;
998
+ }
999
+ /**
1000
+ * Filter `sessionFiles` (from unified state `.changes.modifiedFiles`) down to the
1001
+ * ones this agent actually wrote. Matches on the exact recorded string first,
1002
+ * then tolerantly on `basename` so a relative-vs-absolute path drift between the
1003
+ * PostToolUse record and the transcript input never loses an owned file.
1004
+ * @param sessionFiles - Session-wide modified files.
1005
+ * @param written - Paths the agent wrote (from {@link filesWrittenByAgent}).
1006
+ * @returns The subset of `sessionFiles` attributable to this agent.
1007
+ */
1008
+ function attributeFiles(sessionFiles, written) {
1009
+ const exact = new Set(written);
1010
+ const bases = new Set(written.map((f) => basename(f)));
1011
+ return sessionFiles.filter((f) => exact.has(f) || bases.has(basename(f)));
1012
+ }
1013
+ //#endregion
870
1014
  //#region src/runtime/lifecycle/agent-memory.ts
871
1015
  /** `~/.claude/memory/agents` — agent completion history dir. */
872
1016
  function memoryDir(home) {
@@ -903,17 +1047,19 @@ function trackAgentMemory(data, home = homedir(), now = Date.now()) {
903
1047
  if (SKIP_AGENTS.test(agentType)) return JSON.stringify({ message: `Agent ${agentType} completed` });
904
1048
  const state = loadSessionState(sessionId, home);
905
1049
  const changes = state.changes;
906
- const count = changes?.cumulativeCodeFiles ?? 0;
907
- if (count > 0) {
908
- const files = (changes?.modifiedFiles ?? []).join(", ");
909
- saveSessionState(sessionId, {
910
- ...state,
911
- changes: {
912
- ...changes,
913
- cumulativeCodeFiles: 0
914
- }
915
- }, home);
916
- return contextResponse("SubagentStop", `SNIPER VALIDATION REQUIRED: Agent '${agentType}' modified ${count} code file(s): ${files}. Run sniper agent now.`);
1050
+ if ((changes?.cumulativeCodeFiles ?? 0) > 0) {
1051
+ const written = filesWrittenByAgent(typeof data.agent_transcript_path === "string" ? data.agent_transcript_path : void 0);
1052
+ const owned = written === null ? changes?.modifiedFiles ?? [] : attributeFiles(changes?.modifiedFiles ?? [], written);
1053
+ if (owned.length > 0) {
1054
+ saveSessionState(sessionId, {
1055
+ ...state,
1056
+ changes: {
1057
+ ...changes,
1058
+ cumulativeCodeFiles: 0
1059
+ }
1060
+ }, home);
1061
+ return contextResponse("SubagentStop", `SNIPER VALIDATION REQUIRED: Agent '${agentType}' modified ${owned.length} code file(s): ${owned.join(", ")}. Run sniper agent now.`);
1062
+ }
917
1063
  }
918
1064
  return JSON.stringify({ message: `Agent ${agentType} completed (no code changes)` });
919
1065
  }
@@ -1555,6 +1701,109 @@ function cartoSessionStart(cwd, now = Date.now()) {
1555
1701
  return ctx ? contextResponse("SessionStart", ctx) : "";
1556
1702
  }
1557
1703
  //#endregion
1704
+ //#region src/runtime/lifecycle/aipilot/curate-lessons.ts
1705
+ /**
1706
+ * Mechanical, LLM-free curation of `MEMORY/LESSON.md` bullets (anti-obesity):
1707
+ * strict-dedup near-identical bullets (keep newest, `[TRIGGERS …]` preserved),
1708
+ * flag over-cap + stale (>90d, cited path gone) in a report. Only dedup writes.
1709
+ */
1710
+ const CAP = 50;
1711
+ const STALE_DAYS = 90;
1712
+ const SIM_THRESHOLD = .8;
1713
+ const MIN_TOKENS = 4;
1714
+ const TRIG = /^\[TRIGGERS\s+.+\]$/;
1715
+ /** Epoch ms for a `[YYYY-MM-DD HH:MM]` stamp; `NaN` if absent or out of range. */
1716
+ function parseTs$1(line) {
1717
+ const m = line.match(/\[(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}))?/);
1718
+ if (!m) return NaN;
1719
+ const mo = +(m[2] ?? 0), d = +(m[3] ?? 0);
1720
+ if (mo < 1 || mo > 12 || d < 1 || d > 31) return NaN;
1721
+ return Date.UTC(+(m[1] ?? 0), mo - 1, d, +(m[4] ?? 0), +(m[5] ?? 0));
1722
+ }
1723
+ /** Content words (>=4 chars), timestamp & TRIGGERS marker stripped. */
1724
+ function tokenize(text) {
1725
+ return new Set(text.toLowerCase().replace(/\[triggers[^\]]*\]/g, " ").replace(/\[\d{4}-\d{2}-\d{2}[^\]]*\]/g, " ").replace(/[^a-z0-9àâäéèêëîïôöùûüç/._-]+/gi, " ").split(/\s+/).filter((t) => t.length >= 4));
1726
+ }
1727
+ /** Jaccard overlap of two token sets (0 when both empty). */
1728
+ function jaccard(a, b) {
1729
+ if (a.size === 0 && b.size === 0) return 0;
1730
+ const inter = [...a].filter((t) => b.has(t)).length;
1731
+ return inter / (a.size + b.size - inter);
1732
+ }
1733
+ /** Repo-relative cited paths (slash + extension) referenced in a block. */
1734
+ function citedPaths(text) {
1735
+ const out = /* @__PURE__ */ new Set();
1736
+ for (const m of text.matchAll(/`([^`]+)`/g)) if (m[1]) out.add(m[1]);
1737
+ for (const m of text.matchAll(/[\w./@-]+\.\w{1,5}/g)) if (m[0]) out.add(m[0]);
1738
+ return [...out].filter((p) => p.includes("/") && /\.\w{1,5}$/.test(p));
1739
+ }
1740
+ /** Split into a verbatim preamble and one Block per `- ` bullet. */
1741
+ function parse(content) {
1742
+ const lines = content.split("\n");
1743
+ const blocks = [];
1744
+ let i = 0;
1745
+ while (i < lines.length && !/^-\s/.test(lines[i] ?? "")) i++;
1746
+ const preamble = lines.slice(0, i).join("\n");
1747
+ for (; i < lines.length; i++) {
1748
+ const l = lines[i] ?? "", last = blocks[blocks.length - 1];
1749
+ if (/^-\s/.test(l)) blocks.push({
1750
+ raw: [l],
1751
+ ts: parseTs$1(l),
1752
+ tokens: tokenize(l)
1753
+ });
1754
+ else if (l.trim() && last) last.raw.push(l);
1755
+ }
1756
+ return {
1757
+ preamble,
1758
+ blocks
1759
+ };
1760
+ }
1761
+ /** Report lines for bullets older than STALE_DAYS whose only cited path is gone. */
1762
+ function staleReport(blocks, now, root) {
1763
+ const cutoff = now - STALE_DAYS * 864e5;
1764
+ return blocks.flatMap((b) => {
1765
+ if (!(b.ts <= cutoff)) return [];
1766
+ const paths = citedPaths(b.raw.join(" "));
1767
+ if (paths.length === 0 || paths.some((p) => existsSync(join(root, p)))) return [];
1768
+ return [`[STALE?] ${(b.raw[0] ?? "").slice(0, 90)} — chemin(s) disparu(s): ${paths.join(", ")}`];
1769
+ });
1770
+ }
1771
+ /**
1772
+ * Strict-dedup LESSON.md bullets (keep newest, its `[TRIGGERS …]` line preserved —
1773
+ * or carried over from the dropped twin if the kept one lacks it) and report
1774
+ * cap/stale. `content` unchanged unless a dedup occurred. Returns content + report.
1775
+ */
1776
+ function curateLessons(content, now, root = process.cwd()) {
1777
+ const { preamble, blocks } = parse(content);
1778
+ const kept = [];
1779
+ const fused = [];
1780
+ for (const b of blocks) {
1781
+ const hit = b.tokens.size >= MIN_TOKENS ? kept.find((k) => k.tokens.size >= MIN_TOKENS && jaccard(k.tokens, b.tokens) >= SIM_THRESHOLD) : void 0;
1782
+ if (!hit) {
1783
+ kept.push(b);
1784
+ continue;
1785
+ }
1786
+ const [win, drop] = b.ts > hit.ts || Number.isNaN(hit.ts) ? [b, hit] : [hit, b];
1787
+ if (win !== hit) kept[kept.indexOf(hit)] = win;
1788
+ if (!win.raw.some((l) => TRIG.test(l.trim()))) {
1789
+ const t = drop.raw.find((l) => TRIG.test(l.trim()));
1790
+ if (t) win.raw.push(t);
1791
+ }
1792
+ fused.push(`fusion: gardé ${(win.raw[0] ?? "").slice(0, 60)} · retiré ${(drop.raw[0] ?? "").slice(0, 60)}`);
1793
+ }
1794
+ const old = [...kept].sort((a, b) => a.ts - b.ts).slice(0, Math.max(0, kept.length - CAP));
1795
+ const cap = old.length ? [`${kept.length} bullets (> ${CAP}) — plus anciens candidats à l'archivage:`, ...old.map((b) => ` ${(b.raw[0] ?? "").slice(0, 80)}`)] : [];
1796
+ const report = [
1797
+ ...fused,
1798
+ ...cap,
1799
+ ...staleReport(blocks, now, root)
1800
+ ].join("\n");
1801
+ return {
1802
+ content: fused.length ? `${preamble}\n${kept.map((b) => b.raw.join("\n")).join("\n\n")}\n` : content,
1803
+ report
1804
+ };
1805
+ }
1806
+ //#endregion
1558
1807
  //#region src/runtime/lifecycle/lessons/state.ts
1559
1808
  /**
1560
1809
  * Per-project lessons paths. The `fuse-lessons` plugin stores its lessons under
@@ -1579,9 +1828,10 @@ function lessonsStateFileFor(root) {
1579
1828
  * across every project with unsaved code edits; PostToolUse marks the write to
1580
1829
  * arm/silence the per-project throttle. Non-fatal by design.
1581
1830
  */
1582
- /** Inject `<root>/MEMORY/LESSON.md` as additionalContext for `event`. */
1583
- function injectMemory(cwd, event) {
1584
- const file = lessonsFileFor(projectRoot(cwd));
1831
+ /** 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). */
1832
+ function injectMemory(cwd, event, now) {
1833
+ const root = projectRoot(cwd);
1834
+ const file = lessonsFileFor(root);
1585
1835
  if (!existsSync(file)) return "";
1586
1836
  let content = "";
1587
1837
  try {
@@ -1590,7 +1840,13 @@ function injectMemory(cwd, event) {
1590
1840
  return "";
1591
1841
  }
1592
1842
  if (!content) return "";
1593
- return contextResponse(event, `Project lessons never reproduce these:\n${content}\nYou may append OR refine/merge/dedupe bullets in MEMORY/LESSON.md — keep it terse.`);
1843
+ const { content: curated, report } = curateLessons(content, now, root);
1844
+ if (curated !== content) try {
1845
+ atomicWrite(file, curated);
1846
+ content = curated;
1847
+ } catch {}
1848
+ const ctx = `Project lessons — never reproduce these:\n${content}\nYou may append OR refine/merge/dedupe bullets in MEMORY/LESSON.md — keep it terse.`;
1849
+ return report ? attachSystemMessage(contextResponse(event, ctx), `LESSON.md curation:\n${report}`) : contextResponse(event, ctx);
1594
1850
  }
1595
1851
  /** Select roots with unsaved code edits past the throttle, bumping their state. */
1596
1852
  function collectPending(now, window) {
@@ -1638,7 +1894,7 @@ function markWrite(payload, now) {
1638
1894
  function dispatchLessons(event, payload, cwd, now) {
1639
1895
  switch (event) {
1640
1896
  case "SessionStart":
1641
- case "SubagentStart": return injectMemory(cwd, event);
1897
+ case "SubagentStart": return injectMemory(cwd, event, now);
1642
1898
  case "Stop": return remindWrite(now);
1643
1899
  case "PostToolUse":
1644
1900
  markWrite(payload, now);
@@ -5159,6 +5415,206 @@ function designGate(payload, event, cacheDir, cwd) {
5159
5415
  return null;
5160
5416
  }
5161
5417
  //#endregion
5418
+ //#region src/policy/lessons/trigger-index.ts
5419
+ /**
5420
+ * Compile the triggered-lesson index from `MEMORY/LESSON.md`. A lesson is a
5421
+ * bullet (`- [YYYY-MM-DD HH:MM] ...`); it opts into decision-time injection by
5422
+ * ending with a `[TRIGGERS tool:.. path:.. error:.. keyword:..]` line. Lessons
5423
+ * WITHOUT that tag are skipped here (they keep the SessionStart block behavior —
5424
+ * zero regression). Parsed once per file version (mtime-memoized).
5425
+ */
5426
+ /** Matches a trailing `[TRIGGERS ...]` line (its body captured). */
5427
+ const TRIGGER_RE = /^\[TRIGGERS\s+(.+?)\]$/;
5428
+ /** Comma list for `key:` in a trigger body (values are space-delimited). */
5429
+ function list(body, key) {
5430
+ const val = body.match(new RegExp(`\\b${key}:([^\\s\\]]+)`))?.[1];
5431
+ return val ? val.split(",").filter(Boolean) : [];
5432
+ }
5433
+ /** Parse a `[TRIGGERS ...]` body into predicates (error is a single regex). */
5434
+ function parseTriggers(body) {
5435
+ const err = body.match(/\berror:([^\s\]]+)/);
5436
+ return {
5437
+ tools: list(body, "tool"),
5438
+ paths: list(body, "path"),
5439
+ error: err?.[1],
5440
+ keywords: list(body, "keyword")
5441
+ };
5442
+ }
5443
+ /** Collapse to a single ≤3-line compact string (cap length). */
5444
+ function compact(text) {
5445
+ const one = text.replace(/\s+/g, " ").trim();
5446
+ return one.length > 280 ? `${one.slice(0, 277)}…` : one;
5447
+ }
5448
+ /**
5449
+ * Parse LESSON.md content into triggered entries. A bullet's text spans its
5450
+ * `- ` line plus any following non-blank continuation lines up to the next
5451
+ * bullet; a `[TRIGGERS ...]` continuation line arms it.
5452
+ * @param content - Raw LESSON.md text.
5453
+ * @returns Entries that declared triggers (others skipped).
5454
+ */
5455
+ function parseLessons(content) {
5456
+ const lines = content.split("\n");
5457
+ const out = [];
5458
+ for (let i = 0; i < lines.length; i++) {
5459
+ const line = lines[i];
5460
+ if (line === void 0 || !line.startsWith("- ")) continue;
5461
+ let text = line.slice(2);
5462
+ let triggers = null;
5463
+ for (let j = i + 1; j < lines.length; j++) {
5464
+ const cont = lines[j];
5465
+ if (cont === void 0 || cont.trim() === "" || cont.startsWith("- ")) break;
5466
+ const body = cont.trim().match(TRIGGER_RE)?.[1];
5467
+ if (body !== void 0) triggers = parseTriggers(body);
5468
+ else text += ` ${cont.trim()}`;
5469
+ }
5470
+ if (triggers) out.push({
5471
+ text: compact(text),
5472
+ triggers
5473
+ });
5474
+ }
5475
+ return out;
5476
+ }
5477
+ let memo = null;
5478
+ /**
5479
+ * Compile (once per file version) the triggered-lesson index from `file`.
5480
+ * Memoized by path+mtime: re-parses only when LESSON.md changes.
5481
+ * @param file - Absolute path to MEMORY/LESSON.md.
5482
+ * @returns The compiled entries (missing/unreadable file → empty).
5483
+ */
5484
+ function lessonIndex(file) {
5485
+ let key;
5486
+ try {
5487
+ key = `${file}:${statSync(file).mtimeMs}`;
5488
+ } catch {
5489
+ return [];
5490
+ }
5491
+ if (memo?.key === key) return memo.entries;
5492
+ let entries = [];
5493
+ try {
5494
+ entries = parseLessons(readFileSync(file, "utf-8"));
5495
+ } catch {
5496
+ entries = [];
5497
+ }
5498
+ memo = {
5499
+ key,
5500
+ entries
5501
+ };
5502
+ return entries;
5503
+ }
5504
+ /** Glob (`*`/`**`) → RegExp, matching a path segment/tail. */
5505
+ function globToRe(glob) {
5506
+ const esc = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(//g, ".*");
5507
+ return new RegExp(`(^|/)${esc}$`);
5508
+ }
5509
+ /** Safe case-insensitive regex test (absent source or invalid → false). */
5510
+ function safeTest(src, s) {
5511
+ if (!src) return false;
5512
+ try {
5513
+ return new RegExp(src, "i").test(s);
5514
+ } catch {
5515
+ return false;
5516
+ }
5517
+ }
5518
+ /** Score one entry against the call; null = no predicate matched. */
5519
+ function scoreEntry(e, tool, filePath, inputJson, prevError) {
5520
+ const tr = e.triggers;
5521
+ if (tr.tools.includes(tool)) return {
5522
+ entry: e,
5523
+ rank: 3
5524
+ };
5525
+ if (filePath && tr.paths.some((g) => globToRe(g).test(filePath))) return {
5526
+ entry: e,
5527
+ rank: 2
5528
+ };
5529
+ if (prevError && safeTest(tr.error, prevError)) return {
5530
+ entry: e,
5531
+ rank: 1
5532
+ };
5533
+ if (tr.keywords.some((k) => inputJson.includes(k))) return {
5534
+ entry: e,
5535
+ rank: 0
5536
+ };
5537
+ return null;
5538
+ }
5539
+ /** Stable, filesystem-safe cooldown key from a lesson's compact text (djb2). */
5540
+ function cooldownKey(text) {
5541
+ let h = 5381;
5542
+ for (let i = 0; i < text.length; i++) h = (h << 5) + h + text.charCodeAt(i) | 0;
5543
+ return `lesson:${(h >>> 0).toString(36)}`;
5544
+ }
5545
+ /**
5546
+ * The single most-specific lesson for this PreToolUse call, or null. Matching
5547
+ * priority: exact tool > path glob > error regex > input-JSON keyword. Cooldown
5548
+ * suppresses a lesson already injected within the window.
5549
+ * @param tool - The tool being called (e.g. `Write`).
5550
+ * @param toolInput - The raw `tool_input`.
5551
+ * @param opts - Index file, cooldown gate, and optional prior error.
5552
+ * @returns An `inform` prompt, or null when nothing matches / in cooldown.
5553
+ */
5554
+ function lessonFor(tool, toolInput, opts) {
5555
+ const entries = lessonIndex(opts.file);
5556
+ if (entries.length === 0) return null;
5557
+ const filePath = typeof toolInput?.file_path === "string" ? toolInput.file_path : "";
5558
+ const inputJson = JSON.stringify(toolInput ?? {});
5559
+ let best = null;
5560
+ for (const e of entries) {
5561
+ const m = scoreEntry(e, tool, filePath, inputJson, opts.prevError);
5562
+ if (m && (!best || m.rank > best.rank)) best = m;
5563
+ }
5564
+ if (!best) return null;
5565
+ if (!opts.once(cooldownKey(best.entry.text), opts.cooldownMs ?? 18e5)) return null;
5566
+ return {
5567
+ kind: "inform",
5568
+ title: `Project lesson${filePath ? ` (${basename(filePath)})` : ""}`,
5569
+ reason: best.entry.text
5570
+ };
5571
+ }
5572
+ //#endregion
5573
+ //#region src/runtime/pre-allow.ts
5574
+ /**
5575
+ * PreToolUse ALLOW-path response assembly. Reached only after every gate
5576
+ * allowed (a deny/ask already returned upstream), so nothing here can block nor
5577
+ * override a decision. Combines the Python-parity pass notice (systemMessage)
5578
+ * with the single most-specific decision-time lesson (additionalContext).
5579
+ */
5580
+ /**
5581
+ * Build the native outcome for a PreToolUse call that passed every gate: emit a
5582
+ * user-visible pass notice (once per allowed call) and, when its TRIGGERS match
5583
+ * this call, the one cooldown-guarded decision-time lesson. Both channels ride a
5584
+ * single response (lesson → additionalContext, notice → systemMessage).
5585
+ * @param id - Harness id for {@link respond}.
5586
+ * @param event - The normalized PreToolUse event.
5587
+ * @param payload - The raw hook payload (for `agent_id`).
5588
+ * @param mcpDir - MCP state dir backing the pass-notice throttle.
5589
+ * @param cwd - Project root (lesson file + notice scope).
5590
+ * @returns The native hook outcome (empty stdout when nothing to emit).
5591
+ */
5592
+ function allowOutcome(id, event, payload, mcpDir, cwd) {
5593
+ const notice = designPassNotice({
5594
+ agentId: typeof payload.agent_id === "string" ? payload.agent_id : "",
5595
+ tool: event.tool,
5596
+ filePath: event.filePath ?? "",
5597
+ content: event.content ?? "",
5598
+ url: typeof event.input.url === "string" ? event.input.url : "",
5599
+ phase: "pre"
5600
+ }, mcpDir);
5601
+ const lesson = lessonFor(event.tool, event.input, {
5602
+ file: lessonsFileFor(projectRoot(cwd)),
5603
+ once: oncePerWindow
5604
+ });
5605
+ if (lesson) return {
5606
+ stdout: respond(id, notice?.userMessage ? {
5607
+ ...lesson,
5608
+ userMessage: notice.userMessage
5609
+ } : lesson),
5610
+ exit: 0
5611
+ };
5612
+ return {
5613
+ stdout: notice ? respond(id, notice) : "",
5614
+ exit: 0
5615
+ };
5616
+ }
5617
+ //#endregion
5162
5618
  //#region src/runtime/handle-pre.ts
5163
5619
  /**
5164
5620
  * Run the PreToolUse pipeline: MCP/WebFetch cache intercept, design gate, APEX
@@ -5223,18 +5679,7 @@ async function handlePre(ctx) {
5223
5679
  stdout: respond(id, prompt),
5224
5680
  exit: 0
5225
5681
  };
5226
- const notice = designPassNotice({
5227
- agentId: typeof payload.agent_id === "string" ? payload.agent_id : "",
5228
- tool: event.tool,
5229
- filePath: event.filePath ?? "",
5230
- content: event.content ?? "",
5231
- url: typeof event.input.url === "string" ? event.input.url : "",
5232
- phase: "pre"
5233
- }, mcpDir);
5234
- return {
5235
- stdout: notice ? respond(id, notice) : "",
5236
- exit: 0
5237
- };
5682
+ return allowOutcome(id, event, payload, mcpDir, opts.cwd);
5238
5683
  }
5239
5684
  //#endregion
5240
5685
  //#region src/freshness/query-framework.ts
@@ -5564,4 +6009,4 @@ async function handleHook(id, payload, opts) {
5564
6009
  });
5565
6010
  }
5566
6011
  //#endregion
5567
- export { saveApexState as $, trackSkillRead as A, securityStateDir as At, writePluginMap as B, seoPostToolUse as C, defaultStateDir as Ct, postTrackingSideEffects as D, isoUtc as Dt, securityAdvisory as E, normalizeEvent as Et, dispatchLessons as F, mergeLines as G, isProject as H, lessonsFileFor as I, listChildren as J, countFiles as K, lessonsStateFileFor as L, dispatchLifecycle as M, todayUtc as Mt, aipilotPostToolUse as N, trackWatchResearch as O, loadSecurityState as Ot, dispatchAipilot as P, cleanupSession as Q, cartoSessionStart as R, postEditContext as S, taskContext as St, dispatchMemory as T, trackFile as Tt, writeTree as U, generateProjectMap as V, loadEnriched as W, trackSessionChanges as X, postEditTypescript as Y, validateRulesLoaded as Z, preCommitGate as _, trimLogFile as _t, recordActivity as a, validateSolidGate as at, extractSymbols as b, projectContext as bt, MCP_TTL_MS as c, detectSolidProfile as ct, isMcpTool as d, readRules as dt, logToolFailure as et, queryOf as f, runSessionStartCleanups as ft, gate as g, removeOldFiles as gt, TRIVIAL_BUDGET as h, purgeTtlTree as ht, respond as i, validateTailwind as it, trackEnrichment as j, securityStatePath as jt, trackMcpResearch as k, saveSecurityState as kt, WEBFETCH_TTL_MS as l, solidDetectStart as lt, REQUIRED_AGENTS as m, pruneEmptyDirs as mt, activityFor as n, trackAgentMemory as nt, mcpPostStore as o, checkFileSize as ot, DEFAULT_WINDOW_MS as p, sessionStartCore as pt, getFileDesc as q, handlePre as r, subagentCacheContext as rt, mcpPreIntercept as s, countLoc as st, handleHook as t, validateTeammateOutput as tt, cacheQueryOf as u, injectRules as ut, detectDuplication as v, devContext as vt, seoPostToolUseResponse as w, projectHash$1 as wt, lifecycleStdout as x, promptSubmitContext as xt, dryGate as y, gitContext as yt, generateEcosystemMap as z };
6012
+ export { saveApexState as $, trackSkillRead as A, saveSecurityState as At, writePluginMap as B, seoPostToolUse as C, taskContext as Ct, postTrackingSideEffects as D, normalizeEvent as Dt, securityAdvisory as E, trackFile as Et, dispatchLessons as F, mergeLines as G, isProject as H, lessonsFileFor as I, listChildren as J, countFiles as K, lessonsStateFileFor as L, dispatchLifecycle as M, securityStatePath as Mt, aipilotPostToolUse as N, todayUtc as Nt, trackWatchResearch as O, isoUtc as Ot, dispatchAipilot as P, cleanupSession as Q, cartoSessionStart as R, postEditContext as S, promptSubmitContext as St, dispatchMemory as T, projectHash$1 as Tt, writeTree as U, generateProjectMap as V, loadEnriched as W, trackSessionChanges as X, postEditTypescript as Y, validateRulesLoaded as Z, preCommitGate as _, trimLogFile as _t, recordActivity as a, validateSolidGate as at, extractSymbols as b, projectContext as bt, MCP_TTL_MS as c, detectSolidProfile as ct, isMcpTool as d, readRules as dt, logToolFailure as et, queryOf as f, runSessionStartCleanups as ft, gate as g, removeOldFiles as gt, TRIVIAL_BUDGET as h, purgeTtlTree as ht, respond as i, validateTailwind as it, trackEnrichment as j, securityStateDir as jt, trackMcpResearch as k, loadSecurityState as kt, WEBFETCH_TTL_MS as l, solidDetectStart as lt, REQUIRED_AGENTS as m, pruneEmptyDirs as mt, activityFor as n, trackAgentMemory as nt, mcpPostStore as o, checkFileSize as ot, DEFAULT_WINDOW_MS as p, sessionStartCore as pt, getFileDesc as q, handlePre as r, subagentCacheContext as rt, mcpPreIntercept as s, countLoc as st, handleHook as t, validateTeammateOutput as tt, cacheQueryOf as u, injectRules as ut, detectDuplication as v, devContext as vt, seoPostToolUseResponse as w, defaultStateDir as wt, lifecycleStdout as x, claudeMdKey as xt, dryGate as y, gitContext as yt, generateEcosystemMap as z };
@@ -240,8 +240,24 @@ declare function mcpPostStore(tool: string, input: Record<string, unknown>, resp
240
240
  //#endregion
241
241
  //#region src/runtime/inject-context.d.ts
242
242
  /**
243
+ * Build the {@link oncePerWindow} key for the CLAUDE.md preamble gate. The
244
+ * prompt hash keeps two distinct legitimate turns from colliding — even non-dev
245
+ * prompts, whose block is prompt-independent (just CLAUDE.md) and would
246
+ * otherwise hash-collide within the window — while the content hash still lets a
247
+ * same-turn double-fire of an identical block be suppressed. Single source of
248
+ * truth so the owner invariant test guards the real production key.
249
+ * @param prompt - The raw user prompt.
250
+ * @param ctx - The rendered CLAUDE.md (+ optional APEX) block.
251
+ * @returns The namespaced dedup key.
252
+ */
253
+ declare function claudeMdKey(prompt: string, ctx: string): string;
254
+ /**
243
255
  * UserPromptSubmit context injection: render the CLAUDE.md (+ optional APEX)
244
256
  * preamble as a Claude `additionalContext` response, or "" when nothing to emit.
257
+ * Guarded by {@link oncePerWindow} via {@link claudeMdKey}: only a
258
+ * near-simultaneous double-fire of the SAME turn (identical prompt AND identical
259
+ * block, within {@link DEDUP_WINDOW_MS}) is suppressed. The invariant "CLAUDE.md
260
+ * is emitted on EVERY message" is thus preserved.
245
261
  * @param prompt - The raw user prompt.
246
262
  * @param cwd - Project root (for project-type detection).
247
263
  * @returns The native hook stdout (possibly empty).
@@ -811,4 +827,4 @@ interface PreContext {
811
827
  */
812
828
  declare function handlePre(ctx: PreContext): Promise<HandleOutcome>;
813
829
  //#endregion
814
- 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, 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 };
830
+ 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 };
@@ -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 saveApexState, A as trackSkillRead, At as securityStateDir, B as writePluginMap, C as seoPostToolUse, Ct as defaultStateDir, D as postTrackingSideEffects, Dt as isoUtc, E as securityAdvisory, Et as normalizeEvent, F as dispatchLessons, G as mergeLines, H as isProject, I as lessonsFileFor, J as listChildren, K as countFiles, L as lessonsStateFileFor, M as dispatchLifecycle, Mt as todayUtc, N as aipilotPostToolUse, O as trackWatchResearch, Ot as loadSecurityState, P as dispatchAipilot, Q as cleanupSession, R as cartoSessionStart, S as postEditContext, St as taskContext, T as dispatchMemory, Tt as trackFile, U as writeTree, V as generateProjectMap, W as loadEnriched, X as trackSessionChanges, Y as postEditTypescript, Z as validateRulesLoaded, _ as preCommitGate, _t as trimLogFile, a as recordActivity, at as validateSolidGate, b as extractSymbols, bt as projectContext, c as MCP_TTL_MS, ct as detectSolidProfile, d as isMcpTool, dt as readRules, et as logToolFailure, f as queryOf, ft as runSessionStartCleanups, g as gate, gt as removeOldFiles, h as TRIVIAL_BUDGET, ht as purgeTtlTree, i as respond, it as validateTailwind, j as trackEnrichment, jt as securityStatePath, k as trackMcpResearch, kt as saveSecurityState, l as WEBFETCH_TTL_MS, lt as solidDetectStart, m as REQUIRED_AGENTS, mt as pruneEmptyDirs, n as activityFor, nt as trackAgentMemory, o as mcpPostStore, ot as checkFileSize, p as DEFAULT_WINDOW_MS, pt as sessionStartCore, q as getFileDesc, r as handlePre, rt as subagentCacheContext, s as mcpPreIntercept, st as countLoc, t as handleHook, tt as validateTeammateOutput, u as cacheQueryOf, ut as injectRules, v as detectDuplication, vt as devContext, w as seoPostToolUseResponse, wt as projectHash, x as lifecycleStdout, xt as promptSubmitContext, y as dryGate, yt as gitContext, z as generateEcosystemMap } from "../handle-D1hHqHvh.mjs";
3
+ import { $ as saveApexState, A as trackSkillRead, At as saveSecurityState, B as writePluginMap, C as seoPostToolUse, Ct as taskContext, D as postTrackingSideEffects, Dt as normalizeEvent, E as securityAdvisory, Et as trackFile, F as dispatchLessons, G as mergeLines, H as isProject, I as lessonsFileFor, J as listChildren, K as countFiles, L as lessonsStateFileFor, M as dispatchLifecycle, Mt as securityStatePath, N as aipilotPostToolUse, Nt as todayUtc, O as trackWatchResearch, Ot as isoUtc, P as dispatchAipilot, Q as cleanupSession, R as cartoSessionStart, S as postEditContext, St as promptSubmitContext, T as dispatchMemory, Tt as projectHash, U as writeTree, V as generateProjectMap, W as loadEnriched, X as trackSessionChanges, Y as postEditTypescript, Z as validateRulesLoaded, _ as preCommitGate, _t as trimLogFile, a as recordActivity, at as validateSolidGate, b as extractSymbols, bt as projectContext, c as MCP_TTL_MS, ct as detectSolidProfile, d as isMcpTool, dt as readRules, et as logToolFailure, f as queryOf, ft as runSessionStartCleanups, g as gate, gt as removeOldFiles, h as TRIVIAL_BUDGET, ht as purgeTtlTree, i as respond, it as validateTailwind, j as trackEnrichment, jt as securityStateDir, k as trackMcpResearch, kt as loadSecurityState, l as WEBFETCH_TTL_MS, lt as solidDetectStart, m as REQUIRED_AGENTS, mt as pruneEmptyDirs, n as activityFor, nt as trackAgentMemory, o as mcpPostStore, ot as checkFileSize, p as DEFAULT_WINDOW_MS, pt as sessionStartCore, q as getFileDesc, r as handlePre, rt as subagentCacheContext, s as mcpPreIntercept, st as countLoc, t as handleHook, tt as validateTeammateOutput, u as cacheQueryOf, ut as injectRules, v as detectDuplication, vt as devContext, w as seoPostToolUseResponse, wt as defaultStateDir, x as lifecycleStdout, xt as claudeMdKey, y as dryGate, yt as gitContext, z as generateEcosystemMap } from "../handle-DzPjaPS-.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, 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, 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.49",
3
+ "version": "0.1.50",
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",