@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.
- package/dist/adapters/claude/index.mjs +1 -1
- package/dist/adapters/cline/index.mjs +1 -1
- package/dist/adapters/codex/index.mjs +1 -1
- package/dist/adapters/cursor/index.mjs +1 -1
- package/dist/adapters/gemini/index.mjs +1 -1
- package/dist/claude-BatVYnAf.mjs +137 -0
- package/dist/cli/bin.mjs +6 -4
- package/dist/cli/index.mjs +1 -1
- package/dist/{evaluate-j3gRJ_ng.mjs → evaluate-9ch1K2kt.mjs} +1 -1
- package/dist/{handle-DW9cWdVt.mjs → handle-Cxgzd4pZ.mjs} +516 -81
- package/dist/{index-C1vLIMwN.d.mts → index-BEMumjOw.d.mts} +8 -1
- package/dist/{index-mISsk0ff.d.mts → index-DmbOUJK8.d.mts} +12 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +5 -5
- package/dist/init/index.mjs +1 -1
- package/dist/memory/index.mjs +2 -1
- package/dist/memory-la_KkjCS.mjs +1 -0
- package/dist/policy/index.d.mts +2 -2
- package/dist/policy/index.mjs +3 -3
- package/dist/{project-root-ff0_poWU.mjs → project-root-3kk7gCOp.mjs} +7 -1
- package/dist/{run-CXsV-wIJ.mjs → run-CQbtlAKa.mjs} +2 -2
- package/dist/{run-D91N4ul1.mjs → run-Do2JltgU.mjs} +2 -2
- package/dist/runtime/index.d.mts +77 -8
- package/dist/runtime/index.mjs +2 -2
- package/dist/util/index.d.mts +2 -2
- package/dist/util/index.mjs +2 -2
- package/dist/{describe-BYqhoV4c.mjs → validate-CccewDwk.mjs} +47 -2
- package/package.json +1 -1
- package/dist/claude-B9FYp0Yw.mjs +0 -66
- /package/dist/{memory-BkoEbdec.mjs → registry-BkoEbdec.mjs} +0 -0
|
@@ -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 {
|
|
4
|
-
import {
|
|
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-
|
|
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
|
|
1465
|
-
* `generate_project_map.py
|
|
1466
|
-
*
|
|
1467
|
-
*
|
|
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.
|
|
1471
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
|
|
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
|
|
1517
|
-
const agents =
|
|
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(
|
|
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
|
-
|
|
1643
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1771
|
-
if (!
|
|
1772
|
-
const content =
|
|
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
|
|
1809
|
-
const
|
|
1810
|
-
"
|
|
1811
|
-
"
|
|
1812
|
-
"
|
|
1813
|
-
"
|
|
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
|
|
2218
|
+
* @returns Absolute paths matching the source extensions.
|
|
1820
2219
|
*/
|
|
1821
2220
|
async function scanSourceFiles(projectPath, maxFiles = 200) {
|
|
1822
2221
|
const files = [];
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 = (
|
|
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 || !
|
|
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
|
|
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
|
-
|
|
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 || !
|
|
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 =
|
|
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 || !
|
|
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
|
|
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
|
-
|
|
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":
|
|
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
|
|
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")
|
|
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 {
|
|
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 };
|