@fusengine/harness 0.1.30 → 0.1.31

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
@@ -3,7 +3,7 @@ import { r as resolveTtlSec } from "../ttl-BG55s6HZ.mjs";
3
3
  import { t as detectHarness } from "../harness-C8Nxxyn_.mjs";
4
4
  import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-CXsV-wIJ.mjs";
5
5
  import { n as writeInitFile, t as initFor } from "../run-D91N4ul1.mjs";
6
- import { t as handleHook } from "../handle-USWK4NSE.mjs";
6
+ import { t as handleHook } from "../handle-DW9cWdVt.mjs";
7
7
  //#region src/cli/bin.ts
8
8
  /**
9
9
  * harness — CLI for @fusengine/harness.
@@ -32,7 +32,8 @@ if (cmd === "hook") {
32
32
  "rules",
33
33
  "carto",
34
34
  "security",
35
- "changelog"
35
+ "changelog",
36
+ "aipilot"
36
37
  ])).has(scopeArg) ? scopeArg : "core";
37
38
  const outcome = await handleHook(id, await readStdin(), {
38
39
  now: Date.now(),
@@ -708,11 +708,10 @@ function detectClaudeMdProjectType(cwd) {
708
708
  function buildApexInstruction(projectType, maxLines) {
709
709
  return `INSTRUCTION: This is a development task. Use APEX methodology:
710
710
 
711
- **TRACKING FILE**: [project]/.claude/apex/task.json
711
+ **TRACKING FILE**: [project]/.claude/apex/task.json (auto-created on first Write/Edit)
712
712
 
713
- 1. **ANALYZE** (3 AGENTS IN PARALLEL):
714
- - explore-codebase + research-expert + general-purpose
715
- - Project type: ${projectType}\n\n2. **PLAN**: TaskCreate (<${maxLines} lines per file)\n\n3. **EXECUTE**: ${projectType}-expert, SOLID principles\n\n4. **EXAMINE**: Run sniper agent after ANY modification`;
713
+ 1. **ANALYZE** (MANDATORY - 3 AGENTS IN PARALLEL):
714
+ - explore-codebase + research-expert + ${projectType}-expert (framework expertise)\n - Project type detected: ${projectType}\n\n2. **PLAN**: Use TaskCreate to break down tasks (<${maxLines} lines per file)\n\n3. **EXECUTE**: ${projectType}-expert, follow SOLID principles, split at ${maxLines - 10} lines\n\n4. **EXAMINE**: Run sniper agent after ANY modification\n\n**IMPORTANT**: Read .claude/apex/task.json to check documentation status before writing code.`;
716
715
  }
717
716
  /**
718
717
  * Build the UserPromptSubmit injection text: read `~/.claude/CLAUDE.md` and,
@@ -1,16 +1,20 @@
1
+ import { r as resolveMaxLines } from "./limits-CHn8AIL1.mjs";
1
2
  import { r as projectLayout } from "./layout-C0jaaCQC.mjs";
2
- 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-CPtgUzFS.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";
3
4
  import { j as detectFramework, k as countLines, n as FAIL_CLOSED, t as evaluate } from "./evaluate-j3gRJ_ng.mjs";
4
5
  import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
5
6
  import { a as extractText, o as loadIndex, r as cacheStore, t as cacheLookup } from "./store-PrNPm6So.mjs";
6
- import { t as atomicWrite } from "./json-io-CAn72gI4.mjs";
7
+ import { i as writeJsonFile, r as readJsonFile, t as atomicWrite } from "./json-io-CAn72gI4.mjs";
7
8
  import { t as loadRefs } from "./loader-CyAoJv2W.mjs";
8
9
  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";
9
10
  import { t as contextResponse } from "./claude-B9FYp0Yw.mjs";
10
11
  import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
11
- import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
12
+ import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
12
13
  import { homedir, tmpdir } from "node:os";
14
+ import { createHash } from "node:crypto";
15
+ import { mkdir, rmdir } from "node:fs/promises";
13
16
  import { execFileSync } from "node:child_process";
17
+ import { Glob } from "bun";
14
18
  //#region src/runtime/activity.ts
15
19
  /** Min response length (chars) for a lead agent call to count as `sufficient`. */
16
20
  const AGENT_QUALITY_MIN = 500;
@@ -1473,6 +1477,1040 @@ function cartoSessionStart(cwd) {
1473
1477
  return "";
1474
1478
  }
1475
1479
  //#endregion
1480
+ //#region src/runtime/lifecycle/aipilot/inject-apex.ts
1481
+ /**
1482
+ * SubagentStart (matcher "") for the ai-pilot scope: inject APEX AGENTS.md +
1483
+ * task context + cartographer paths into every sub-agent. Ports
1484
+ * `inject-subagent-context.ts`. The core-scope SubagentStart already surfaces
1485
+ * the MCP cache table (`subagent-cache.ts`) in a separate hook entry — this
1486
+ * handler only emits the APEX/cartographer block, so there is no double-emit.
1487
+ */
1488
+ /** Last 3 completed task subjects (newest first), or "none". */
1489
+ function completedTasks(tasks) {
1490
+ const done = Object.entries(tasks).filter(([, t]) => t.status === "completed").sort((a, b) => (b[1].completed_at ?? "").localeCompare(a[1].completed_at ?? "")).slice(0, 3).map(([id, t]) => `#${id}: ${t.subject}`);
1491
+ return done.length > 0 ? done.join(", ") : "none";
1492
+ }
1493
+ /** Pending task subjects, or "none". */
1494
+ function pendingTasks(tasks) {
1495
+ const pending = Object.entries(tasks).filter(([, t]) => t.status === "pending").map(([id, t]) => `#${id}: ${t.subject}`);
1496
+ return pending.length > 0 ? pending.join(", ") : "none";
1497
+ }
1498
+ /** Cartographer navigation block, or "" when no plugin map is present. */
1499
+ function cartographerContext() {
1500
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
1501
+ if (!pluginRoot) return "";
1502
+ const pluginsMap = resolve(pluginRoot, "..", ".cartographer", "index.md");
1503
+ if (!existsSync(pluginsMap)) return "";
1504
+ return `\n### 7. Cartographer Maps\nNavigate branches (index.md) -> leaves link to real files:\n- Plugin skills: ${pluginsMap}\n- Project files: .cartographer/project/index.md`;
1505
+ }
1506
+ /**
1507
+ * Build the APEX sub-agent injection for SubagentStart, or "" when the project
1508
+ * has no `.claude/apex/` dir. Reads AGENTS.md (first 4KB) + task.json.
1509
+ * @param cwd - Fallback project root when `CLAUDE_PROJECT_DIR` is unset.
1510
+ * @param home - Home dir (unused placeholder; kept for symmetry/testing).
1511
+ * @returns The native hook stdout (possibly empty).
1512
+ */
1513
+ async function injectApexSubagentContext(cwd, home = homedir()) {
1514
+ const apexDir = join(process.env.CLAUDE_PROJECT_DIR ?? cwd, ".claude", "apex");
1515
+ 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) : "";
1518
+ const taskData = await readJsonFile(join(apexDir, "task.json"));
1519
+ return contextResponse("SubagentStart", `## APEX Sub-Agent Instructions
1520
+
1521
+ You are a sub-agent in APEX workflow. Follow these rules:
1522
+
1523
+ ### 1. AGENTS.md Rules
1524
+ ${agents}
1525
+
1526
+ ### 2. Task Context
1527
+ - Last completed: ${taskData ? completedTasks(taskData.tasks) : "none"}
1528
+ - Pending: ${taskData ? pendingTasks(taskData.tasks) : "none"}
1529
+
1530
+ ### 3. Before Starting Work
1531
+ - Use TaskUpdate(taskId, status: in_progress) before starting
1532
+
1533
+ ### 4. SOLID Rules
1534
+ - Files < ${resolveMaxLines()} lines | Interfaces in src/interfaces/ | JSDoc/PHPDoc required
1535
+
1536
+ ### 5. Research Before Code
1537
+ - Use Context7/Exa for docs | Write notes to .claude/apex/docs/
1538
+
1539
+ ### 6. When Done
1540
+ - TaskUpdate(taskId, status: completed) triggers auto-commit${cartographerContext()}`);
1541
+ }
1542
+ //#endregion
1543
+ //#region src/runtime/lifecycle/aipilot/cache-base.ts
1544
+ /**
1545
+ * Shared cache-path + age helpers for the ai-pilot scope.
1546
+ * Reuses the harness home-state (`fusengineCache`) so the ai-pilot caches live
1547
+ * under the same `~/.claude/fusengine-cache` tree as the core MCP cache — no
1548
+ * second cache layer.
1549
+ */
1550
+ /** 16-char hex SHA-256 of `text` (project hash / doc topic key). */
1551
+ function hashText16(text) {
1552
+ return createHash("sha256").update(text).digest("hex").slice(0, 16);
1553
+ }
1554
+ /** 16-char project hash from its absolute path. */
1555
+ function projectHash(projectPath) {
1556
+ return hashText16(projectPath);
1557
+ }
1558
+ /** `~/.claude/fusengine-cache` base dir (shared with the core MCP cache). */
1559
+ function cacheBaseDir(home = homedir()) {
1560
+ return fusengineCache(home);
1561
+ }
1562
+ /** Per-type, per-project cache dir: `fusengine-cache/<type>/<projectHash>`. */
1563
+ function cacheDirFor(type, projectPath, home = homedir()) {
1564
+ return join(cacheBaseDir(home), type, projectHash(projectPath));
1565
+ }
1566
+ /** Age in seconds from an ISO timestamp (now - ts). */
1567
+ function cacheAge(ts, now = Date.now()) {
1568
+ return Math.floor((now - new Date(ts).getTime()) / 1e3);
1569
+ }
1570
+ /** Full SHA-256 hex checksum of a file's text; "" when unreadable. */
1571
+ async function fileChecksum(path) {
1572
+ try {
1573
+ return createHash("sha256").update(await Bun.file(path).text()).digest("hex");
1574
+ } catch {
1575
+ return "";
1576
+ }
1577
+ }
1578
+ //#endregion
1579
+ //#region src/runtime/lifecycle/aipilot/analytics.ts
1580
+ /**
1581
+ * Cache analytics for the ai-pilot scope: append hit/miss events to
1582
+ * `sessions.jsonl` and aggregate them into `summary.json` on SessionEnd.
1583
+ * Ports `cache-analytics-save.ts` + the `logCacheEvent` helper.
1584
+ */
1585
+ const TOKEN_WEIGHTS = {
1586
+ explore: 15e3,
1587
+ doc: 1e4,
1588
+ lessons: 3e3,
1589
+ tests: 5e3
1590
+ };
1591
+ const CATEGORIES = [
1592
+ "explore",
1593
+ "doc",
1594
+ "lessons",
1595
+ "tests"
1596
+ ];
1597
+ /** Append a single cache event to `analytics/sessions.jsonl` (best effort). */
1598
+ function logCacheEvent(type, action, projHash, extra = {}, home = homedir()) {
1599
+ try {
1600
+ const dir = join(cacheBaseDir(home), "analytics");
1601
+ mkdirSync(dir, { recursive: true });
1602
+ const entry = {
1603
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1604
+ session: String(Math.floor(Date.now() / 1e3)),
1605
+ type,
1606
+ action,
1607
+ project_hash: projHash,
1608
+ ...extra
1609
+ };
1610
+ appendFileSync(join(dir, "sessions.jsonl"), JSON.stringify(entry) + "\n");
1611
+ } catch {}
1612
+ }
1613
+ /** Count entries matching a `type`+`action`. */
1614
+ function countBy(entries, type, action) {
1615
+ return entries.filter((e) => e.type === type && e.action === action).length;
1616
+ }
1617
+ /** Format a hit-rate percentage string. */
1618
+ function hitRate(hits, misses) {
1619
+ const total = hits + misses;
1620
+ return total === 0 ? "0%" : `${Math.floor(hits * 100 / total)}%`;
1621
+ }
1622
+ /** Parse the JSONL session log into typed entries. */
1623
+ function parseEntries(raw) {
1624
+ return raw.split("\n").filter(Boolean).map((line) => {
1625
+ try {
1626
+ return JSON.parse(line);
1627
+ } catch {
1628
+ return null;
1629
+ }
1630
+ }).filter((e) => e !== null);
1631
+ }
1632
+ /**
1633
+ * Aggregate `sessions.jsonl` into `summary.json` and prune entries > 30 days.
1634
+ * Ports `cache-analytics-save.ts`. SessionEnd hook output is ignored by Claude,
1635
+ * so this returns nothing — it is a pure side-effect.
1636
+ * @param home - Home dir (defaults to `~`).
1637
+ * @param now - Clock (defaults to `Date.now()`).
1638
+ */
1639
+ async function cacheAnalyticsSave(home = homedir(), now = Date.now()) {
1640
+ const dir = join(cacheBaseDir(home), "analytics");
1641
+ const sessionsFile = join(dir, "sessions.jsonl");
1642
+ const file = Bun.file(sessionsFile);
1643
+ if (!await file.exists()) return;
1644
+ const raw = await file.text();
1645
+ if (!raw.trim()) return;
1646
+ const entries = parseEntries(raw);
1647
+ if (entries.length === 0) return;
1648
+ const hits = {};
1649
+ const misses = {};
1650
+ for (const cat of CATEGORIES) {
1651
+ hits[cat] = countBy(entries, cat, "hit") + (cat === "doc" ? countBy(entries, cat, "blocked") : 0);
1652
+ misses[cat] = countBy(entries, cat, "miss");
1653
+ }
1654
+ const tokensSaved = CATEGORIES.reduce((sum, c) => sum + (hits[c] ?? 0) * (TOKEN_WEIGHTS[c] ?? 0), 0);
1655
+ const sessionCount = new Set(entries.map((e) => e.session).filter(Boolean)).size;
1656
+ const old = await readJsonFile(join(dir, "summary.json"));
1657
+ const merged = {
1658
+ updated: new Date(now).toISOString(),
1659
+ total_sessions: (old?.total_sessions ?? 0) + sessionCount,
1660
+ cache_hits: {},
1661
+ cache_misses: {},
1662
+ hit_rates: {},
1663
+ estimated_tokens_saved: (old?.estimated_tokens_saved ?? 0) + tokensSaved
1664
+ };
1665
+ for (const cat of CATEGORIES) {
1666
+ merged.cache_hits[cat] = (old?.cache_hits?.[cat] ?? 0) + (hits[cat] ?? 0);
1667
+ merged.cache_misses[cat] = (old?.cache_misses?.[cat] ?? 0) + (misses[cat] ?? 0);
1668
+ merged.hit_rates[cat] = hitRate(merged.cache_hits[cat] ?? 0, merged.cache_misses[cat] ?? 0);
1669
+ }
1670
+ await writeJsonFile(join(dir, "summary.json"), merged, true);
1671
+ 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");
1674
+ }
1675
+ //#endregion
1676
+ //#region src/runtime/lifecycle/aipilot/inject-explore.ts
1677
+ /**
1678
+ * SubagentStart (matcher "explore-codebase") for the ai-pilot scope: serve a
1679
+ * cached architecture report when fresh + config-matching, else inject save
1680
+ * instructions. Ports `explore-cache-check.ts`.
1681
+ */
1682
+ const TTL_SECONDS$2 = 86400;
1683
+ const CONFIG_FILES = [
1684
+ "package.json",
1685
+ "tsconfig.json",
1686
+ "composer.json",
1687
+ "go.mod",
1688
+ "Cargo.toml",
1689
+ "Package.swift",
1690
+ "biome.json",
1691
+ ".eslintrc.js",
1692
+ ".eslintrc.json"
1693
+ ];
1694
+ /** Compute a config hash from git-tracked config files; "noconfig" on failure. */
1695
+ async function configHash(cwd) {
1696
+ try {
1697
+ const proc = Bun.spawn([
1698
+ "git",
1699
+ "ls-tree",
1700
+ "HEAD",
1701
+ ...CONFIG_FILES
1702
+ ], {
1703
+ cwd,
1704
+ stdout: "pipe",
1705
+ stderr: "ignore"
1706
+ });
1707
+ const output = await new Response(proc.stdout).text();
1708
+ return output.trim() ? hashText16(output) : "noconfig";
1709
+ } catch {
1710
+ return "noconfig";
1711
+ }
1712
+ }
1713
+ /** Build the cache-miss save-instructions block. */
1714
+ function missBlock(cacheDir, metaFile, snapFile, ts, cfgHash, projPath) {
1715
+ return `## EXPLORATION CACHE INSTRUCTIONS\nAfter completing your exploration, save the report for future runs:\n\`\`\`bash\nmkdir -p ${cacheDir}\ncat > ${metaFile} << 'METAEOF'\n{"timestamp":"${ts}","config_hash":"${cfgHash}","project":"${projPath}"}\nMETAEOF\n\`\`\`\nThen write your full exploration report (markdown) to: ${snapFile}`;
1716
+ }
1717
+ /**
1718
+ * SubagentStart for explore-codebase: inject cached architecture or save block.
1719
+ * @param cwd - Fallback project root (uses `CLAUDE_PROJECT_DIR` first).
1720
+ * @param home - Home dir (defaults to `~`).
1721
+ * @param now - Clock (defaults to `Date.now()`).
1722
+ * @returns The native hook stdout.
1723
+ */
1724
+ async function injectExploreCache(cwd, home = homedir(), now = Date.now()) {
1725
+ const projPath = process.env.CLAUDE_PROJECT_DIR ?? cwd;
1726
+ const pHash = projectHash(projPath);
1727
+ const cacheDir = cacheDirFor("explore", projPath, home);
1728
+ const metaFile = join(cacheDir, "metadata.json");
1729
+ const snapFile = join(cacheDir, "snapshot.md");
1730
+ const cfgHash = await configHash(projPath);
1731
+ let context = "";
1732
+ const meta = await readJsonFile(metaFile);
1733
+ const snapBunFile = Bun.file(snapFile);
1734
+ const snapshot = await snapBunFile.exists() ? await snapBunFile.text() : "";
1735
+ if (meta?.timestamp && snapshot) {
1736
+ const age = cacheAge(meta.timestamp, now);
1737
+ if (age < TTL_SECONDS$2 && meta.config_hash === cfgHash) {
1738
+ context = `## CACHED ARCHITECTURE AVAILABLE (age: ${Math.floor(age / 60)}min)\nUSE this cached report. Do NOT run full exploration. Return it immediately.\n\n${snapshot}`;
1739
+ logCacheEvent("explore", "hit", pHash, {}, home);
1740
+ }
1741
+ }
1742
+ if (!context) {
1743
+ logCacheEvent("explore", "miss", pHash, {}, home);
1744
+ context = missBlock(cacheDir, metaFile, snapFile, new Date(now).toISOString().replace(/\.\d+Z$/, ""), cfgHash, projPath);
1745
+ }
1746
+ return contextResponse("SubagentStart", context);
1747
+ }
1748
+ //#endregion
1749
+ //#region src/runtime/lifecycle/aipilot/inject-doc.ts
1750
+ /**
1751
+ * SubagentStart (matcher "research-expert") for the ai-pilot scope: inject
1752
+ * cached documentation summaries. Ports `doc-cache-inject.ts`. Doc *saving*
1753
+ * happens on SubagentStop (`cache-doc.ts`).
1754
+ */
1755
+ const TTL_SECONDS$1 = 604800;
1756
+ const MAX_SIZE = 8192;
1757
+ /** Concatenate fresh, dedup-by-hash cached doc bodies. */
1758
+ async function buildDocsContext(entries, docsDir, now) {
1759
+ let ctx = "";
1760
+ let count = 0;
1761
+ let maxAge = 0;
1762
+ const seen = /* @__PURE__ */ new Set();
1763
+ for (const entry of entries) {
1764
+ if (!entry.timestamp) continue;
1765
+ const age = cacheAge(entry.timestamp, now);
1766
+ if (age >= TTL_SECONDS$1) continue;
1767
+ if (age > maxAge) maxAge = age;
1768
+ if (!entry.hash || seen.has(entry.hash)) continue;
1769
+ 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();
1773
+ if (!content) continue;
1774
+ ctx += `\n${content}\n`;
1775
+ count++;
1776
+ }
1777
+ return {
1778
+ ctx,
1779
+ count,
1780
+ maxAge
1781
+ };
1782
+ }
1783
+ /**
1784
+ * SubagentStart for research-expert: inject cached doc summaries, or "".
1785
+ * @param cwd - Fallback project root (uses `CLAUDE_PROJECT_DIR` first).
1786
+ * @param home - Home dir (defaults to `~`).
1787
+ * @param now - Clock (defaults to `Date.now()`).
1788
+ * @returns The native hook stdout (possibly empty).
1789
+ */
1790
+ async function injectDocCache(cwd, home = homedir(), now = Date.now()) {
1791
+ const projPath = process.env.CLAUDE_PROJECT_DIR ?? cwd;
1792
+ const pHash = projectHash(projPath);
1793
+ const cacheDir = cacheDirFor("doc", projPath, home);
1794
+ const index = await readJsonFile(join(cacheDir, "index.json"));
1795
+ if (!index?.docs?.length) return "";
1796
+ const { ctx, count, maxAge } = await buildDocsContext(index.docs, join(cacheDir, "docs"), now);
1797
+ if (count === 0) return "";
1798
+ logCacheEvent("doc", "hit", pHash, { docs_injected: count }, home);
1799
+ return contextResponse("SubagentStart", `${`## CACHED DOCUMENTATION (${count} docs, ${Math.ceil(maxAge / 3600)}h ago)\nUse this knowledge. Only query Context7 for topics NOT covered below.\n`}${ctx}Full docs: ${join(cacheDir, "docs")}/`.slice(0, MAX_SIZE));
1800
+ }
1801
+ //#endregion
1802
+ //#region src/runtime/lifecycle/aipilot/source-scan.ts
1803
+ /**
1804
+ * Source-file scanning + project-stack detection for the ai-pilot scope.
1805
+ * Ported from the ai-pilot plugin's `cache/source-collector.ts` +
1806
+ * the stack detection in `cache/lesson-helpers.ts` (now removed).
1807
+ */
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
+ ];
1815
+ /**
1816
+ * Scan source files in `projectPath` (monorepo-aware), capped at `maxFiles`.
1817
+ * @param projectPath - Absolute project root.
1818
+ * @param maxFiles - Max files to collect (default 200).
1819
+ * @returns Absolute paths matching the source patterns.
1820
+ */
1821
+ async function scanSourceFiles(projectPath, maxFiles = 200) {
1822
+ 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 {}
1834
+ if (files.length >= maxFiles) break;
1835
+ }
1836
+ return files;
1837
+ }
1838
+ /** Detect the project stack from config files in the project root. */
1839
+ function detectStack(projectPath) {
1840
+ try {
1841
+ const entries = readdirSync(projectPath);
1842
+ if (entries.some((f) => f.startsWith("next.config"))) return "nextjs";
1843
+ if (entries.includes("composer.json")) return "laravel";
1844
+ if (entries.some((f) => f.endsWith(".xcodeproj")) || entries.includes("Package.swift")) return "swift";
1845
+ if (entries.some((f) => f.startsWith("tailwind.config"))) return "tailwindcss";
1846
+ } catch {}
1847
+ return "universal";
1848
+ }
1849
+ //#endregion
1850
+ //#region src/runtime/lifecycle/aipilot/lessons.ts
1851
+ /**
1852
+ * Lesson aggregation, dedup, merge, and edit categorization for the ai-pilot
1853
+ * scope. Ported from the ai-pilot plugin's `cache/lesson-aggregator.ts` +
1854
+ * `cache/lesson-helpers.ts` (now removed).
1855
+ */
1856
+ /** Deduplicate lessons by `error_type:pattern`, summing counts. */
1857
+ function dedupLessons(lessons) {
1858
+ const groups = /* @__PURE__ */ new Map();
1859
+ for (const l of lessons) {
1860
+ const key = `${l.error_type}:${l.pattern}`;
1861
+ const group = groups.get(key);
1862
+ if (group) group.push(l);
1863
+ else groups.set(key, [l]);
1864
+ }
1865
+ return [...groups.values()].map((entries) => {
1866
+ const first = entries[0];
1867
+ return {
1868
+ error_type: first.error_type,
1869
+ pattern: first.pattern,
1870
+ fix: first.fix,
1871
+ last_seen: first.last_seen,
1872
+ count: entries.reduce((s, e) => s + e.count, 0),
1873
+ files: [...new Set(entries.flatMap((e) => e.files))],
1874
+ code: { line: [...new Set(entries.flatMap((e) => e.code?.line ?? []))].slice(0, 5) }
1875
+ };
1876
+ }).sort((a, b) => b.count - a.count);
1877
+ }
1878
+ /** Aggregate all local lesson JSON files into a flat deduplicated list. */
1879
+ async function aggregateLocalLessons(cacheDir) {
1880
+ if (!existsSync(cacheDir)) return [];
1881
+ const files = readdirSync(cacheDir).filter((f) => f.endsWith(".json"));
1882
+ const all = [];
1883
+ for (const f of files) {
1884
+ const data = await readJsonFile(join(cacheDir, f));
1885
+ if (data?.errors) all.push(...data.errors);
1886
+ }
1887
+ return dedupLessons(all);
1888
+ }
1889
+ /** Load global lessons for a stack (stack-specific + universal). */
1890
+ async function loadGlobalLessons(stack, home = homedir()) {
1891
+ const globalDir = join(cacheBaseDir(home), "lessons", "_global");
1892
+ const result = [];
1893
+ for (const name of [`${stack}.json`, "universal.json"]) {
1894
+ const data = await readJsonFile(join(globalDir, name));
1895
+ if (data) result.push(...data);
1896
+ }
1897
+ return result;
1898
+ }
1899
+ /** Merge local + global lessons, dedup by type+pattern, sort by count desc. */
1900
+ function mergeLessons(local, global) {
1901
+ return dedupLessons([...local, ...global]);
1902
+ }
1903
+ /** Categorize an edit entry by analyzing the new code content. */
1904
+ function categorizeEdit(edit) {
1905
+ const n = edit.newStr.toLowerCase();
1906
+ if (n.includes("use client")) return "missing_directive";
1907
+ if (n.includes("displayname")) return "missing_display_name";
1908
+ if (/onkeydown|tabindex|role=/.test(n)) return "missing_a11y";
1909
+ if (/try|catch/.test(n)) return "missing_error_handling";
1910
+ if (/\?\?|if.*null/.test(n)) return "null_safety";
1911
+ return "code_fix";
1912
+ }
1913
+ //#endregion
1914
+ //#region src/runtime/lifecycle/aipilot/inject-lessons.ts
1915
+ /**
1916
+ * SubagentStart (matcher "") for the ai-pilot scope: inject cached lessons
1917
+ * (known project issues) into every agent. Ports `lessons-cache-inject.ts`.
1918
+ */
1919
+ const MAX_AGE_MS = 30 * 864e5;
1920
+ const MAX_LESSONS = 10;
1921
+ /** Remove JSON files older than 30 days from `dir` (best effort). */
1922
+ function pruneOldFiles(dir, now) {
1923
+ try {
1924
+ const cutoff = now - MAX_AGE_MS;
1925
+ for (const f of readdirSync(dir).filter((n) => n.endsWith(".json"))) {
1926
+ const path = `${dir}/${f}`;
1927
+ try {
1928
+ if (statSync(path).mtimeMs < cutoff) unlinkSync(path);
1929
+ } catch {}
1930
+ }
1931
+ } catch {}
1932
+ }
1933
+ /** Render lessons as a numbered known-issues list. */
1934
+ function formatLessonList(lessons) {
1935
+ return lessons.map((l, i) => {
1936
+ const code = l.code?.line?.length ? `\n Code: ${l.code.line.join(" | ").slice(0, 200)}` : "";
1937
+ return `${i + 1}. [${l.count}x] | ${l.pattern ?? "unknown"} -> ${l.fix ?? "see docs"}${code}`;
1938
+ }).join("\n");
1939
+ }
1940
+ /**
1941
+ * SubagentStart lessons injection: aggregate local + global lessons, or "".
1942
+ * @param cwd - Fallback project root (uses `CLAUDE_PROJECT_DIR` first).
1943
+ * @param home - Home dir (defaults to `~`).
1944
+ * @param now - Clock (defaults to `Date.now()`).
1945
+ * @returns The native hook stdout (possibly empty).
1946
+ */
1947
+ async function injectLessonsCache(cwd, home = homedir(), now = Date.now()) {
1948
+ const projectPath = process.env.CLAUDE_PROJECT_DIR ?? cwd;
1949
+ const pHash = projectHash(projectPath);
1950
+ const cacheDir = cacheDirFor("lessons", projectPath, home);
1951
+ const stack = detectStack(projectPath);
1952
+ pruneOldFiles(cacheDir, now);
1953
+ let localLessons = [];
1954
+ try {
1955
+ localLessons = await aggregateLocalLessons(cacheDir);
1956
+ } catch {}
1957
+ const globalLessons = await loadGlobalLessons(stack, home);
1958
+ if (localLessons.length === 0 && globalLessons.length === 0) return "";
1959
+ const merged = mergeLessons(localLessons, globalLessons).slice(0, MAX_LESSONS);
1960
+ if (merged.length === 0) return "";
1961
+ logCacheEvent("lessons", "hit", pHash, {
1962
+ count: merged.length,
1963
+ stack
1964
+ }, home);
1965
+ return contextResponse("SubagentStart", `## KNOWN PROJECT ISSUES (from previous sniper validations)\nThese errors have been found and fixed before. AVOID them:\n${formatLessonList(merged)}\n\nINSTRUCTION: Check your code against these known issues BEFORE submitting.`);
1966
+ }
1967
+ //#endregion
1968
+ //#region src/runtime/lifecycle/aipilot/inject-test.ts
1969
+ /**
1970
+ * SubagentStart (matcher "sniper") for the ai-pilot scope: tell sniper which
1971
+ * source files changed vs. already-validated. Ports `test-cache-inject.ts`.
1972
+ */
1973
+ const TTL_SECONDS = 172800;
1974
+ /**
1975
+ * SubagentStart for sniper: inject the changed-file list, or "" when nothing
1976
+ * useful to say (no cache / no unchanged files).
1977
+ * @param cwd - Fallback project root (uses `CLAUDE_PROJECT_DIR` first).
1978
+ * @param home - Home dir (defaults to `~`).
1979
+ * @param now - Clock (defaults to `Date.now()`).
1980
+ * @returns The native hook stdout (possibly empty).
1981
+ */
1982
+ async function injectTestCache(cwd, home = homedir(), now = Date.now()) {
1983
+ const projectPath = process.env.CLAUDE_PROJECT_DIR ?? cwd;
1984
+ const cache = await readJsonFile(join(cacheDirFor("tests", projectPath, home), "results.json"));
1985
+ if (!cache?.files || Object.keys(cache.files).length === 0) return "";
1986
+ const srcFiles = await scanSourceFiles(projectPath);
1987
+ if (srcFiles.length === 0) return "";
1988
+ const changed = [];
1989
+ let unchanged = 0;
1990
+ for (const filepath of srcFiles) {
1991
+ const relPath = filepath.replace(`${projectPath}/`, "");
1992
+ const cached = cache.files[relPath];
1993
+ if (!cached) {
1994
+ changed.push(relPath);
1995
+ continue;
1996
+ }
1997
+ if ((await fileChecksum(filepath)).slice(0, 16) !== cached.checksum) {
1998
+ changed.push(relPath);
1999
+ continue;
2000
+ }
2001
+ if (cached.last_tested && cacheAge(cached.last_tested, now) > TTL_SECONDS) {
2002
+ changed.push(relPath);
2003
+ continue;
2004
+ }
2005
+ unchanged++;
2006
+ }
2007
+ if (unchanged === 0) return "";
2008
+ const changedList = changed.map((f) => `- ${f}`).join("\n");
2009
+ return contextResponse("SubagentStart", `## TEST CACHE (${unchanged}/${srcFiles.length} files already validated)\nOnly run linters on these CHANGED files:\n${changedList}\nSKIP linting on ${unchanged} unchanged files - already PASS.`);
2010
+ }
2011
+ //#endregion
2012
+ //#region src/runtime/lifecycle/aipilot/transcript.ts
2013
+ /**
2014
+ * JSONL transcript parsing helpers for the ai-pilot scope: project-root
2015
+ * detection from file paths, file-path extraction, Edit extraction, and the
2016
+ * assistant report extraction. Ported into the harness from the ai-pilot
2017
+ * plugin's `cache/project-detect.ts` + `cache/lesson-helpers.ts` (now removed).
2018
+ */
2019
+ const ROOT_MARKERS = [
2020
+ ".git",
2021
+ ".hg",
2022
+ "turbo.json",
2023
+ "nx.json",
2024
+ "lerna.json",
2025
+ "pnpm-workspace.yaml"
2026
+ ];
2027
+ const PKG_MARKERS = [
2028
+ "package.json",
2029
+ "composer.json",
2030
+ "Package.swift",
2031
+ "Cargo.toml",
2032
+ "go.mod",
2033
+ "pyproject.toml",
2034
+ "Gemfile",
2035
+ "pom.xml"
2036
+ ];
2037
+ /** Detect the project root by walking up from the first file path to a marker. */
2038
+ function projectRootFromPaths(filePaths) {
2039
+ const firstPath = filePaths[0];
2040
+ if (!firstPath) return null;
2041
+ const lastSlash = firstPath.lastIndexOf("/");
2042
+ if (lastSlash <= 0) return null;
2043
+ let dir = firstPath.substring(0, lastSlash);
2044
+ let bestRoot = null;
2045
+ while (dir && dir !== "/" && dir.length > 1) {
2046
+ try {
2047
+ const entries = readdirSync(dir);
2048
+ if (ROOT_MARKERS.some((m) => entries.includes(m))) return dir;
2049
+ if (PKG_MARKERS.some((m) => entries.includes(m))) bestRoot = dir;
2050
+ } catch {
2051
+ break;
2052
+ }
2053
+ const parentSlash = dir.lastIndexOf("/");
2054
+ if (parentSlash <= 0) break;
2055
+ dir = dir.substring(0, parentSlash);
2056
+ }
2057
+ return bestRoot;
2058
+ }
2059
+ /** Extract all absolute file paths from tool_use entries in a JSONL transcript. */
2060
+ async function transcriptFilePaths(transcriptPath) {
2061
+ const text = await Bun.file(transcriptPath).text();
2062
+ const paths = /* @__PURE__ */ new Set();
2063
+ for (const line of text.split("\n").filter(Boolean)) try {
2064
+ const content = JSON.parse(line)?.message?.content;
2065
+ if (!Array.isArray(content)) continue;
2066
+ for (const block of content) {
2067
+ if (block?.type !== "tool_use") continue;
2068
+ const fp = block.input?.file_path ?? block.input?.path ?? "";
2069
+ if (typeof fp === "string" && fp.startsWith("/")) paths.add(fp);
2070
+ }
2071
+ } catch {}
2072
+ return [...paths];
2073
+ }
2074
+ /** Extract deduplicated Edit tool_use entries (keyed by basename) from a transcript. */
2075
+ async function transcriptEdits(transcriptPath) {
2076
+ const text = await Bun.file(transcriptPath).text();
2077
+ const edits = [];
2078
+ for (const line of text.split("\n").filter(Boolean)) try {
2079
+ const content = JSON.parse(line)?.message?.content;
2080
+ if (!Array.isArray(content)) continue;
2081
+ for (const block of content) if (block?.type === "tool_use" && block.name === "Edit" && block.input?.file_path) edits.push({
2082
+ file: block.input.file_path,
2083
+ oldStr: block.input.old_string ?? "",
2084
+ newStr: block.input.new_string ?? ""
2085
+ });
2086
+ } catch {}
2087
+ const seen = /* @__PURE__ */ new Map();
2088
+ for (const e of edits) seen.set(e.file.split("/").pop() ?? e.file, e);
2089
+ return [...seen.values()];
2090
+ }
2091
+ /** Extract the last assistant text report (first 500 lines) from a transcript. */
2092
+ async function transcriptReport(transcriptPath) {
2093
+ const text = await Bun.file(transcriptPath).text();
2094
+ let lastReport = "";
2095
+ for (const line of text.split("\n").filter(Boolean)) try {
2096
+ const entry = JSON.parse(line);
2097
+ if (entry?.message?.role !== "assistant") continue;
2098
+ for (const block of entry.message.content ?? []) if (block.type === "text" && block.text) lastReport = block.text;
2099
+ } catch {}
2100
+ return lastReport.split("\n").slice(0, 500).join("\n");
2101
+ }
2102
+ //#endregion
2103
+ //#region src/runtime/lifecycle/aipilot/cache-doc.ts
2104
+ /**
2105
+ * SubagentStop (matcher "research-expert") for the ai-pilot scope: extract the
2106
+ * agent's synthesis text from the transcript and cache it as documentation.
2107
+ * Ports `cache-doc-from-transcript.ts`. SubagentStop output here is a pure
2108
+ * side-effect (no stdout needed).
2109
+ */
2110
+ const TOOL_PATTERN = /context7__query-docs|exa__get_code_context|exa__web_search/;
2111
+ const MAX_DOC_SIZE = 20480;
2112
+ const MIN_TEXT_SIZE = 200;
2113
+ const MAX_DOCS = 15;
2114
+ const RETRY_DELAYS = [
2115
+ 500,
2116
+ 1e3,
2117
+ 2e3
2118
+ ];
2119
+ /** Extract the longest assistant synthesis + queried library ids from a transcript. */
2120
+ async function extractSynthesis(path) {
2121
+ const lines = (await Bun.file(path).text()).split("\n").filter(Boolean);
2122
+ const libraries = [];
2123
+ let synthesis = "";
2124
+ for (const line of lines) try {
2125
+ const entry = JSON.parse(line);
2126
+ const role = entry?.type ?? entry?.role;
2127
+ const contents = entry?.message?.content;
2128
+ if (!Array.isArray(contents)) continue;
2129
+ for (const block of contents) {
2130
+ if (block.type === "tool_use" && TOOL_PATTERN.test(block.name ?? "")) {
2131
+ const lib = block.input?.libraryId ?? block.input?.query ?? "";
2132
+ if (lib && !libraries.includes(lib)) libraries.push(lib);
2133
+ }
2134
+ if (role === "assistant" && block.type === "text" && typeof block.text === "string" && block.text.length > synthesis.length) synthesis = block.text;
2135
+ }
2136
+ } catch {}
2137
+ return {
2138
+ text: synthesis,
2139
+ libraries
2140
+ };
2141
+ }
2142
+ /**
2143
+ * SubagentStop research-expert: cache the synthesis text from the transcript.
2144
+ * @param transcript - Path to the agent JSONL transcript.
2145
+ * @param cwd - Fallback project root.
2146
+ * @param home - Home dir (defaults to `~`).
2147
+ */
2148
+ async function cacheDocFromTranscript(transcript, cwd, home = homedir()) {
2149
+ if (!transcript || !await Bun.file(transcript).exists()) return;
2150
+ const projPath = projectRootFromPaths(await transcriptFilePaths(transcript)) ?? process.env.CLAUDE_PROJECT_DIR ?? cwd;
2151
+ const cacheDir = cacheDirFor("doc", projPath, home);
2152
+ const docsDir = join(cacheDir, "docs");
2153
+ let result = await extractSynthesis(transcript);
2154
+ for (const delay of RETRY_DELAYS) {
2155
+ if (result.text.length >= MIN_TEXT_SIZE && result.libraries.length > 0) break;
2156
+ await Bun.sleep(delay);
2157
+ result = await extractSynthesis(transcript);
2158
+ }
2159
+ const { text, libraries } = result;
2160
+ if (text.length < MIN_TEXT_SIZE || libraries.length === 0) return;
2161
+ const indexFile = join(cacheDir, "index.json");
2162
+ const index = await readJsonFile(indexFile) ?? {
2163
+ project: projPath,
2164
+ docs: []
2165
+ };
2166
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2167
+ const content = text.slice(0, MAX_DOC_SIZE);
2168
+ const topic = libraries.join(", ");
2169
+ const docHash = hashText16(topic);
2170
+ await Bun.write(join(docsDir, `${docHash}.md`), content);
2171
+ const sizeKb = Math.floor(content.length / 1024);
2172
+ for (const lib of libraries) {
2173
+ index.docs = index.docs.filter((d) => d.library !== lib);
2174
+ index.docs.push({
2175
+ hash: docHash,
2176
+ library: lib,
2177
+ topic,
2178
+ timestamp,
2179
+ size_kb: sizeKb
2180
+ });
2181
+ }
2182
+ if (index.docs.length > MAX_DOCS) index.docs = index.docs.slice(-15);
2183
+ await writeJsonFile(indexFile, index, true);
2184
+ }
2185
+ //#endregion
2186
+ //#region src/runtime/lifecycle/aipilot/cache-lessons.ts
2187
+ /**
2188
+ * SubagentStop (matcher "sniper") for the ai-pilot scope: capture error
2189
+ * patterns + corrected code from the sniper transcript as lessons. Ports
2190
+ * `cache-sniper-lessons.ts` (the dead `promote-global-lessons.ts` spawn is
2191
+ * dropped — that helper no longer exists). Pure side-effect.
2192
+ */
2193
+ /**
2194
+ * SubagentStop sniper: extract lessons from the transcript and persist them.
2195
+ * @param transcript - Path to the agent JSONL transcript.
2196
+ * @param cwd - Fallback project root.
2197
+ * @param home - Home dir (defaults to `~`).
2198
+ */
2199
+ async function cacheSniperLessons(transcript, cwd, home = homedir()) {
2200
+ if (!transcript || !await Bun.file(transcript).exists()) return;
2201
+ const edits = await transcriptEdits(transcript);
2202
+ if (edits.length === 0) return;
2203
+ const projectPath = projectRootFromPaths(edits.map((e) => e.file)) ?? process.env.CLAUDE_PROJECT_DIR ?? cwd;
2204
+ const pHash = projectHash(projectPath);
2205
+ const cacheDir = cacheDirFor("lessons", projectPath, home);
2206
+ mkdirSync(cacheDir, { recursive: true });
2207
+ const report = await transcriptReport(transcript);
2208
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2209
+ const errors = edits.map((edit) => {
2210
+ const basename = edit.file.split("/").pop() ?? edit.file;
2211
+ const descLine = report.split("\n").find((l) => l.toLowerCase().includes(basename.toLowerCase()));
2212
+ const errorType = categorizeEdit(edit);
2213
+ return {
2214
+ error_type: errorType,
2215
+ pattern: descLine ?? `Code correction in ${basename}`,
2216
+ fix: `Fix ${errorType} in ${basename}`,
2217
+ count: 1,
2218
+ last_seen: timestamp,
2219
+ files: [edit.file],
2220
+ code: { line: (edit.newStr ?? "").split("\n").filter(Boolean).slice(0, 10) }
2221
+ };
2222
+ });
2223
+ await writeJsonFile(join(cacheDir, `${timestamp.replace(/:/g, "-")}.json`), {
2224
+ project: projectPath,
2225
+ timestamp,
2226
+ errors
2227
+ }, true);
2228
+ logCacheEvent("lessons", "hit", pHash, { count: edits.length }, home);
2229
+ }
2230
+ //#endregion
2231
+ //#region src/runtime/lifecycle/aipilot/cache-test.ts
2232
+ /**
2233
+ * SubagentStop (matcher "sniper") for the ai-pilot scope: extract linter
2234
+ * results from the transcript and cache per-file checksums. Ports
2235
+ * `cache-test-results.ts`. Pure side-effect.
2236
+ */
2237
+ /** Extract linter-related command/output text from a JSONL transcript. */
2238
+ async function extractLinterOutput(path) {
2239
+ const text = await Bun.file(path).text();
2240
+ const outputs = [];
2241
+ for (const line of text.split("\n").filter(Boolean)) try {
2242
+ const content = JSON.parse(line)?.message?.content;
2243
+ if (!Array.isArray(content)) continue;
2244
+ for (const block of content) {
2245
+ if (block.type === "tool_use" && block.name === "Bash") {
2246
+ const cmd = block.input?.command ?? "";
2247
+ if (/eslint|tsc|biome|npx.*lint/i.test(cmd)) outputs.push(cmd);
2248
+ }
2249
+ if (block.type === "tool_result" || block.type === "text") outputs.push(block.text ?? block.content ?? "");
2250
+ }
2251
+ } catch {}
2252
+ return outputs.join("\n");
2253
+ }
2254
+ /**
2255
+ * SubagentStop sniper: cache linter results per-file with checksums.
2256
+ * @param transcript - Path to the agent JSONL transcript.
2257
+ * @param cwd - Fallback project root.
2258
+ * @param home - Home dir (defaults to `~`).
2259
+ */
2260
+ async function cacheTestResults(transcript, cwd, home = homedir()) {
2261
+ if (!transcript || !await Bun.file(transcript).exists()) return;
2262
+ const projectPath = projectRootFromPaths(await transcriptFilePaths(transcript)) ?? process.env.CLAUDE_PROJECT_DIR ?? cwd;
2263
+ const pHash = projectHash(projectPath);
2264
+ const cacheDir = cacheDirFor("tests", projectPath, home);
2265
+ mkdirSync(cacheDir, { recursive: true });
2266
+ const resultsPath = join(cacheDir, "results.json");
2267
+ const linterOutput = await extractLinterOutput(transcript);
2268
+ if (!linterOutput) return;
2269
+ const srcFiles = await scanSourceFiles(projectPath);
2270
+ if (srcFiles.length === 0) return;
2271
+ const existing = await readJsonFile(resultsPath) ?? {
2272
+ timestamp: "",
2273
+ files: {}
2274
+ };
2275
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2276
+ const newFiles = {};
2277
+ for (const filepath of srcFiles) {
2278
+ const relPath = filepath.replace(`${projectPath}/`, "");
2279
+ const checksum = (await fileChecksum(filepath)).slice(0, 16);
2280
+ if (!checksum) continue;
2281
+ const basename = filepath.split("/").pop() ?? "";
2282
+ newFiles[relPath] = {
2283
+ checksum,
2284
+ eslint: linterOutput.includes(basename) && linterOutput.includes("error") ? "fail" : "pass",
2285
+ tsc: "pass",
2286
+ last_tested: timestamp
2287
+ };
2288
+ }
2289
+ await writeJsonFile(resultsPath, {
2290
+ timestamp,
2291
+ files: {
2292
+ ...existing.files,
2293
+ ...newFiles
2294
+ }
2295
+ }, true);
2296
+ logCacheEvent("tests", "hit", pHash, { count: Object.keys(newFiles).length }, home);
2297
+ }
2298
+ //#endregion
2299
+ //#region src/runtime/lifecycle/aipilot/apex-task-store.ts
2300
+ /**
2301
+ * APEX `task.json` mutation helpers + a directory-based lock for the ai-pilot
2302
+ * scope. Ported from the ai-pilot plugin's `apex/state.ts` + `apex/task-helpers.ts`
2303
+ * (now removed).
2304
+ */
2305
+ /**
2306
+ * Acquire a directory-based lock with a timeout.
2307
+ * @param lockDir - Path to the lock directory.
2308
+ * @param timeoutMs - Max wait in ms (default 5000).
2309
+ * @returns A release function, or null if acquisition timed out.
2310
+ */
2311
+ async function acquireLock(lockDir, timeoutMs = 5e3) {
2312
+ const start = Date.now();
2313
+ while (Date.now() - start < timeoutMs) try {
2314
+ await mkdir(lockDir, { recursive: false });
2315
+ return async () => {
2316
+ try {
2317
+ await rmdir(lockDir);
2318
+ } catch {}
2319
+ };
2320
+ } catch {
2321
+ await Bun.sleep(100);
2322
+ }
2323
+ return null;
2324
+ }
2325
+ /** Add a new task to `task.json`. */
2326
+ async function taskCreate(file, id, subject, desc) {
2327
+ const data = await readJsonFile(file);
2328
+ if (!data) return;
2329
+ data.tasks[id] = {
2330
+ subject,
2331
+ description: desc,
2332
+ status: "pending",
2333
+ phase: "pending",
2334
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
2335
+ doc_consulted: {},
2336
+ files_modified: [],
2337
+ blockedBy: []
2338
+ };
2339
+ await writeJsonFile(file, data);
2340
+ }
2341
+ /** Mark a task as in_progress in `task.json`. */
2342
+ async function taskStart(file, id, subject, desc, blocked) {
2343
+ const data = await readJsonFile(file);
2344
+ if (!data) return;
2345
+ if (!data.tasks[id]) data.tasks[id] = {
2346
+ subject: "",
2347
+ description: "",
2348
+ status: "in_progress",
2349
+ phase: "analyze",
2350
+ doc_consulted: {},
2351
+ files_modified: []
2352
+ };
2353
+ data.current_task = id;
2354
+ const task = data.tasks[id];
2355
+ Object.assign(task, {
2356
+ status: "in_progress",
2357
+ phase: "analyze",
2358
+ started_at: (/* @__PURE__ */ new Date()).toISOString()
2359
+ });
2360
+ if (subject) task.subject = subject;
2361
+ if (desc) task.description = desc;
2362
+ if (blocked) task.blockedBy = blocked.split(",");
2363
+ await writeJsonFile(file, data);
2364
+ }
2365
+ /** Mark a task as completed in `task.json`. */
2366
+ async function taskComplete(file, id) {
2367
+ const data = await readJsonFile(file);
2368
+ if (!data?.tasks[id]) return;
2369
+ Object.assign(data.tasks[id], {
2370
+ status: "completed",
2371
+ phase: "completed",
2372
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
2373
+ });
2374
+ await writeJsonFile(file, data);
2375
+ }
2376
+ //#endregion
2377
+ //#region src/runtime/lifecycle/aipilot/sync-task.ts
2378
+ /**
2379
+ * PostToolUse (matcher "TaskCreate|TaskUpdate") for the ai-pilot scope:
2380
+ * synchronize Claude task tools with `.claude/apex/task.json` and prompt an
2381
+ * auto-commit on completion when git changes are detected. Ports
2382
+ * `sync-task-tracking.ts`.
2383
+ */
2384
+ /** True when the project has uncommitted git changes. */
2385
+ async function hasGitChanges(cwd) {
2386
+ 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;
2397
+ } catch {
2398
+ return false;
2399
+ }
2400
+ }
2401
+ /** Handle a completed task: emit the commit reminder (or the no-change note). */
2402
+ async function onComplete(taskFile, taskId, projectRoot) {
2403
+ await taskComplete(taskFile, taskId);
2404
+ if (!await hasGitChanges(projectRoot)) return contextResponse("PostToolUse", "Task completed. No changes to commit.");
2405
+ return contextResponse("PostToolUse", `Task #${taskId} completed: ${(await readJsonFile(taskFile))?.tasks[taskId]?.subject ?? "Task"}\n\nChanges detected. MANDATORY: Run /fuse-commit-pro:commit to commit with smart detection.`);
2406
+ }
2407
+ /**
2408
+ * PostToolUse TaskCreate/TaskUpdate handler.
2409
+ * @param payload - The raw hook payload (`tool_name`, `tool_input`, `tool_response`).
2410
+ * @param cwd - Fallback project root (uses `CLAUDE_PROJECT_DIR` first).
2411
+ * @returns The native hook stdout (possibly empty).
2412
+ */
2413
+ async function syncTaskTracking(payload, cwd) {
2414
+ const toolName = String(payload.tool_name ?? "");
2415
+ if (toolName !== "TaskCreate" && toolName !== "TaskUpdate") return "";
2416
+ const projectRoot = process.env.CLAUDE_PROJECT_DIR ?? cwd;
2417
+ const taskFile = join(projectRoot, ".claude", "apex", "task.json");
2418
+ if (!existsSync(taskFile)) return "";
2419
+ const unlock = await acquireLock(join(projectRoot, ".claude", "apex", ".task.lock"), 1e4);
2420
+ if (!unlock) return "";
2421
+ try {
2422
+ const ti = payload.tool_input ?? {};
2423
+ if (toolName === "TaskCreate") {
2424
+ const existing = (await readJsonFile(taskFile))?.tasks ?? {};
2425
+ await taskCreate(taskFile, payload.tool_response?.id ?? String(Math.max(0, ...Object.keys(existing).map(Number)) + 1), ti.subject ?? "", ti.description ?? "");
2426
+ return "";
2427
+ }
2428
+ const taskId = ti.taskId ?? "";
2429
+ if (!taskId) return "";
2430
+ const newStatus = ti.status ?? "";
2431
+ if (newStatus === "in_progress") {
2432
+ const blocked = ti.addBlockedBy?.join(",") ?? "";
2433
+ await taskStart(taskFile, taskId, ti.subject || void 0, ti.description || void 0, blocked || void 0);
2434
+ }
2435
+ if (newStatus === "completed") return onComplete(taskFile, taskId, projectRoot);
2436
+ return "";
2437
+ } finally {
2438
+ await unlock();
2439
+ }
2440
+ }
2441
+ //#endregion
2442
+ //#region src/runtime/lifecycle/aipilot/dispatch-aipilot.ts
2443
+ /**
2444
+ * ai-pilot scope dispatcher: routes Claude lifecycle events to the ported
2445
+ * cache/injection handlers by (event, agent_type matcher).
2446
+ */
2447
+ /** The agent_type a SubagentStart/Stop payload reports. */
2448
+ function agentTypeOf(payload) {
2449
+ return String(payload.agent_type ?? payload.subagent_type ?? "");
2450
+ }
2451
+ /** Extract the `additionalContext` text from a SubagentStart response, or "". */
2452
+ function contextTextOf(response) {
2453
+ if (!response) return "";
2454
+ try {
2455
+ return JSON.parse(response).hookSpecificOutput?.additionalContext ?? "";
2456
+ } catch {
2457
+ return "";
2458
+ }
2459
+ }
2460
+ /**
2461
+ * Combine two SubagentStart responses into one (Claude concatenates multiple
2462
+ * hooks' additionalContext; collapsing two matcher-"" scripts into one dispatch
2463
+ * call must do that join itself). Returns "" when both are empty.
2464
+ */
2465
+ function combineContext(a, b) {
2466
+ const parts = [contextTextOf(a), contextTextOf(b)].filter(Boolean);
2467
+ return parts.length ? contextResponse("SubagentStart", parts.join("\n\n")) : "";
2468
+ }
2469
+ /** The transcript path a SubagentStop payload reports (Stop-only field). */
2470
+ function transcriptOf(payload) {
2471
+ const t = payload.agent_transcript_path;
2472
+ return typeof t === "string" ? t : void 0;
2473
+ }
2474
+ /** SubagentStart routing: cache injectors keyed on agent_type. */
2475
+ async function onSubagentStart(payload, cwd, now) {
2476
+ const agent = agentTypeOf(payload);
2477
+ if (agent.includes("explore-codebase")) return injectExploreCache(cwd, void 0, now);
2478
+ if (agent.includes("research-expert")) return injectDocCache(cwd, void 0, now);
2479
+ if (agent.includes("sniper")) return injectTestCache(cwd, void 0, now);
2480
+ return combineContext(await injectApexSubagentContext(cwd), await injectLessonsCache(cwd, void 0, now));
2481
+ }
2482
+ /** SubagentStop routing: transcript-driven cache writers (side-effects). */
2483
+ async function onSubagentStop(payload, cwd) {
2484
+ const agent = agentTypeOf(payload);
2485
+ const transcript = transcriptOf(payload);
2486
+ if (agent.includes("research-expert")) {
2487
+ await cacheDocFromTranscript(transcript, cwd);
2488
+ return "";
2489
+ }
2490
+ if (agent.includes("sniper")) {
2491
+ await cacheSniperLessons(transcript, cwd);
2492
+ await cacheTestResults(transcript, cwd);
2493
+ }
2494
+ return "";
2495
+ }
2496
+ /**
2497
+ * Dispatch an ai-pilot-scope lifecycle event. Returns the native stdout, or
2498
+ * `null` when unhandled (caller falls through to the default pipeline).
2499
+ */
2500
+ async function dispatchAipilot(event, payload, cwd, now) {
2501
+ if (event === "SubagentStart") return onSubagentStart(payload, cwd, now);
2502
+ if (event === "SubagentStop") return onSubagentStop(payload, cwd);
2503
+ if (event === "SessionEnd") {
2504
+ await cacheAnalyticsSave(void 0, now);
2505
+ return "";
2506
+ }
2507
+ return null;
2508
+ }
2509
+ /** PostToolUse (TaskCreate/TaskUpdate) sync for the ai-pilot scope. */
2510
+ async function aipilotPostToolUse(payload, cwd) {
2511
+ return syncTaskTracking(payload, cwd);
2512
+ }
2513
+ //#endregion
1476
2514
  //#region src/runtime/lifecycle/dispatch.ts
1477
2515
  /** SessionStart handler keyed on plugin scope. */
1478
2516
  function sessionStart(input) {
@@ -1492,15 +2530,15 @@ function dispatchLifecycle(input) {
1492
2530
  switch (input.event) {
1493
2531
  case "SessionStart": return sessionStart(input);
1494
2532
  case "UserPromptSubmit": return input.scope === "rules" ? injectRules(process.env.CLAUDE_PLUGIN_ROOT ?? input.cwd) : null;
1495
- case "SubagentStart": return subagentCacheContext(input.payload.session_id);
1496
- case "SubagentStop": return trackAgentMemory(input.payload, void 0, input.now);
2533
+ case "SubagentStart": return input.scope === "aipilot" ? "" : subagentCacheContext(input.payload.session_id);
2534
+ case "SubagentStop": return input.scope === "aipilot" ? "" : trackAgentMemory(input.payload, void 0, input.now);
1497
2535
  case "TeammateIdle": return validateTeammateOutput(input.payload);
1498
2536
  case "PostToolUseFailure":
1499
2537
  logToolFailure(input.payload, void 0, input.now);
1500
2538
  return "";
1501
2539
  case "PreCompact": return saveApexState(input.cwd, input.now);
1502
2540
  case "SessionEnd":
1503
- cleanupSession(void 0, input.now);
2541
+ if (input.scope !== "aipilot") cleanupSession(void 0, input.now);
1504
2542
  return "";
1505
2543
  case "InstructionsLoaded":
1506
2544
  validateRulesLoaded(input.payload);
@@ -2237,6 +3275,10 @@ async function handlePre(ctx) {
2237
3275
  }
2238
3276
  //#endregion
2239
3277
  //#region src/runtime/handle.ts
3278
+ /** Raw Claude hook event name from a payload (empty when absent). */
3279
+ function rawEventName(payload) {
3280
+ return typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
3281
+ }
2240
3282
  /**
2241
3283
  * The full hook handler: on a PRE event it gates the tool-use (stateless guards
2242
3284
  * then APEX gates from the session track) and returns the native response; on a
@@ -2253,6 +3295,13 @@ async function handleHook(id, payload, opts) {
2253
3295
  stdout: "",
2254
3296
  exit: 0
2255
3297
  };
3298
+ if (opts.scope === "aipilot") {
3299
+ const ai = await dispatchAipilot(rawEventName(payload), payload, opts.cwd, opts.now);
3300
+ if (ai !== null) return {
3301
+ stdout: ai,
3302
+ exit: 0
3303
+ };
3304
+ }
2256
3305
  const life = lifecycleStdout(payload, opts.cwd, opts.scope ?? "core", opts.now);
2257
3306
  if (life !== null) return {
2258
3307
  stdout: life,
@@ -2280,6 +3329,13 @@ async function handleHook(id, payload, opts) {
2280
3329
  });
2281
3330
  if (activity) await recordActivity(file, activity);
2282
3331
  postTrackingSideEffects(opts.scope ?? "core", event, event.input, opts.now);
3332
+ if (opts.scope === "aipilot" && (event.tool === "TaskCreate" || event.tool === "TaskUpdate")) {
3333
+ const out = await aipilotPostToolUse(payload, opts.cwd);
3334
+ if (out) return {
3335
+ stdout: out,
3336
+ exit: 0
3337
+ };
3338
+ }
2283
3339
  const extra = postEditContext(opts.scope ?? "core", event, opts.now);
2284
3340
  return {
2285
3341
  stdout: designWarn ? respond(id, designWarn) : extra,
@@ -2297,4 +3353,4 @@ async function handleHook(id, payload, opts) {
2297
3353
  });
2298
3354
  }
2299
3355
  //#endregion
2300
- export { trimLogFile as $, loadEnriched as A, logToolFailure as B, todayUtc as C, generateProjectMap as D, cartoSessionStart as E, postEditTypescript as F, solidDetectStart as G, trackAgentMemory as H, trackSessionChanges as I, runSessionStartCleanups as J, injectRules as K, validateRulesLoaded as L, countFiles as M, getFileDesc as N, isProject as O, listChildren as P, removeOldFiles as Q, cleanupSession as R, securityStatePath as S, dispatchLifecycle as T, subagentCacheContext as U, validateTeammateOutput as V, detectSolidProfile as W, pruneEmptyDirs as X, sessionStartCore as Y, purgeTtlTree as Z, trackSkillRead as _, isMcpTool as _t, TRIVIAL_BUDGET as a, loadSessionState as at, saveSecurityState as b, queryOf as bt, detectDuplication as c, sessionStatePath as ct, lifecycleStdout as d, taskContext as dt, devContext as et, postEditContext as f, respond as ft, trackMcpResearch as g, MCP_TTL_MS as gt, trackWatchResearch as h, normalizeEvent as ht, REQUIRED_AGENTS as i, fusengineCache as it, mergeLines as j, writeTree as k, dryGate as l, sessionsDir as lt, postTrackingSideEffects as m, trackFile as mt, handlePre as n, projectContext as nt, gate as o, sanitizeSessionId as ot, securityAdvisory as p, recordActivity as pt, readRules as q, DEFAULT_WINDOW_MS as r, claudeHome as rt, preCommitGate as s, saveSessionState as st, handleHook as t, gitContext as tt, extractSymbols as u, promptSubmitContext as ut, isoUtc as v, mcpPostStore as vt, trackEnrichment as w, securityStateDir as x, activityFor as xt, loadSecurityState as y, mcpPreIntercept as yt, saveApexState as z };
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 };
package/dist/index.mjs CHANGED
@@ -4,7 +4,7 @@ import { n as STATE_ROOT, r as projectLayout, t as STATE_GITIGNORE } from "./lay
4
4
  import { t as compactJson } from "./compact-json-DK2nX-MK.mjs";
5
5
  import { n as projectRoot, r as projectRootOrNull, t as isCodeFile } from "./project-root-ff0_poWU.mjs";
6
6
  import { n as detectMode, r as modeFor, t as detectHarness } from "./harness-C8Nxxyn_.mjs";
7
- import { A as solidReadGate, C as capVerbosity, D as docConsultedGate, E as brainstormGate, F as requiredArchSkill, M as detectModularArchitecture, N as detectProjectType, O as evaluateApex, P as isApexCommand, S as MAX_TOKENS, T as APEX_GATES, _ as detectRequiredSkills, a as parseEntry, b as frameworkSolidGate, c as EXCLUDE_DIRS, d as buildApexTaskInjection, f as loadApexTaskState, g as detectClaudeMdProjectType, h as buildClaudeMdContext, i as parseEnrichment, j as DEV_KEYWORDS, k as freshnessGate, l as PROJECT_INDICATORS, m as buildApexInstruction, n as firstComment, o as parseBodyDesc, p as DEV_VERBS, r as firstHeading, s as parseField, t as descFromText, u as buildApexTaskContext, v as skillTriggerGate, w as detectCreationIntent, x as MAX_EXA_RESULTS, y as SKILL_TRIGGERS } from "./describe-CPtgUzFS.mjs";
7
+ import { A as solidReadGate, C as capVerbosity, D as docConsultedGate, E as brainstormGate, F as requiredArchSkill, M as detectModularArchitecture, N as detectProjectType, O as evaluateApex, P as isApexCommand, S as MAX_TOKENS, T as APEX_GATES, _ as detectRequiredSkills, a as parseEntry, b as frameworkSolidGate, c as EXCLUDE_DIRS, d as buildApexTaskInjection, f as loadApexTaskState, g as detectClaudeMdProjectType, h as buildClaudeMdContext, i as parseEnrichment, j as DEV_KEYWORDS, k as freshnessGate, l as PROJECT_INDICATORS, m as buildApexInstruction, n as firstComment, o as parseBodyDesc, p as DEV_VERBS, r as firstHeading, s as parseField, t as descFromText, u as buildApexTaskContext, v as skillTriggerGate, w as detectCreationIntent, x as MAX_EXA_RESULTS, y as SKILL_TRIGGERS } from "./describe-BYqhoV4c.mjs";
8
8
  import { A as evaluateFileSize, C as securityGuard, D as SYSTEM_INSTALL, E as PROJECT_INSTALL, O as matchPatterns, S as CRITICAL_PATTERNS, T as GIT_BLOCKED, _ as CODE_REDIRECT, a as registerGuard, b as protectedPathGuard, c as GO_DECL_RE, d as PY_MODEL_RE, f as SWIFT_PROTO_RE, g as CODE_MUTATORS, h as ASK_WRITERS, i as clearUserGuards, j as detectFramework, k as countLines, l as JAVA_DECL_RE, m as interfaceSeparationGuard, n as FAIL_CLOSED, o as runGuards, p as TS_DECL_RE, r as GUARDS, s as installGuard, t as evaluate, u as PHP_DECL_RE, v as bashWriteGuard, w as GIT_ASK, x as ASK_PATTERNS, y as PROTECTED_FRAGMENTS } from "./evaluate-j3gRJ_ng.mjs";
9
9
  import { i as resolveSessions, n as formatDocSatisfactionStatus, r as isDocConsulted, t as formatDocDeny } from "./doc-helpers-Dd_x1-tZ.mjs";
10
10
  import { i as parseFrontmatter, n as scoreReferences, r as globToRe, t as routeReferences } from "./router-D8cVrI-s.mjs";
@@ -1,4 +1,4 @@
1
- import { A as solidReadGate, C as capVerbosity, D as docConsultedGate, E as brainstormGate, F as requiredArchSkill, M as detectModularArchitecture, N as detectProjectType, O as evaluateApex, P as isApexCommand, S as MAX_TOKENS, T as APEX_GATES, _ as detectRequiredSkills, a as parseEntry, b as frameworkSolidGate, c as EXCLUDE_DIRS, d as buildApexTaskInjection, f as loadApexTaskState, g as detectClaudeMdProjectType, h as buildClaudeMdContext, i as parseEnrichment, j as DEV_KEYWORDS, k as freshnessGate, l as PROJECT_INDICATORS, m as buildApexInstruction, n as firstComment, o as parseBodyDesc, p as DEV_VERBS, r as firstHeading, s as parseField, t as descFromText, u as buildApexTaskContext, v as skillTriggerGate, w as detectCreationIntent, x as MAX_EXA_RESULTS, y as SKILL_TRIGGERS } from "../describe-CPtgUzFS.mjs";
1
+ import { A as solidReadGate, C as capVerbosity, D as docConsultedGate, E as brainstormGate, F as requiredArchSkill, M as detectModularArchitecture, N as detectProjectType, O as evaluateApex, P as isApexCommand, S as MAX_TOKENS, T as APEX_GATES, _ as detectRequiredSkills, a as parseEntry, b as frameworkSolidGate, c as EXCLUDE_DIRS, d as buildApexTaskInjection, f as loadApexTaskState, g as detectClaudeMdProjectType, h as buildClaudeMdContext, i as parseEnrichment, j as DEV_KEYWORDS, k as freshnessGate, l as PROJECT_INDICATORS, m as buildApexInstruction, n as firstComment, o as parseBodyDesc, p as DEV_VERBS, r as firstHeading, s as parseField, t as descFromText, u as buildApexTaskContext, v as skillTriggerGate, w as detectCreationIntent, x as MAX_EXA_RESULTS, y as SKILL_TRIGGERS } from "../describe-BYqhoV4c.mjs";
2
2
  import { A as evaluateFileSize, C as securityGuard, D as SYSTEM_INSTALL, E as PROJECT_INSTALL, O as matchPatterns, S as CRITICAL_PATTERNS, T as GIT_BLOCKED, _ as CODE_REDIRECT, a as registerGuard, b as protectedPathGuard, c as GO_DECL_RE, d as PY_MODEL_RE, f as SWIFT_PROTO_RE, g as CODE_MUTATORS, h as ASK_WRITERS, i as clearUserGuards, j as detectFramework, k as countLines, l as JAVA_DECL_RE, m as interfaceSeparationGuard, n as FAIL_CLOSED, o as runGuards, p as TS_DECL_RE, r as GUARDS, s as installGuard, t as evaluate, u as PHP_DECL_RE, v as bashWriteGuard, w as GIT_ASK, x as ASK_PATTERNS, y as PROTECTED_FRAGMENTS } from "../evaluate-j3gRJ_ng.mjs";
3
3
  import "../policy-la_KkjCS.mjs";
4
4
  export { APEX_GATES, ASK_PATTERNS, ASK_WRITERS, CODE_MUTATORS, CODE_REDIRECT, CRITICAL_PATTERNS, DEV_KEYWORDS, DEV_VERBS, EXCLUDE_DIRS, FAIL_CLOSED, GIT_ASK, GIT_BLOCKED, GO_DECL_RE, GUARDS, JAVA_DECL_RE, MAX_EXA_RESULTS, MAX_TOKENS, PHP_DECL_RE, PROJECT_INDICATORS, PROJECT_INSTALL, PROTECTED_FRAGMENTS, PY_MODEL_RE, SKILL_TRIGGERS, SWIFT_PROTO_RE, SYSTEM_INSTALL, TS_DECL_RE, bashWriteGuard, brainstormGate, buildApexInstruction, buildApexTaskContext, buildApexTaskInjection, buildClaudeMdContext, capVerbosity, clearUserGuards, countLines, descFromText, detectClaudeMdProjectType, detectCreationIntent, detectFramework, detectModularArchitecture, detectProjectType, detectRequiredSkills, docConsultedGate, evaluate, evaluateApex, evaluateFileSize, firstComment, firstHeading, frameworkSolidGate, freshnessGate, installGuard, interfaceSeparationGuard, isApexCommand, loadApexTaskState, matchPatterns, parseBodyDesc, parseEnrichment, parseEntry, parseField, protectedPathGuard, registerGuard, requiredArchSkill, runGuards, securityGuard, skillTriggerGate, solidReadGate };
@@ -355,9 +355,18 @@ declare function trackSessionChanges(sessionIdRaw: unknown, filePath: string, ho
355
355
  */
356
356
  declare function postEditTypescript(filePath: string): string;
357
357
  //#endregion
358
+ //#region src/runtime/lifecycle/aipilot/dispatch-aipilot.d.ts
359
+ /**
360
+ * Dispatch an ai-pilot-scope lifecycle event. Returns the native stdout, or
361
+ * `null` when unhandled (caller falls through to the default pipeline).
362
+ */
363
+ declare function dispatchAipilot(event: string, payload: Record<string, unknown>, cwd: string, now: number): Promise<string | null>;
364
+ /** PostToolUse (TaskCreate/TaskUpdate) sync for the ai-pilot scope. */
365
+ declare function aipilotPostToolUse(payload: Record<string, unknown>, cwd: string): Promise<string>;
366
+ //#endregion
358
367
  //#region src/runtime/lifecycle/dispatch.d.ts
359
368
  /** Which plugin's hooks.json invoked the harness (selects SessionStart behavior). */
360
- type PluginScope = "core" | "solid" | "rules" | "carto" | "security" | "changelog";
369
+ type PluginScope = "core" | "solid" | "rules" | "carto" | "security" | "changelog" | "aipilot";
361
370
  /** Inputs the lifecycle dispatcher needs (clock + roots injected). */
362
371
  interface LifecycleInput {
363
372
  event: string;
@@ -600,4 +609,4 @@ interface PreContext {
600
609
  */
601
610
  declare function handlePre(ctx: PreContext): Promise<HandleOutcome>;
602
611
  //#endregion
603
- export { Activity, DEFAULT_WINDOW_MS, DuplicationVerdict, type GateInput, HandleOptions, HandleOutcome, LifecycleInput, MCP_TTL_MS, McpIntercept, NormalizedEvent, PluginScope, PreContext, REQUIRED_AGENTS, SolidProfile, TRIVIAL_BUDGET, ToolEvent, activityFor, cartoSessionStart, claudeHome, cleanupSession, countFiles, detectDuplication, detectSolidProfile, devContext, dispatchLifecycle, dryGate, extractSymbols, fusengineCache, gate, generateProjectMap, getFileDesc, gitContext, handleHook, handlePre, harnessStateDir, injectRules, isMcpTool, isProject, isoUtc, lifecycleStdout, listChildren, loadEnriched, loadSecurityState, loadSessionState, logToolFailure, mcpPostStore, mcpPreIntercept, mergeLines, normalizeEvent, postEditContext, postEditTypescript, postTrackingSideEffects, preCommitGate, projectContext, promptSubmitContext, pruneEmptyDirs, purgeTtlTree, queryOf, readRules, recordActivity, removeOldFiles, respond, runSessionStartCleanups, sanitizeSessionId, saveApexState, saveSecurityState, saveSessionState, securityAdvisory, securityStateDir, securityStatePath, sessionStartCore, sessionStatePath, sessionsDir, solidDetectStart, subagentCacheContext, taskContext, todayUtc, trackAgentMemory, trackEnrichment, trackFile, trackMcpResearch, trackSessionChanges, trackSkillRead, trackWatchResearch, trimLogFile, validateRulesLoaded, validateTeammateOutput, writeTree };
612
+ export { Activity, DEFAULT_WINDOW_MS, DuplicationVerdict, type GateInput, HandleOptions, HandleOutcome, LifecycleInput, MCP_TTL_MS, McpIntercept, NormalizedEvent, PluginScope, PreContext, REQUIRED_AGENTS, SolidProfile, TRIVIAL_BUDGET, ToolEvent, activityFor, aipilotPostToolUse, cartoSessionStart, claudeHome, cleanupSession, countFiles, detectDuplication, detectSolidProfile, devContext, dispatchAipilot, dispatchLifecycle, dryGate, extractSymbols, fusengineCache, gate, generateProjectMap, getFileDesc, gitContext, handleHook, handlePre, harnessStateDir, injectRules, isMcpTool, isProject, isoUtc, lifecycleStdout, listChildren, loadEnriched, loadSecurityState, loadSessionState, logToolFailure, mcpPostStore, mcpPreIntercept, mergeLines, normalizeEvent, postEditContext, postEditTypescript, postTrackingSideEffects, preCommitGate, projectContext, promptSubmitContext, pruneEmptyDirs, purgeTtlTree, queryOf, readRules, recordActivity, removeOldFiles, respond, runSessionStartCleanups, sanitizeSessionId, saveApexState, saveSecurityState, saveSessionState, securityAdvisory, securityStateDir, securityStatePath, sessionStartCore, sessionStatePath, sessionsDir, solidDetectStart, subagentCacheContext, taskContext, todayUtc, trackAgentMemory, trackEnrichment, trackFile, trackMcpResearch, trackSessionChanges, trackSkillRead, trackWatchResearch, trimLogFile, validateRulesLoaded, validateTeammateOutput, writeTree };
@@ -1,5 +1,5 @@
1
1
  import { r as projectLayout } from "../layout-C0jaaCQC.mjs";
2
- import { $ as trimLogFile, A as loadEnriched, B as logToolFailure, C as todayUtc, D as generateProjectMap, E as cartoSessionStart, F as postEditTypescript, G as solidDetectStart, H as trackAgentMemory, I as trackSessionChanges, J as runSessionStartCleanups, K as injectRules, L as validateRulesLoaded, M as countFiles, N as getFileDesc, O as isProject, P as listChildren, Q as removeOldFiles, R as cleanupSession, S as securityStatePath, T as dispatchLifecycle, U as subagentCacheContext, V as validateTeammateOutput, W as detectSolidProfile, X as pruneEmptyDirs, Y as sessionStartCore, Z as purgeTtlTree, _ as trackSkillRead, _t as isMcpTool, a as TRIVIAL_BUDGET, at as loadSessionState, b as saveSecurityState, bt as queryOf, c as detectDuplication, ct as sessionStatePath, d as lifecycleStdout, dt as taskContext, et as devContext, f as postEditContext, ft as respond, g as trackMcpResearch, gt as MCP_TTL_MS, h as trackWatchResearch, ht as normalizeEvent, i as REQUIRED_AGENTS, it as fusengineCache, j as mergeLines, k as writeTree, l as dryGate, lt as sessionsDir, m as postTrackingSideEffects, mt as trackFile, n as handlePre, nt as projectContext, o as gate, ot as sanitizeSessionId, p as securityAdvisory, pt as recordActivity, q as readRules, r as DEFAULT_WINDOW_MS, rt as claudeHome, s as preCommitGate, st as saveSessionState, t as handleHook, tt as gitContext, u as extractSymbols, ut as promptSubmitContext, v as isoUtc, vt as mcpPostStore, w as trackEnrichment, x as securityStateDir, xt as activityFor, y as loadSecurityState, yt as mcpPreIntercept, z as saveApexState } from "../handle-USWK4NSE.mjs";
2
+ import { $ as purgeTtlTree, A as isProject, B as cleanupSession, C as todayUtc, Ct as activityFor, D as dispatchAipilot, E as aipilotPostToolUse, F as getFileDesc, G as subagentCacheContext, H as logToolFailure, I as listChildren, J as injectRules, K as detectSolidProfile, L as postEditTypescript, M as loadEnriched, N as mergeLines, O as cartoSessionStart, P as countFiles, Q as pruneEmptyDirs, R as trackSessionChanges, S as securityStatePath, St as queryOf, T as dispatchLifecycle, U as validateTeammateOutput, V as saveApexState, W as trackAgentMemory, X as runSessionStartCleanups, Y as readRules, Z as sessionStartCore, _ as trackSkillRead, _t as normalizeEvent, a as TRIVIAL_BUDGET, at as claudeHome, b as saveSecurityState, bt as mcpPostStore, c as detectDuplication, ct as sanitizeSessionId, d as lifecycleStdout, dt as sessionsDir, et as removeOldFiles, f as postEditContext, ft as promptSubmitContext, g as trackMcpResearch, gt as trackFile, h as trackWatchResearch, ht as recordActivity, i as REQUIRED_AGENTS, it as projectContext, j as writeTree, k as generateProjectMap, l as dryGate, lt as saveSessionState, m as postTrackingSideEffects, mt as respond, n as handlePre, nt as devContext, o as gate, ot as fusengineCache, p as securityAdvisory, pt as taskContext, q as solidDetectStart, r as DEFAULT_WINDOW_MS, rt as gitContext, s as preCommitGate, st as loadSessionState, t as handleHook, tt as trimLogFile, u as extractSymbols, ut as sessionStatePath, v as isoUtc, vt as MCP_TTL_MS, w as trackEnrichment, x as securityStateDir, xt as mcpPreIntercept, y as loadSecurityState, yt as isMcpTool, z as validateRulesLoaded } from "../handle-DW9cWdVt.mjs";
3
3
  //#region src/runtime/storage.ts
4
4
  /**
5
5
  * The project's single state dir (`<root>/.harness`) — neutral + harness-agnostic,
@@ -9,4 +9,4 @@ function harnessStateDir(root) {
9
9
  return projectLayout(root).stateDir;
10
10
  }
11
11
  //#endregion
12
- export { DEFAULT_WINDOW_MS, MCP_TTL_MS, REQUIRED_AGENTS, TRIVIAL_BUDGET, activityFor, cartoSessionStart, claudeHome, cleanupSession, countFiles, detectDuplication, detectSolidProfile, devContext, dispatchLifecycle, dryGate, extractSymbols, fusengineCache, gate, generateProjectMap, getFileDesc, gitContext, handleHook, handlePre, harnessStateDir, injectRules, isMcpTool, isProject, isoUtc, lifecycleStdout, listChildren, loadEnriched, loadSecurityState, loadSessionState, logToolFailure, mcpPostStore, mcpPreIntercept, mergeLines, normalizeEvent, postEditContext, postEditTypescript, postTrackingSideEffects, preCommitGate, projectContext, promptSubmitContext, pruneEmptyDirs, purgeTtlTree, queryOf, readRules, recordActivity, removeOldFiles, respond, runSessionStartCleanups, sanitizeSessionId, saveApexState, saveSecurityState, saveSessionState, securityAdvisory, securityStateDir, securityStatePath, sessionStartCore, sessionStatePath, sessionsDir, solidDetectStart, subagentCacheContext, taskContext, todayUtc, trackAgentMemory, trackEnrichment, trackFile, trackMcpResearch, trackSessionChanges, trackSkillRead, trackWatchResearch, trimLogFile, validateRulesLoaded, validateTeammateOutput, writeTree };
12
+ export { DEFAULT_WINDOW_MS, MCP_TTL_MS, REQUIRED_AGENTS, TRIVIAL_BUDGET, activityFor, aipilotPostToolUse, cartoSessionStart, claudeHome, cleanupSession, countFiles, detectDuplication, detectSolidProfile, devContext, dispatchAipilot, dispatchLifecycle, dryGate, extractSymbols, fusengineCache, gate, generateProjectMap, getFileDesc, gitContext, handleHook, handlePre, harnessStateDir, injectRules, isMcpTool, isProject, isoUtc, lifecycleStdout, listChildren, loadEnriched, loadSecurityState, loadSessionState, logToolFailure, mcpPostStore, mcpPreIntercept, mergeLines, normalizeEvent, postEditContext, postEditTypescript, postTrackingSideEffects, preCommitGate, projectContext, promptSubmitContext, pruneEmptyDirs, purgeTtlTree, queryOf, readRules, recordActivity, removeOldFiles, respond, runSessionStartCleanups, sanitizeSessionId, saveApexState, saveSecurityState, saveSessionState, securityAdvisory, securityStateDir, securityStatePath, sessionStartCore, sessionStatePath, sessionsDir, solidDetectStart, subagentCacheContext, taskContext, todayUtc, trackAgentMemory, trackEnrichment, trackFile, trackMcpResearch, trackSessionChanges, trackSkillRead, trackWatchResearch, trimLogFile, validateRulesLoaded, validateTeammateOutput, writeTree };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusengine/harness",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
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",