@fusengine/harness 0.1.31 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,20 +1,21 @@
1
1
  import { r as resolveMaxLines } from "./limits-CHn8AIL1.mjs";
2
2
  import { r as projectLayout } from "./layout-C0jaaCQC.mjs";
3
- import { C as capVerbosity, F as requiredArchSkill, M as detectModularArchitecture, O as evaluateApex, a as parseEntry, b as frameworkSolidGate, c as EXCLUDE_DIRS$1, d as buildApexTaskInjection, h as buildClaudeMdContext, i as parseEnrichment, l as PROJECT_INDICATORS, s as parseField, t as descFromText, v as skillTriggerGate, w as detectCreationIntent } from "./describe-BYqhoV4c.mjs";
4
- import { j as detectFramework, k as countLines, n as FAIL_CLOSED, t as evaluate } from "./evaluate-j3gRJ_ng.mjs";
3
+ import { i as walkUpFor, n as projectRoot, r as projectRootOrNull, t as isCodeFile } from "./project-root-3kk7gCOp.mjs";
4
+ import { A as evaluateApex, E as detectCreationIntent, L as requiredArchSkill, P as detectModularArchitecture, S as frameworkSolidGate, T as capVerbosity, _ as buildClaudeMdContext, b as skillTriggerGate, c as parseBodyDesc, d as PROJECT_INDICATORS, l as parseField, n as missingSeoElements, o as parseEnrichment, p as buildApexTaskInjection, r as descFromText, s as parseEntry, t as isHtmlLike, u as EXCLUDE_DIRS$1 } from "./validate-CccewDwk.mjs";
5
+ import { j as detectFramework, k as countLines, n as FAIL_CLOSED, t as evaluate } from "./evaluate-9ch1K2kt.mjs";
5
6
  import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
7
+ import { a as nowStamp, l as throttleMs, n as readRoots, o as readState, s as setStateField, t as addRoot } from "./registry-BkoEbdec.mjs";
6
8
  import { a as extractText, o as loadIndex, r as cacheStore, t as cacheLookup } from "./store-PrNPm6So.mjs";
7
9
  import { i as writeJsonFile, r as readJsonFile, t as atomicWrite } from "./json-io-CAn72gI4.mjs";
8
10
  import { t as loadRefs } from "./loader-CyAoJv2W.mjs";
9
11
  import { a as recordAgent, c as recordRefRead, l as recordTrivialEdit, n as saveTrack, o as recordBrainstormRequired, r as agentsFresh, s as recordDoc, t as loadTrack, u as trivialCount } from "./store-D-ge2ZPI.mjs";
10
- import { t as contextResponse } from "./claude-B9FYp0Yw.mjs";
12
+ import { c as pathExists, d as spawnCapture, f as writeText, l as readText, n as denyResponse, s as collectFiles, t as contextResponse, u as sleep } from "./claude-BatVYnAf.mjs";
11
13
  import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
12
14
  import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
13
15
  import { homedir, tmpdir } from "node:os";
14
16
  import { createHash } from "node:crypto";
15
17
  import { mkdir, rmdir } from "node:fs/promises";
16
18
  import { execFileSync } from "node:child_process";
17
- import { Glob } from "bun";
18
19
  //#region src/runtime/activity.ts
19
20
  /** Min response length (chars) for a lead agent call to count as `sufficient`. */
20
21
  const AGENT_QUALITY_MIN = 500;
@@ -1459,22 +1460,417 @@ function generateProjectMap(cwd, outputDir) {
1459
1460
  return "";
1460
1461
  }
1461
1462
  //#endregion
1463
+ //#region src/runtime/lifecycle/cartographer/detect.ts
1464
+ /**
1465
+ * Plugin discovery (fs). Ports `detect_plugins.py`: marketplace `plugins` dir
1466
+ * resolution + `plugin.json` meta reading.
1467
+ */
1468
+ /** Sorted entry names of `dir` (alpha, byte-order), or `[]` on error. */
1469
+ function sortedNames$1(dir) {
1470
+ try {
1471
+ return readdirSync(dir).sort((a, b) => a.localeCompare(b, "en"));
1472
+ } catch {
1473
+ return [];
1474
+ }
1475
+ }
1476
+ /**
1477
+ * Read `[version, name]` from `<pluginPath>/.claude-plugin/plugin.json`.
1478
+ * @param pluginPath - Absolute plugin directory.
1479
+ * @returns The `[version, name]` pair (both "" when absent/unreadable).
1480
+ */
1481
+ function readPluginMeta(pluginPath) {
1482
+ const pj = join(pluginPath, ".claude-plugin", "plugin.json");
1483
+ if (!existsSync(pj)) return ["", ""];
1484
+ try {
1485
+ const meta = JSON.parse(readFileSync(pj, "utf-8"));
1486
+ return [meta.version ?? "", meta.name ?? ""];
1487
+ } catch {
1488
+ return ["", ""];
1489
+ }
1490
+ }
1491
+ /**
1492
+ * Auto-detect the marketplace `plugins` dir that contains `cartographer`,
1493
+ * falling back to the first marketplace with a `plugins` dir, else `cwd`.
1494
+ * Ports `find_marketplace_plugins`.
1495
+ * @param home - Home directory (defaults to `~`).
1496
+ * @returns The resolved plugins directory.
1497
+ */
1498
+ function findMarketplacePlugins(home = homedir()) {
1499
+ const mp = join(home, ".claude", "plugins", "marketplaces");
1500
+ const markets = sortedNames$1(mp);
1501
+ for (const m of markets) if (existsSync(join(mp, m, "plugins", "cartographer"))) return join(mp, m, "plugins");
1502
+ for (const m of markets) if (existsSync(join(mp, m, "plugins"))) return join(mp, m, "plugins");
1503
+ return process.cwd();
1504
+ }
1505
+ //#endregion
1506
+ //#region src/runtime/lifecycle/cartographer/scan.ts
1507
+ /**
1508
+ * Plugin scanning (fs). Ports `scan_plugins.py`: turns a plugin's
1509
+ * agents/skills/commands/hooks into ordered `[type, name, desc]` rows.
1510
+ */
1511
+ /** Sorted entry names of `dir` (alpha, byte-order), or `[]` on error. */
1512
+ function sortedNames(dir) {
1513
+ try {
1514
+ return readdirSync(dir).sort((a, b) => a.localeCompare(b, "en"));
1515
+ } catch {
1516
+ return [];
1517
+ }
1518
+ }
1519
+ /** Read a `.md` frontmatter field from a file path, "" when missing/unreadable. */
1520
+ function fileField(path, field) {
1521
+ try {
1522
+ return parseField(readFileSync(path, "utf-8"), field);
1523
+ } catch {
1524
+ return "";
1525
+ }
1526
+ }
1527
+ /** Scan `agents/*.md` → `("agent", name, desc[:50])` rows. */
1528
+ function scanAgents(root) {
1529
+ const dir = join(root, "agents");
1530
+ return sortedNames(dir).filter((n) => extname(n) === ".md").map((n) => {
1531
+ const f = join(dir, n);
1532
+ return [
1533
+ "agent",
1534
+ fileField(f, "name") || n.replace(/\.md$/, ""),
1535
+ fileField(f, "description").slice(0, 50)
1536
+ ];
1537
+ });
1538
+ }
1539
+ /** Scan `skills/<dir>/SKILL.md` → `("skill", dir, desc)` rows. */
1540
+ function scanSkills(root) {
1541
+ const dir = join(root, "skills");
1542
+ const rows = [];
1543
+ for (const name of sortedNames(dir)) {
1544
+ try {
1545
+ if (!statSync(join(dir, name)).isDirectory()) continue;
1546
+ } catch {
1547
+ continue;
1548
+ }
1549
+ const skillMd = join(dir, name, "SKILL.md");
1550
+ let desc = "";
1551
+ if (existsSync(skillMd)) {
1552
+ desc = fileField(skillMd, "description");
1553
+ if (!desc) try {
1554
+ desc = parseBodyDesc(readFileSync(skillMd, "utf-8"));
1555
+ } catch {}
1556
+ }
1557
+ rows.push([
1558
+ "skill",
1559
+ name,
1560
+ desc || "(no description)"
1561
+ ]);
1562
+ }
1563
+ return rows;
1564
+ }
1565
+ /** Scan `commands/*.md` → `("command", "/name", desc[:50])` rows. */
1566
+ function scanCommands(root) {
1567
+ const dir = join(root, "commands");
1568
+ return sortedNames(dir).filter((n) => extname(n) === ".md").map((n) => [
1569
+ "command",
1570
+ `/${n.replace(/\.md$/, "")}`,
1571
+ fileField(join(dir, n), "description").slice(0, 50)
1572
+ ]);
1573
+ }
1574
+ /** Scan `hooks/hooks.json` → a single `("hooks", "<events>", "")` row, or none. */
1575
+ function scanHooks(root) {
1576
+ const file = join(root, "hooks", "hooks.json");
1577
+ if (!existsSync(file)) return [];
1578
+ try {
1579
+ const raw = JSON.parse(readFileSync(file, "utf-8"));
1580
+ const data = raw && typeof raw === "object" ? raw : {};
1581
+ const hooks = data.hooks && typeof data.hooks === "object" ? data.hooks : data;
1582
+ const events = Object.keys(hooks).filter((k) => !k.startsWith("_")).sort((a, b) => a.localeCompare(b, "en"));
1583
+ return events.length ? [[
1584
+ "hooks",
1585
+ events.join(", "),
1586
+ ""
1587
+ ]] : [];
1588
+ } catch {
1589
+ return [];
1590
+ }
1591
+ }
1592
+ /**
1593
+ * Scan a single plugin directory into ordered `[type, name, desc]` rows.
1594
+ * @param pluginDir - Absolute plugin directory.
1595
+ * @returns The agents + skills + commands + hooks rows.
1596
+ */
1597
+ function scanPlugin(pluginDir) {
1598
+ return [
1599
+ ...scanAgents(pluginDir),
1600
+ ...scanSkills(pluginDir),
1601
+ ...scanCommands(pluginDir),
1602
+ ...scanHooks(pluginDir)
1603
+ ];
1604
+ }
1605
+ //#endregion
1606
+ //#region src/policy/cartographer/build-tree.ts
1607
+ const SECTION_ORDER = [
1608
+ "agent",
1609
+ "skill",
1610
+ "command"
1611
+ ];
1612
+ /**
1613
+ * Format grouped items with tree connectors and optional markdown links.
1614
+ * Skill sections link to `./skills/<name>/index.md`; other sections to
1615
+ * `./<folder>/<name>.md`; unlinked sections render the bare name.
1616
+ * @param prefix - The line prefix (indent + branch glyphs).
1617
+ * @param items - The `[name, desc]` pairs to render.
1618
+ * @param folder - The link folder ("" disables linking).
1619
+ * @param asDirs - Whether items link to a subdirectory `index.md`.
1620
+ * @returns The rendered lines.
1621
+ */
1622
+ function printItems(prefix, items, folder, asDirs) {
1623
+ return items.map(([name, desc], i) => {
1624
+ const connector = i === items.length - 1 ? "└──" : "├──";
1625
+ const safe = name.replace(/^\/+/, "");
1626
+ let label = name;
1627
+ if (folder && asDirs) label = `[${name}](./${folder}/${safe}/index.md)`;
1628
+ else if (folder) label = `[${name}](./${folder}/${safe}.md)`;
1629
+ const short = desc && desc !== "(no description)" ? ` — ${desc.slice(0, 80)}` : "";
1630
+ return `${prefix}${connector} ${label}${short}`;
1631
+ });
1632
+ }
1633
+ /**
1634
+ * Build an indented tree from scanned items. The `hooks` row renders as a single
1635
+ * trailing `└── hooks: …` line; agents/skills/commands render as folder sections.
1636
+ * @param items - The scanned `[type, name, desc]` rows.
1637
+ * @param linked - When true, leaf names become markdown links.
1638
+ * @returns The joined tree text.
1639
+ */
1640
+ function buildTree(items, linked = false) {
1641
+ const groups = {};
1642
+ let hooksLine = "";
1643
+ for (const [typ, name, desc] of items) if (typ === "hooks") hooksLine = name;
1644
+ else (groups[typ] ??= []).push([name, desc]);
1645
+ const sections = SECTION_ORDER.filter((s) => s in groups);
1646
+ if (hooksLine) sections.push("hooks");
1647
+ const lines = [];
1648
+ const total = sections.length;
1649
+ for (let idx = 0; idx < total; idx++) {
1650
+ const section = sections[idx] ?? "";
1651
+ if (section === "hooks") {
1652
+ lines.push(`└── hooks: ${hooksLine}`);
1653
+ continue;
1654
+ }
1655
+ const isLast = idx === total - 1;
1656
+ const folder = `${section}s`;
1657
+ const prefix = isLast ? "└──" : "├──";
1658
+ const subPrefix = isLast ? " " : "│ ";
1659
+ lines.push(`${prefix} ${folder}/`);
1660
+ const linkFolder = linked ? folder : "";
1661
+ const isDirSection = section === "skill";
1662
+ lines.push(...printItems(subPrefix, groups[section] ?? [], linkFolder, linked && isDirSection));
1663
+ }
1664
+ return lines.join("\n");
1665
+ }
1666
+ //#endregion
1667
+ //#region src/runtime/lifecycle/cartographer/write-plugin-map.ts
1668
+ /**
1669
+ * Per-plugin map writer (fs). Ports `write_plugin_map.py`: writes a level-2
1670
+ * `<plugin>/index.md` (indented linked tree) then recurses agents/skills/
1671
+ * commands into deeper index trees. Reuses `buildTree`, `mergeLines`, `writeTree`.
1672
+ */
1673
+ /** True when `dir` is a real directory. */
1674
+ function isDir(dir) {
1675
+ try {
1676
+ return statSync(dir).isDirectory();
1677
+ } catch {
1678
+ return false;
1679
+ }
1680
+ }
1681
+ /**
1682
+ * Write `<outputDir>/<pluginName>/index.md` (indented linked tree) and recurse
1683
+ * agents/skills/commands into their own index trees rooted there.
1684
+ * @param outputDir - The map root directory.
1685
+ * @param pluginName - Display name of the plugin (the index subfolder).
1686
+ * @param version - Plugin version ("" to omit).
1687
+ * @param items - The scanned `[type, name, desc]` rows.
1688
+ * @param pluginPath - Absolute source plugin directory (for recursion).
1689
+ */
1690
+ function writePluginMap(outputDir, pluginName, version, items, pluginPath) {
1691
+ const pluginDir = join(outputDir, pluginName);
1692
+ mkdirSync(pluginDir, { recursive: true });
1693
+ const newLines = `# ${pluginName}${version ? ` (v${version})` : ""}\n\n${items.length ? buildTree(items, true) : "└── (empty)"}`.split("\n");
1694
+ const indexPath = join(pluginDir, "index.md");
1695
+ writeFileSync(indexPath, mergeLines(newLines, indexPath).join("\n") + "\n", "utf-8");
1696
+ for (const section of [
1697
+ "agents",
1698
+ "skills",
1699
+ "commands"
1700
+ ]) {
1701
+ const src = join(pluginPath, section);
1702
+ if (isDir(src)) writeTree(src, join(pluginDir, section), "../index.md");
1703
+ }
1704
+ }
1705
+ //#endregion
1706
+ //#region src/runtime/lifecycle/cartographer/ecosystem-map.ts
1707
+ /**
1708
+ * Ecosystem (plugin) map generation (fs). Ports `generate_map.py`: scans every
1709
+ * installed plugin into a level-1 `.cartographer/index.md` + per-plugin level-2+
1710
+ * trees, preserving enriched descriptions. Reuses `findMarketplacePlugins`,
1711
+ * `readPluginMeta`, `scanPlugin`, `mergeLines`, `writePluginMap`.
1712
+ */
1713
+ function pluginDirs(dir) {
1714
+ let entries = [];
1715
+ try {
1716
+ entries = readdirSync(dir);
1717
+ } catch {
1718
+ return [];
1719
+ }
1720
+ return entries.filter((n) => !n.startsWith("_") && !n.startsWith(".")).filter((n) => {
1721
+ try {
1722
+ return statSync(join(dir, n)).isDirectory();
1723
+ } catch {
1724
+ return false;
1725
+ }
1726
+ }).sort((a, b) => a.localeCompare(b, "en"));
1727
+ }
1728
+ function utcStamp(now) {
1729
+ return new Date(now).toISOString().slice(0, 16).replace("T", " ");
1730
+ }
1731
+ /**
1732
+ * Generate the plugin ecosystem map under `<pluginsDir>/.cartographer`.
1733
+ * @param now - Clock for the banner timestamp.
1734
+ * @param pluginsDirOverride - Override for the marketplace plugins directory.
1735
+ * @returns The map navigation context, or "".
1736
+ */
1737
+ function generateEcosystemMap(now, pluginsDirOverride) {
1738
+ const pluginsDir = resolve(pluginsDirOverride ?? findMarketplacePlugins());
1739
+ try {
1740
+ if (!statSync(pluginsDir).isDirectory()) return "";
1741
+ } catch {
1742
+ return "";
1743
+ }
1744
+ const outputDir = join(pluginsDir, ".cartographer");
1745
+ mkdirSync(outputDir, { recursive: true });
1746
+ const dirs = pluginDirs(pluginsDir);
1747
+ const lines = [`# Ecosystem Map (${dirs.length} plugins)\n`, `> Auto-generated by cartographer — ${utcStamp(now)}\n`];
1748
+ for (const name of dirs) {
1749
+ const pluginPath = join(pluginsDir, name);
1750
+ const [version, pkgName] = readPluginMeta(pluginPath);
1751
+ const display = pkgName || name;
1752
+ const items = scanPlugin(pluginPath);
1753
+ const agents = items.filter(([t]) => t === "agent").map(([, n]) => n);
1754
+ const ver = version ? ` (v${version})` : "";
1755
+ lines.push(`- [${display}](./${display}/index.md)${ver} → ${agents.length ? agents.join(", ") : "(no agents)"}`);
1756
+ writePluginMap(outputDir, display, version, items, pluginPath);
1757
+ writePluginMap(pluginPath, ".cartographer", version, items, pluginPath);
1758
+ }
1759
+ const indexPath = join(outputDir, "index.md");
1760
+ writeFileSync(indexPath, mergeLines(lines, indexPath).join("\n") + "\n", "utf-8");
1761
+ return `Project map: .cartographer/project/index.md — navigate project files. Plugin skills map: ${outputDir}/index.md — navigate agent skills. Branches link to deeper index.md, leaves link to real files.`;
1762
+ }
1763
+ //#endregion
1462
1764
  //#region src/runtime/lifecycle/cartographer/session-start.ts
1463
1765
  /**
1464
- * Cartographer SessionStart handler. Ports the project-map half of
1465
- * `generate_project_map.py`: regenerates `.cartographer/project` and emits no
1466
- * additionalContext (the plugin ecosystem map from `generate_map.py` is not
1467
- * ported and stays as Python).
1766
+ * Cartographer SessionStart handler. Ports BOTH halves of the Python maps:
1767
+ * `generate_project_map.py` (regenerate `.cartographer/project`) and
1768
+ * `generate_map.py` (regenerate the plugin ecosystem map), emitting the
1769
+ * navigation context from the latter as additionalContext.
1468
1770
  */
1469
1771
  /**
1470
- * Regenerate the project map for `cwd` on SessionStart. Returns "" (side-effect
1471
- * only no additionalContext).
1772
+ * Regenerate the project map + plugin ecosystem map for `cwd` on SessionStart.
1773
+ * Emits the ecosystem navigation context as additionalContext (or "").
1472
1774
  * @param cwd - The working directory.
1473
- * @returns "" always.
1775
+ * @param now - Clock for the ecosystem map banner timestamp.
1776
+ * @returns The SessionStart additionalContext response, or "".
1474
1777
  */
1475
- function cartoSessionStart(cwd) {
1778
+ function cartoSessionStart(cwd, now = Date.now()) {
1476
1779
  generateProjectMap(cwd);
1477
- return "";
1780
+ const ctx = generateEcosystemMap(now);
1781
+ return ctx ? contextResponse("SessionStart", ctx) : "";
1782
+ }
1783
+ //#endregion
1784
+ //#region src/runtime/lifecycle/lessons/state.ts
1785
+ /**
1786
+ * Per-project lessons paths. The `fuse-lessons` plugin stores its lessons under
1787
+ * `<root>/MEMORY/` (NOT the harness `.harness/memory/`), so these two path
1788
+ * helpers override the layout while ALL state/gitignore/throttle logic is
1789
+ * reused from `src/memory` (`setStateField`, `ensureMemoryGitignore`,
1790
+ * `readState`, `nowStamp`, `throttleMs`).
1791
+ */
1792
+ /** Absolute `<root>/MEMORY/LESSON.md` — the curated, committable lessons file. */
1793
+ function lessonsFileFor(root) {
1794
+ return join(root, "MEMORY", "LESSON.md");
1795
+ }
1796
+ /** Absolute `<root>/MEMORY/state.json` — machine-local throttle counter. */
1797
+ function lessonsStateFileFor(root) {
1798
+ return join(root, "MEMORY", "state.json");
1799
+ }
1800
+ //#endregion
1801
+ //#region src/runtime/lifecycle/lessons/dispatch.ts
1802
+ /**
1803
+ * fuse-lessons scope dispatch (TS port of the 4 handler scripts). Routes by
1804
+ * event: SessionStart/SubagentStart inject `MEMORY/LESSON.md`; Stop reminds
1805
+ * across every project with unsaved code edits; PostToolUse marks the write to
1806
+ * arm/silence the per-project throttle. Non-fatal by design.
1807
+ */
1808
+ /** Inject `<root>/MEMORY/LESSON.md` as additionalContext for `event`. */
1809
+ function injectMemory(cwd, event) {
1810
+ const file = lessonsFileFor(projectRoot(cwd));
1811
+ if (!existsSync(file)) return "";
1812
+ let content = "";
1813
+ try {
1814
+ content = readFileSync(file, "utf-8").trim();
1815
+ } catch {
1816
+ return "";
1817
+ }
1818
+ if (!content) return "";
1819
+ 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.`);
1820
+ }
1821
+ /** Select roots with unsaved code edits past the throttle, bumping their state. */
1822
+ function collectPending(now, window) {
1823
+ const pending = [];
1824
+ for (const root of readRoots()) {
1825
+ const stateFile = lessonsStateFileFor(root);
1826
+ const { lastRemindedAt, lastCodeEditAt } = readState(stateFile);
1827
+ if (lastCodeEditAt <= lastRemindedAt) continue;
1828
+ if (now - lastRemindedAt < window) continue;
1829
+ pending.push(root);
1830
+ setStateField(stateFile, "lastRemindedAt", now);
1831
+ }
1832
+ return pending;
1833
+ }
1834
+ /** Stop: emit one reminder covering every project with pending lessons. */
1835
+ function remindWrite(now) {
1836
+ const pending = collectPending(now, throttleMs());
1837
+ if (pending.length === 0) return "";
1838
+ return contextResponse("Stop", `Before ending: if this session hit a mistake/blocker worth never reproducing, append 1-3 COMPACT bullets OR sharpen/merge existing ones (format \`- [${nowStamp()}] what went wrong → do instead\`, use exactly this timestamp) in each project's lessons file below. Skip if nothing notable.\n${pending.map((r) => `- ${r}/MEMORY/LESSON.md`).join("\n")}`);
1839
+ }
1840
+ /** PostToolUse: record the relevant throttle timestamp for the edited file. */
1841
+ function markWrite(payload, now) {
1842
+ const input = payload.tool_input;
1843
+ if (!input?.file_path) return;
1844
+ const abs = resolve(input.file_path);
1845
+ const root = projectRootOrNull(dirname(abs));
1846
+ if (!root) return;
1847
+ const stateFile = lessonsStateFileFor(root);
1848
+ if (abs === resolve(root, "MEMORY", "LESSON.md")) setStateField(stateFile, "lastRemindedAt", now);
1849
+ else if (isCodeFile(abs)) {
1850
+ setStateField(stateFile, "lastCodeEditAt", now);
1851
+ addRoot(root);
1852
+ }
1853
+ }
1854
+ /**
1855
+ * Route a fuse-lessons event to its handler. Returns the native stdout for
1856
+ * context-injecting events (SessionStart/SubagentStart/Stop) or "" for the
1857
+ * side-effect-only PostToolUse mark.
1858
+ * @param event - The raw hook event name.
1859
+ * @param payload - The raw hook payload.
1860
+ * @param cwd - Project root for memory injection.
1861
+ * @param now - Clock.
1862
+ * @returns The native stdout (possibly empty).
1863
+ */
1864
+ function dispatchLessons(event, payload, cwd, now) {
1865
+ switch (event) {
1866
+ case "SessionStart":
1867
+ case "SubagentStart": return injectMemory(cwd, event);
1868
+ case "Stop": return remindWrite(now);
1869
+ case "PostToolUse":
1870
+ markWrite(payload, now);
1871
+ return "";
1872
+ default: return "";
1873
+ }
1478
1874
  }
1479
1875
  //#endregion
1480
1876
  //#region src/runtime/lifecycle/aipilot/inject-apex.ts
@@ -1513,8 +1909,8 @@ function cartographerContext() {
1513
1909
  async function injectApexSubagentContext(cwd, home = homedir()) {
1514
1910
  const apexDir = join(process.env.CLAUDE_PROJECT_DIR ?? cwd, ".claude", "apex");
1515
1911
  if (!existsSync(apexDir)) return "";
1516
- const agentsFile = Bun.file(join(apexDir, "AGENTS.md"));
1517
- const agents = await agentsFile.exists() ? (await agentsFile.text()).slice(0, 4e3) : "";
1912
+ const agentsPath = join(apexDir, "AGENTS.md");
1913
+ const agents = existsSync(agentsPath) ? readText(agentsPath).slice(0, 4e3) : "";
1518
1914
  const taskData = await readJsonFile(join(apexDir, "task.json"));
1519
1915
  return contextResponse("SubagentStart", `## APEX Sub-Agent Instructions
1520
1916
 
@@ -1570,7 +1966,7 @@ function cacheAge(ts, now = Date.now()) {
1570
1966
  /** Full SHA-256 hex checksum of a file's text; "" when unreadable. */
1571
1967
  async function fileChecksum(path) {
1572
1968
  try {
1573
- return createHash("sha256").update(await Bun.file(path).text()).digest("hex");
1969
+ return createHash("sha256").update(readText(path)).digest("hex");
1574
1970
  } catch {
1575
1971
  return "";
1576
1972
  }
@@ -1639,9 +2035,8 @@ function parseEntries(raw) {
1639
2035
  async function cacheAnalyticsSave(home = homedir(), now = Date.now()) {
1640
2036
  const dir = join(cacheBaseDir(home), "analytics");
1641
2037
  const sessionsFile = join(dir, "sessions.jsonl");
1642
- const file = Bun.file(sessionsFile);
1643
- if (!await file.exists()) return;
1644
- const raw = await file.text();
2038
+ if (!pathExists(sessionsFile)) return;
2039
+ const raw = readText(sessionsFile);
1645
2040
  if (!raw.trim()) return;
1646
2041
  const entries = parseEntries(raw);
1647
2042
  if (entries.length === 0) return;
@@ -1669,8 +2064,7 @@ async function cacheAnalyticsSave(home = homedir(), now = Date.now()) {
1669
2064
  }
1670
2065
  await writeJsonFile(join(dir, "summary.json"), merged, true);
1671
2066
  const cutoff = (/* @__PURE__ */ new Date(now - 30 * 864e5)).toISOString();
1672
- const kept = entries.filter((e) => e.ts >= cutoff);
1673
- await Bun.write(sessionsFile, kept.map((e) => JSON.stringify(e)).join("\n") + "\n");
2067
+ writeText(sessionsFile, entries.filter((e) => e.ts >= cutoff).map((e) => JSON.stringify(e)).join("\n") + "\n");
1674
2068
  }
1675
2069
  //#endregion
1676
2070
  //#region src/runtime/lifecycle/aipilot/inject-explore.ts
@@ -1694,17 +2088,11 @@ const CONFIG_FILES = [
1694
2088
  /** Compute a config hash from git-tracked config files; "noconfig" on failure. */
1695
2089
  async function configHash(cwd) {
1696
2090
  try {
1697
- const proc = Bun.spawn([
1698
- "git",
2091
+ const output = spawnCapture("git", [
1699
2092
  "ls-tree",
1700
2093
  "HEAD",
1701
2094
  ...CONFIG_FILES
1702
- ], {
1703
- cwd,
1704
- stdout: "pipe",
1705
- stderr: "ignore"
1706
- });
1707
- const output = await new Response(proc.stdout).text();
2095
+ ], cwd);
1708
2096
  return output.trim() ? hashText16(output) : "noconfig";
1709
2097
  } catch {
1710
2098
  return "noconfig";
@@ -1730,8 +2118,7 @@ async function injectExploreCache(cwd, home = homedir(), now = Date.now()) {
1730
2118
  const cfgHash = await configHash(projPath);
1731
2119
  let context = "";
1732
2120
  const meta = await readJsonFile(metaFile);
1733
- const snapBunFile = Bun.file(snapFile);
1734
- const snapshot = await snapBunFile.exists() ? await snapBunFile.text() : "";
2121
+ const snapshot = pathExists(snapFile) ? readText(snapFile) : "";
1735
2122
  if (meta?.timestamp && snapshot) {
1736
2123
  const age = cacheAge(meta.timestamp, now);
1737
2124
  if (age < TTL_SECONDS$2 && meta.config_hash === cfgHash) {
@@ -1767,9 +2154,9 @@ async function buildDocsContext(entries, docsDir, now) {
1767
2154
  if (age > maxAge) maxAge = age;
1768
2155
  if (!entry.hash || seen.has(entry.hash)) continue;
1769
2156
  seen.add(entry.hash);
1770
- const file = Bun.file(join(docsDir, `${entry.hash}.md`));
1771
- if (!await file.exists()) continue;
1772
- const content = await file.text();
2157
+ const docPath = join(docsDir, `${entry.hash}.md`);
2158
+ if (!pathExists(docPath)) continue;
2159
+ const content = readText(docPath);
1773
2160
  if (!content) continue;
1774
2161
  ctx += `\n${content}\n`;
1775
2162
  count++;
@@ -1805,32 +2192,36 @@ async function injectDocCache(cwd, home = homedir(), now = Date.now()) {
1805
2192
  * Ported from the ai-pilot plugin's `cache/source-collector.ts` +
1806
2193
  * the stack detection in `cache/lesson-helpers.ts` (now removed).
1807
2194
  */
1808
- /** Source file glob patterns (monorepo-aware; separate to avoid brace-wildcards). */
1809
- const SRC_PATTERNS = [
1810
- "src/**/*.{ts,tsx,js,jsx}",
1811
- "app/**/*.{ts,tsx,js,jsx}",
1812
- "apps/*/src/**/*.{ts,tsx,js,jsx}",
1813
- "packages/*/src/**/*.{ts,tsx,js,jsx}"
1814
- ];
2195
+ /** Source extensions to collect (monorepo-aware, dot-prefixed for matching). */
2196
+ const SRC_EXTS = /* @__PURE__ */ new Set([
2197
+ ".ts",
2198
+ ".tsx",
2199
+ ".js",
2200
+ ".jsx"
2201
+ ]);
2202
+ /** Roots walked: `src`, `app`, plus each child `src` under `apps/` and `packages/`. */
2203
+ const TOP_DIRS = ["src", "app"];
2204
+ const NESTED_PARENTS = ["apps", "packages"];
2205
+ /** Collect the existing monorepo `src` roots nested under `apps/` and `packages/`. */
2206
+ function nestedRoots(projectPath) {
2207
+ const roots = [];
2208
+ for (const parent of NESTED_PARENTS) try {
2209
+ for (const e of readdirSync(join(projectPath, parent), { withFileTypes: true })) if (e.isDirectory()) roots.push(join(projectPath, parent, e.name, "src"));
2210
+ } catch {}
2211
+ return roots;
2212
+ }
1815
2213
  /**
1816
2214
  * Scan source files in `projectPath` (monorepo-aware), capped at `maxFiles`.
2215
+ * Node+Bun portable: walks `node:fs` recursively (replaces the Bun `Glob`).
1817
2216
  * @param projectPath - Absolute project root.
1818
2217
  * @param maxFiles - Max files to collect (default 200).
1819
- * @returns Absolute paths matching the source patterns.
2218
+ * @returns Absolute paths matching the source extensions.
1820
2219
  */
1821
2220
  async function scanSourceFiles(projectPath, maxFiles = 200) {
1822
2221
  const files = [];
1823
- for (const pattern of SRC_PATTERNS) {
1824
- try {
1825
- for await (const p of new Glob(pattern).scan({
1826
- cwd: projectPath,
1827
- absolute: true
1828
- })) {
1829
- if (p.includes("node_modules")) continue;
1830
- files.push(p);
1831
- if (files.length >= maxFiles) break;
1832
- }
1833
- } catch {}
2222
+ const roots = [...TOP_DIRS.map((d) => join(projectPath, d)), ...nestedRoots(projectPath)];
2223
+ for (const root of roots) {
2224
+ collectFiles(root, SRC_EXTS, files, maxFiles);
1834
2225
  if (files.length >= maxFiles) break;
1835
2226
  }
1836
2227
  return files;
@@ -2058,7 +2449,7 @@ function projectRootFromPaths(filePaths) {
2058
2449
  }
2059
2450
  /** Extract all absolute file paths from tool_use entries in a JSONL transcript. */
2060
2451
  async function transcriptFilePaths(transcriptPath) {
2061
- const text = await Bun.file(transcriptPath).text();
2452
+ const text = readText(transcriptPath);
2062
2453
  const paths = /* @__PURE__ */ new Set();
2063
2454
  for (const line of text.split("\n").filter(Boolean)) try {
2064
2455
  const content = JSON.parse(line)?.message?.content;
@@ -2073,7 +2464,7 @@ async function transcriptFilePaths(transcriptPath) {
2073
2464
  }
2074
2465
  /** Extract deduplicated Edit tool_use entries (keyed by basename) from a transcript. */
2075
2466
  async function transcriptEdits(transcriptPath) {
2076
- const text = await Bun.file(transcriptPath).text();
2467
+ const text = readText(transcriptPath);
2077
2468
  const edits = [];
2078
2469
  for (const line of text.split("\n").filter(Boolean)) try {
2079
2470
  const content = JSON.parse(line)?.message?.content;
@@ -2090,7 +2481,7 @@ async function transcriptEdits(transcriptPath) {
2090
2481
  }
2091
2482
  /** Extract the last assistant text report (first 500 lines) from a transcript. */
2092
2483
  async function transcriptReport(transcriptPath) {
2093
- const text = await Bun.file(transcriptPath).text();
2484
+ const text = readText(transcriptPath);
2094
2485
  let lastReport = "";
2095
2486
  for (const line of text.split("\n").filter(Boolean)) try {
2096
2487
  const entry = JSON.parse(line);
@@ -2118,7 +2509,7 @@ const RETRY_DELAYS = [
2118
2509
  ];
2119
2510
  /** Extract the longest assistant synthesis + queried library ids from a transcript. */
2120
2511
  async function extractSynthesis(path) {
2121
- const lines = (await Bun.file(path).text()).split("\n").filter(Boolean);
2512
+ const lines = readText(path).split("\n").filter(Boolean);
2122
2513
  const libraries = [];
2123
2514
  let synthesis = "";
2124
2515
  for (const line of lines) try {
@@ -2146,14 +2537,14 @@ async function extractSynthesis(path) {
2146
2537
  * @param home - Home dir (defaults to `~`).
2147
2538
  */
2148
2539
  async function cacheDocFromTranscript(transcript, cwd, home = homedir()) {
2149
- if (!transcript || !await Bun.file(transcript).exists()) return;
2540
+ if (!transcript || !pathExists(transcript)) return;
2150
2541
  const projPath = projectRootFromPaths(await transcriptFilePaths(transcript)) ?? process.env.CLAUDE_PROJECT_DIR ?? cwd;
2151
2542
  const cacheDir = cacheDirFor("doc", projPath, home);
2152
2543
  const docsDir = join(cacheDir, "docs");
2153
2544
  let result = await extractSynthesis(transcript);
2154
2545
  for (const delay of RETRY_DELAYS) {
2155
2546
  if (result.text.length >= MIN_TEXT_SIZE && result.libraries.length > 0) break;
2156
- await Bun.sleep(delay);
2547
+ await sleep(delay);
2157
2548
  result = await extractSynthesis(transcript);
2158
2549
  }
2159
2550
  const { text, libraries } = result;
@@ -2167,7 +2558,7 @@ async function cacheDocFromTranscript(transcript, cwd, home = homedir()) {
2167
2558
  const content = text.slice(0, MAX_DOC_SIZE);
2168
2559
  const topic = libraries.join(", ");
2169
2560
  const docHash = hashText16(topic);
2170
- await Bun.write(join(docsDir, `${docHash}.md`), content);
2561
+ writeText(join(docsDir, `${docHash}.md`), content);
2171
2562
  const sizeKb = Math.floor(content.length / 1024);
2172
2563
  for (const lib of libraries) {
2173
2564
  index.docs = index.docs.filter((d) => d.library !== lib);
@@ -2197,7 +2588,7 @@ async function cacheDocFromTranscript(transcript, cwd, home = homedir()) {
2197
2588
  * @param home - Home dir (defaults to `~`).
2198
2589
  */
2199
2590
  async function cacheSniperLessons(transcript, cwd, home = homedir()) {
2200
- if (!transcript || !await Bun.file(transcript).exists()) return;
2591
+ if (!transcript || !pathExists(transcript)) return;
2201
2592
  const edits = await transcriptEdits(transcript);
2202
2593
  if (edits.length === 0) return;
2203
2594
  const projectPath = projectRootFromPaths(edits.map((e) => e.file)) ?? process.env.CLAUDE_PROJECT_DIR ?? cwd;
@@ -2236,7 +2627,7 @@ async function cacheSniperLessons(transcript, cwd, home = homedir()) {
2236
2627
  */
2237
2628
  /** Extract linter-related command/output text from a JSONL transcript. */
2238
2629
  async function extractLinterOutput(path) {
2239
- const text = await Bun.file(path).text();
2630
+ const text = readText(path);
2240
2631
  const outputs = [];
2241
2632
  for (const line of text.split("\n").filter(Boolean)) try {
2242
2633
  const content = JSON.parse(line)?.message?.content;
@@ -2258,7 +2649,7 @@ async function extractLinterOutput(path) {
2258
2649
  * @param home - Home dir (defaults to `~`).
2259
2650
  */
2260
2651
  async function cacheTestResults(transcript, cwd, home = homedir()) {
2261
- if (!transcript || !await Bun.file(transcript).exists()) return;
2652
+ if (!transcript || !pathExists(transcript)) return;
2262
2653
  const projectPath = projectRootFromPaths(await transcriptFilePaths(transcript)) ?? process.env.CLAUDE_PROJECT_DIR ?? cwd;
2263
2654
  const pHash = projectHash(projectPath);
2264
2655
  const cacheDir = cacheDirFor("tests", projectPath, home);
@@ -2318,7 +2709,7 @@ async function acquireLock(lockDir, timeoutMs = 5e3) {
2318
2709
  } catch {}
2319
2710
  };
2320
2711
  } catch {
2321
- await Bun.sleep(100);
2712
+ await sleep(100);
2322
2713
  }
2323
2714
  return null;
2324
2715
  }
@@ -2384,16 +2775,7 @@ async function taskComplete(file, id) {
2384
2775
  /** True when the project has uncommitted git changes. */
2385
2776
  async function hasGitChanges(cwd) {
2386
2777
  try {
2387
- const proc = Bun.spawn([
2388
- "git",
2389
- "status",
2390
- "--porcelain"
2391
- ], {
2392
- cwd,
2393
- stdout: "pipe",
2394
- stderr: "ignore"
2395
- });
2396
- return (await new Response(proc.stdout).text()).trim().length > 0;
2778
+ return spawnCapture("git", ["status", "--porcelain"], cwd).trim().length > 0;
2397
2779
  } catch {
2398
2780
  return false;
2399
2781
  }
@@ -2516,7 +2898,8 @@ async function aipilotPostToolUse(payload, cwd) {
2516
2898
  function sessionStart(input) {
2517
2899
  if (input.scope === "solid") return solidDetectStart();
2518
2900
  if (input.scope === "rules") return injectRules(process.env.CLAUDE_PLUGIN_ROOT ?? input.cwd);
2519
- if (input.scope === "carto") return cartoSessionStart(input.cwd);
2901
+ if (input.scope === "carto") return cartoSessionStart(input.cwd, input.now);
2902
+ if (input.scope === "lessons") return dispatchLessons("SessionStart", input.payload, input.cwd, input.now);
2520
2903
  return sessionStartCore(input.cwd, void 0, input.now);
2521
2904
  }
2522
2905
  /**
@@ -2530,7 +2913,11 @@ function dispatchLifecycle(input) {
2530
2913
  switch (input.event) {
2531
2914
  case "SessionStart": return sessionStart(input);
2532
2915
  case "UserPromptSubmit": return input.scope === "rules" ? injectRules(process.env.CLAUDE_PLUGIN_ROOT ?? input.cwd) : null;
2533
- case "SubagentStart": return input.scope === "aipilot" ? "" : subagentCacheContext(input.payload.session_id);
2916
+ case "SubagentStart":
2917
+ if (input.scope === "aipilot") return "";
2918
+ if (input.scope === "lessons") return dispatchLessons("SubagentStart", input.payload, input.cwd, input.now);
2919
+ return subagentCacheContext(input.payload.session_id);
2920
+ case "Stop": return input.scope === "lessons" ? dispatchLessons("Stop", input.payload, input.cwd, input.now) : null;
2534
2921
  case "SubagentStop": return input.scope === "aipilot" ? "" : trackAgentMemory(input.payload, void 0, input.now);
2535
2922
  case "TeammateIdle": return validateTeammateOutput(input.payload);
2536
2923
  case "PostToolUseFailure":
@@ -2715,13 +3102,16 @@ function trackWatchResearch(tool, input, now = Date.now(), home = homedir()) {
2715
3102
  /**
2716
3103
  * Dispatch the appropriate PostToolUse tracker for the invoking scope. Carto
2717
3104
  * persists manual enrichments; security records skill reads + MCP research;
2718
- * changelog records watch research. Side-effect only.
3105
+ * changelog records watch research; lessons arms the per-project throttle.
3106
+ * Side-effect only.
2719
3107
  * @param scope - The invoking plugin scope.
2720
3108
  * @param event - The normalized event.
2721
3109
  * @param input - The raw tool input.
2722
3110
  * @param now - Clock.
3111
+ * @param payload - The raw hook payload (for the lessons mark).
3112
+ * @param cwd - The project root (for the lessons mark).
2723
3113
  */
2724
- function postTrackingSideEffects(scope, event, input, now) {
3114
+ function postTrackingSideEffects(scope, event, input, now, payload = {}, cwd = process.cwd()) {
2725
3115
  if (scope === "carto" && (event.tool === "Edit" || event.tool === "Write") && event.filePath) {
2726
3116
  trackEnrichment(event.filePath);
2727
3117
  return;
@@ -2731,7 +3121,11 @@ function postTrackingSideEffects(scope, event, input, now) {
2731
3121
  trackMcpResearch(event.tool, input, now);
2732
3122
  return;
2733
3123
  }
2734
- if (scope === "changelog") trackWatchResearch(event.tool, input, now);
3124
+ if (scope === "changelog") {
3125
+ trackWatchResearch(event.tool, input, now);
3126
+ return;
3127
+ }
3128
+ if (scope === "lessons") dispatchLessons("PostToolUse", payload, cwd, now);
2735
3129
  }
2736
3130
  //#endregion
2737
3131
  //#region src/runtime/lifecycle/security/check-skill.ts
@@ -2765,6 +3159,42 @@ function securityAdvisory(tool, filePath, now = Date.now(), home = homedir()) {
2765
3159
  } });
2766
3160
  }
2767
3161
  //#endregion
3162
+ //#region src/runtime/lifecycle/seo/post-tool-use.ts
3163
+ /**
3164
+ * SEO PostToolUse handler (fs effects). Ports `seo/hooks/validate-seo.ts`: on an
3165
+ * edited HTML-like file under a `.fuse-seo` marker, deny when SEO elements are
3166
+ * missing. Opt-in only — silent when no marker / non-HTML / file unreadable.
3167
+ */
3168
+ /**
3169
+ * Validate the edited file's SEO completeness. Returns a deny message (for a
3170
+ * `permissionDecision: deny` response) when HTML-like, opted-in, and missing
3171
+ * elements; otherwise `null` (allow).
3172
+ * @param payload - The raw PostToolUse payload.
3173
+ * @returns The deny reason string, or `null` to allow.
3174
+ */
3175
+ function seoPostToolUse(payload) {
3176
+ const path = payload.tool_input?.file_path;
3177
+ if (!path || !isHtmlLike(path)) return null;
3178
+ if (!walkUpFor(typeof payload.cwd === "string" ? payload.cwd : dirname(path), ".fuse-seo")) return null;
3179
+ try {
3180
+ const missing = missingSeoElements(readFileSync(path, "utf-8"));
3181
+ if (missing.length === 0) return null;
3182
+ return `fuse-seo: missing SEO elements in ${path}:\n - ${missing.join("\n - ")}`;
3183
+ } catch {
3184
+ return null;
3185
+ }
3186
+ }
3187
+ /**
3188
+ * SEO PostToolUse as a ready native response: a `permissionDecision: deny`
3189
+ * string when the edited file is missing SEO elements, else `null` (allow).
3190
+ * @param payload - The raw PostToolUse payload.
3191
+ * @returns The deny response string, or `null` to allow.
3192
+ */
3193
+ function seoPostToolUseResponse(payload) {
3194
+ const deny = seoPostToolUse(payload);
3195
+ return deny ? denyResponse("PostToolUse", deny) : null;
3196
+ }
3197
+ //#endregion
2768
3198
  //#region src/runtime/lifecycle-bridge.ts
2769
3199
  /** Raw event name from a payload (Cline lacks one; lifecycle is Claude-only). */
2770
3200
  function rawEvent(payload) {
@@ -3328,7 +3758,12 @@ async function handleHook(id, payload, opts) {
3328
3758
  responseLength: extractText(response).length
3329
3759
  });
3330
3760
  if (activity) await recordActivity(file, activity);
3331
- postTrackingSideEffects(opts.scope ?? "core", event, event.input, opts.now);
3761
+ postTrackingSideEffects(opts.scope ?? "core", event, event.input, opts.now, payload, opts.cwd);
3762
+ const seoDeny = opts.scope === "seo" ? seoPostToolUseResponse(payload) : null;
3763
+ if (seoDeny) return {
3764
+ stdout: seoDeny,
3765
+ exit: 0
3766
+ };
3332
3767
  if (opts.scope === "aipilot" && (event.tool === "TaskCreate" || event.tool === "TaskUpdate")) {
3333
3768
  const out = await aipilotPostToolUse(payload, opts.cwd);
3334
3769
  if (out) return {
@@ -3353,4 +3788,4 @@ async function handleHook(id, payload, opts) {
3353
3788
  });
3354
3789
  }
3355
3790
  //#endregion
3356
- export { purgeTtlTree as $, isProject as A, cleanupSession as B, todayUtc as C, activityFor as Ct, dispatchAipilot as D, aipilotPostToolUse as E, getFileDesc as F, subagentCacheContext as G, logToolFailure as H, listChildren as I, injectRules as J, detectSolidProfile as K, postEditTypescript as L, loadEnriched as M, mergeLines as N, cartoSessionStart as O, countFiles as P, pruneEmptyDirs as Q, trackSessionChanges as R, securityStatePath as S, queryOf as St, dispatchLifecycle as T, validateTeammateOutput as U, saveApexState as V, trackAgentMemory as W, runSessionStartCleanups as X, readRules as Y, sessionStartCore as Z, trackSkillRead as _, normalizeEvent as _t, TRIVIAL_BUDGET as a, claudeHome as at, saveSecurityState as b, mcpPostStore as bt, detectDuplication as c, sanitizeSessionId as ct, lifecycleStdout as d, sessionsDir as dt, removeOldFiles as et, postEditContext as f, promptSubmitContext as ft, trackMcpResearch as g, trackFile as gt, trackWatchResearch as h, recordActivity as ht, REQUIRED_AGENTS as i, projectContext as it, writeTree as j, generateProjectMap as k, dryGate as l, saveSessionState as lt, postTrackingSideEffects as m, respond as mt, handlePre as n, devContext as nt, gate as o, fusengineCache as ot, securityAdvisory as p, taskContext as pt, solidDetectStart as q, DEFAULT_WINDOW_MS as r, gitContext as rt, preCommitGate as s, loadSessionState as st, handleHook as t, trimLogFile as tt, extractSymbols as u, sessionStatePath as ut, isoUtc as v, MCP_TTL_MS as vt, trackEnrichment as w, securityStateDir as x, mcpPreIntercept as xt, loadSecurityState as y, isMcpTool as yt, validateRulesLoaded as z };
3791
+ export { detectSolidProfile as $, dispatchLessons as A, activityFor as At, mergeLines as B, securityStateDir as C, trackFile as Ct, dispatchLifecycle as D, mcpPostStore as Dt, trackEnrichment as E, isMcpTool as Et, writePluginMap as F, trackSessionChanges as G, getFileDesc as H, generateProjectMap as I, saveApexState as J, validateRulesLoaded as K, isProject as L, lessonsStateFileFor as M, cartoSessionStart as N, aipilotPostToolUse as O, mcpPreIntercept as Ot, generateEcosystemMap as P, subagentCacheContext as Q, writeTree as R, saveSecurityState as S, recordActivity as St, todayUtc as T, MCP_TTL_MS as Tt, listChildren as U, countFiles as V, postEditTypescript as W, validateTeammateOutput as X, logToolFailure as Y, trackAgentMemory as Z, trackWatchResearch as _, sessionStatePath as _t, TRIVIAL_BUDGET as a, pruneEmptyDirs as at, isoUtc as b, taskContext as bt, detectDuplication as c, trimLogFile as ct, lifecycleStdout as d, projectContext as dt, solidDetectStart as et, postEditContext as f, claudeHome as ft, postTrackingSideEffects as g, saveSessionState as gt, securityAdvisory as h, sanitizeSessionId as ht, REQUIRED_AGENTS as i, sessionStartCore as it, lessonsFileFor as j, dispatchAipilot as k, queryOf as kt, dryGate as l, devContext as lt, seoPostToolUseResponse as m, loadSessionState as mt, handlePre as n, readRules as nt, gate as o, purgeTtlTree as ot, seoPostToolUse as p, fusengineCache as pt, cleanupSession as q, DEFAULT_WINDOW_MS as r, runSessionStartCleanups as rt, preCommitGate as s, removeOldFiles as st, handleHook as t, injectRules as tt, extractSymbols as u, gitContext as ut, trackMcpResearch as v, sessionsDir as vt, securityStatePath as w, normalizeEvent as wt, loadSecurityState as x, respond as xt, trackSkillRead as y, promptSubmitContext as yt, loadEnriched as z };