@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 +3 -2
- package/dist/{describe-CPtgUzFS.mjs → describe-BYqhoV4c.mjs} +3 -4
- package/dist/{handle-USWK4NSE.mjs → handle-DW9cWdVt.mjs} +1063 -7
- package/dist/index.mjs +1 -1
- package/dist/policy/index.mjs +1 -1
- package/dist/runtime/index.d.mts +11 -2
- package/dist/runtime/index.mjs +2 -2
- package/package.json +1 -1
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-
|
|
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 +
|
|
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-
|
|
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 {
|
|
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-
|
|
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";
|
package/dist/policy/index.mjs
CHANGED
|
@@ -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-
|
|
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 };
|
package/dist/runtime/index.d.mts
CHANGED
|
@@ -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 };
|
package/dist/runtime/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as projectLayout } from "../layout-C0jaaCQC.mjs";
|
|
2
|
-
import { $ as
|
|
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.
|
|
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",
|