@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 {
|
|
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
|
-
|
|
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
|
-
|
|
907
|
-
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
|
1583
|
-
function injectMemory(cwd, event) {
|
|
1584
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 };
|
package/dist/runtime/index.d.mts
CHANGED
|
@@ -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 };
|
package/dist/runtime/index.mjs
CHANGED
|
@@ -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
|
|
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.
|
|
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",
|