@fusengine/harness 0.1.32 → 0.1.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude/index.mjs +1 -1
- package/dist/adapters/codex/index.mjs +1 -1
- package/dist/claude-BatVYnAf.mjs +137 -0
- package/dist/cli/bin.mjs +2 -2
- package/dist/{handle-DJqHZcml.mjs → handle-Cxgzd4pZ.mjs} +48 -63
- package/dist/init/index.mjs +1 -1
- package/dist/{run-D91N4ul1.mjs → run-Do2JltgU.mjs} +2 -2
- package/dist/runtime/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/claude-BWZcrZbS.mjs +0 -66
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as readClaudeInput, i as guard, n as denyResponse, o as toClaudeResponse, r as fileSizeGuard, t as contextResponse } from "../../claude-
|
|
1
|
+
import { a as readClaudeInput, i as guard, n as denyResponse, o as toClaudeResponse, r as fileSizeGuard, t as contextResponse } from "../../claude-BatVYnAf.mjs";
|
|
2
2
|
export { contextResponse, denyResponse, fileSizeGuard, guard, readClaudeInput, toClaudeResponse };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as readClaudeInput, i as guard, n as denyResponse, t as contextResponse } from "../../claude-
|
|
1
|
+
import { a as readClaudeInput, i as guard, n as denyResponse, t as contextResponse } from "../../claude-BatVYnAf.mjs";
|
|
2
2
|
export { contextResponse, denyResponse, guard, readClaudeInput as readCodexInput };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { t as evaluate } from "./evaluate-9ch1K2kt.mjs";
|
|
2
|
+
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
//#region src/util/runtime-io.ts
|
|
7
|
+
/**
|
|
8
|
+
* Cross-runtime I/O helpers (Node + Bun). The published CLI ships a `node`
|
|
9
|
+
* shebang and is invoked via `npx`/`npm`/`bunx`/`bun`; using only `node:*` APIs
|
|
10
|
+
* here keeps the bundle runnable under EVERY runtime — replacing the Bun-only
|
|
11
|
+
* `Bun.file/write/spawn/sleep/stdin` calls and the `bun` `Glob` import that made
|
|
12
|
+
* the package crash with `Cannot find package 'bun'` under plain Node.
|
|
13
|
+
*/
|
|
14
|
+
/** Read a file as UTF-8 text (throws on missing/unreadable — callers catch). */
|
|
15
|
+
function readText(path) {
|
|
16
|
+
return readFileSync(path, "utf8");
|
|
17
|
+
}
|
|
18
|
+
/** True when `path` exists. */
|
|
19
|
+
function pathExists(path) {
|
|
20
|
+
return existsSync(path);
|
|
21
|
+
}
|
|
22
|
+
/** Write `data` to `path`, creating parent dirs (mirrors `Bun.write`). */
|
|
23
|
+
function writeText(path, data) {
|
|
24
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
25
|
+
writeFileSync(path, data, { encoding: "utf8" });
|
|
26
|
+
}
|
|
27
|
+
/** Resolve after `ms` milliseconds (`Bun.sleep` replacement). */
|
|
28
|
+
function sleep(ms) {
|
|
29
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30
|
+
}
|
|
31
|
+
/** Run `cmd args` in `cwd` and capture stdout text ("" on failure/non-zero). */
|
|
32
|
+
function spawnCapture(cmd, args, cwd) {
|
|
33
|
+
try {
|
|
34
|
+
const r = spawnSync(cmd, args, {
|
|
35
|
+
cwd,
|
|
36
|
+
encoding: "utf8"
|
|
37
|
+
});
|
|
38
|
+
return r.status === 0 ? r.stdout ?? "" : "";
|
|
39
|
+
} catch {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Read the full process stdin as UTF-8 text (works under Node and Bun). */
|
|
44
|
+
async function readStdin() {
|
|
45
|
+
const chunks = [];
|
|
46
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
47
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Recursively collect files under `dir` whose extension is in `exts`, skipping
|
|
51
|
+
* `node_modules` and dot-dirs, capped at `cap`. `Bun.Glob().scan()` replacement.
|
|
52
|
+
* @param dir - Directory to walk.
|
|
53
|
+
* @param exts - Allowed extensions including the dot (e.g. `.ts`).
|
|
54
|
+
* @param out - Accumulator (mutated in place).
|
|
55
|
+
* @param cap - Max files to collect.
|
|
56
|
+
*/
|
|
57
|
+
function collectFiles(dir, exts, out, cap) {
|
|
58
|
+
if (out.length >= cap) return;
|
|
59
|
+
let entries;
|
|
60
|
+
try {
|
|
61
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
62
|
+
} catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const e of entries) {
|
|
66
|
+
if (out.length >= cap) return;
|
|
67
|
+
if (e.name === "node_modules" || e.name.startsWith(".")) continue;
|
|
68
|
+
const full = join(dir, e.name);
|
|
69
|
+
if (e.isDirectory()) collectFiles(full, exts, out, cap);
|
|
70
|
+
else if (exts.has(e.name.slice(e.name.lastIndexOf(".")))) out.push(full);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/adapters/claude/index.ts
|
|
75
|
+
/**
|
|
76
|
+
* Claude Code adapter — the thin Claude-only shim over the portable policy core.
|
|
77
|
+
* Reads the hook stdin payload and emits hookSpecificOutput responses.
|
|
78
|
+
*/
|
|
79
|
+
/** Read & parse the Claude hook payload from stdin (empty object on bad input). */
|
|
80
|
+
async function readClaudeInput() {
|
|
81
|
+
const text = await readStdin();
|
|
82
|
+
if (!text.trim()) return {};
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(text);
|
|
85
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
86
|
+
} catch {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** A `deny` hook response for a given event. */
|
|
91
|
+
function denyResponse(event, reason) {
|
|
92
|
+
return JSON.stringify({ hookSpecificOutput: {
|
|
93
|
+
hookEventName: event,
|
|
94
|
+
permissionDecision: "deny",
|
|
95
|
+
permissionDecisionReason: reason
|
|
96
|
+
} });
|
|
97
|
+
}
|
|
98
|
+
/** An `additionalContext` injection response. */
|
|
99
|
+
function contextResponse(event, text) {
|
|
100
|
+
return JSON.stringify({ hookSpecificOutput: {
|
|
101
|
+
hookEventName: event,
|
|
102
|
+
additionalContext: text
|
|
103
|
+
} });
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Render a portable {@link Prompt} as a Claude Code hook response:
|
|
107
|
+
* `block` → `permissionDecision: deny`, `ask` → `permissionDecision: ask`
|
|
108
|
+
* (interactive confirm), `inform` → `additionalContext`.
|
|
109
|
+
*/
|
|
110
|
+
function toClaudeResponse(event, prompt) {
|
|
111
|
+
const reason = formatPrompt(prompt);
|
|
112
|
+
if (prompt.kind === "block") return denyResponse(event, reason);
|
|
113
|
+
if (prompt.kind === "ask") return JSON.stringify({ hookSpecificOutput: {
|
|
114
|
+
hookEventName: event,
|
|
115
|
+
permissionDecision: "ask",
|
|
116
|
+
permissionDecisionReason: reason
|
|
117
|
+
} });
|
|
118
|
+
return contextResponse(event, reason);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Run the bundled policy over a Claude payload and return the native response
|
|
122
|
+
* string (deny/ask/additionalContext), or null to allow.
|
|
123
|
+
*/
|
|
124
|
+
function guard(input) {
|
|
125
|
+
const result = evaluate({
|
|
126
|
+
tool: input.tool_name ?? "Write",
|
|
127
|
+
filePath: input.tool_input?.file_path,
|
|
128
|
+
content: input.tool_input?.content ?? input.tool_input?.new_string,
|
|
129
|
+
command: input.tool_input?.command
|
|
130
|
+
});
|
|
131
|
+
if (result.decision === "allow" || !result.prompt) return null;
|
|
132
|
+
return toClaudeResponse(input.hook_event_name ?? "PreToolUse", result.prompt);
|
|
133
|
+
}
|
|
134
|
+
/** @deprecated use {@link guard}. Kept for back-compat. */
|
|
135
|
+
const fileSizeGuard = guard;
|
|
136
|
+
//#endregion
|
|
137
|
+
export { readClaudeInput as a, pathExists as c, spawnCapture as d, writeText as f, guard as i, readText as l, denyResponse as n, toClaudeResponse as o, fileSizeGuard as r, collectFiles as s, contextResponse as t, sleep as u };
|
package/dist/cli/bin.mjs
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
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-CQbtlAKa.mjs";
|
|
5
|
-
import { n as writeInitFile, t as initFor } from "../run-
|
|
6
|
-
import { t as handleHook } from "../handle-
|
|
5
|
+
import { n as writeInitFile, t as initFor } from "../run-Do2JltgU.mjs";
|
|
6
|
+
import { t as handleHook } from "../handle-Cxgzd4pZ.mjs";
|
|
7
7
|
//#region src/cli/bin.ts
|
|
8
8
|
/**
|
|
9
9
|
* harness — CLI for @fusengine/harness.
|
|
@@ -9,14 +9,13 @@ import { a as extractText, o as loadIndex, r as cacheStore, t as cacheLookup } f
|
|
|
9
9
|
import { i as writeJsonFile, r as readJsonFile, t as atomicWrite } from "./json-io-CAn72gI4.mjs";
|
|
10
10
|
import { t as loadRefs } from "./loader-CyAoJv2W.mjs";
|
|
11
11
|
import { a as recordAgent, c as recordRefRead, l as recordTrivialEdit, n as saveTrack, o as recordBrainstormRequired, r as agentsFresh, s as recordDoc, t as loadTrack, u as trivialCount } from "./store-D-ge2ZPI.mjs";
|
|
12
|
-
import { n as denyResponse, t as contextResponse } from "./claude-
|
|
12
|
+
import { c as pathExists, d as spawnCapture, f as writeText, l as readText, n as denyResponse, s as collectFiles, t as contextResponse, u as sleep } from "./claude-BatVYnAf.mjs";
|
|
13
13
|
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
14
14
|
import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
15
15
|
import { homedir, tmpdir } from "node:os";
|
|
16
16
|
import { createHash } from "node:crypto";
|
|
17
17
|
import { mkdir, rmdir } from "node:fs/promises";
|
|
18
18
|
import { execFileSync } from "node:child_process";
|
|
19
|
-
import { Glob } from "bun";
|
|
20
19
|
//#region src/runtime/activity.ts
|
|
21
20
|
/** Min response length (chars) for a lead agent call to count as `sufficient`. */
|
|
22
21
|
const AGENT_QUALITY_MIN = 500;
|
|
@@ -1910,8 +1909,8 @@ function cartographerContext() {
|
|
|
1910
1909
|
async function injectApexSubagentContext(cwd, home = homedir()) {
|
|
1911
1910
|
const apexDir = join(process.env.CLAUDE_PROJECT_DIR ?? cwd, ".claude", "apex");
|
|
1912
1911
|
if (!existsSync(apexDir)) return "";
|
|
1913
|
-
const
|
|
1914
|
-
const agents =
|
|
1912
|
+
const agentsPath = join(apexDir, "AGENTS.md");
|
|
1913
|
+
const agents = existsSync(agentsPath) ? readText(agentsPath).slice(0, 4e3) : "";
|
|
1915
1914
|
const taskData = await readJsonFile(join(apexDir, "task.json"));
|
|
1916
1915
|
return contextResponse("SubagentStart", `## APEX Sub-Agent Instructions
|
|
1917
1916
|
|
|
@@ -1967,7 +1966,7 @@ function cacheAge(ts, now = Date.now()) {
|
|
|
1967
1966
|
/** Full SHA-256 hex checksum of a file's text; "" when unreadable. */
|
|
1968
1967
|
async function fileChecksum(path) {
|
|
1969
1968
|
try {
|
|
1970
|
-
return createHash("sha256").update(
|
|
1969
|
+
return createHash("sha256").update(readText(path)).digest("hex");
|
|
1971
1970
|
} catch {
|
|
1972
1971
|
return "";
|
|
1973
1972
|
}
|
|
@@ -2036,9 +2035,8 @@ function parseEntries(raw) {
|
|
|
2036
2035
|
async function cacheAnalyticsSave(home = homedir(), now = Date.now()) {
|
|
2037
2036
|
const dir = join(cacheBaseDir(home), "analytics");
|
|
2038
2037
|
const sessionsFile = join(dir, "sessions.jsonl");
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
const raw = await file.text();
|
|
2038
|
+
if (!pathExists(sessionsFile)) return;
|
|
2039
|
+
const raw = readText(sessionsFile);
|
|
2042
2040
|
if (!raw.trim()) return;
|
|
2043
2041
|
const entries = parseEntries(raw);
|
|
2044
2042
|
if (entries.length === 0) return;
|
|
@@ -2066,8 +2064,7 @@ async function cacheAnalyticsSave(home = homedir(), now = Date.now()) {
|
|
|
2066
2064
|
}
|
|
2067
2065
|
await writeJsonFile(join(dir, "summary.json"), merged, true);
|
|
2068
2066
|
const cutoff = (/* @__PURE__ */ new Date(now - 30 * 864e5)).toISOString();
|
|
2069
|
-
|
|
2070
|
-
await Bun.write(sessionsFile, kept.map((e) => JSON.stringify(e)).join("\n") + "\n");
|
|
2067
|
+
writeText(sessionsFile, entries.filter((e) => e.ts >= cutoff).map((e) => JSON.stringify(e)).join("\n") + "\n");
|
|
2071
2068
|
}
|
|
2072
2069
|
//#endregion
|
|
2073
2070
|
//#region src/runtime/lifecycle/aipilot/inject-explore.ts
|
|
@@ -2091,17 +2088,11 @@ const CONFIG_FILES = [
|
|
|
2091
2088
|
/** Compute a config hash from git-tracked config files; "noconfig" on failure. */
|
|
2092
2089
|
async function configHash(cwd) {
|
|
2093
2090
|
try {
|
|
2094
|
-
const
|
|
2095
|
-
"git",
|
|
2091
|
+
const output = spawnCapture("git", [
|
|
2096
2092
|
"ls-tree",
|
|
2097
2093
|
"HEAD",
|
|
2098
2094
|
...CONFIG_FILES
|
|
2099
|
-
],
|
|
2100
|
-
cwd,
|
|
2101
|
-
stdout: "pipe",
|
|
2102
|
-
stderr: "ignore"
|
|
2103
|
-
});
|
|
2104
|
-
const output = await new Response(proc.stdout).text();
|
|
2095
|
+
], cwd);
|
|
2105
2096
|
return output.trim() ? hashText16(output) : "noconfig";
|
|
2106
2097
|
} catch {
|
|
2107
2098
|
return "noconfig";
|
|
@@ -2127,8 +2118,7 @@ async function injectExploreCache(cwd, home = homedir(), now = Date.now()) {
|
|
|
2127
2118
|
const cfgHash = await configHash(projPath);
|
|
2128
2119
|
let context = "";
|
|
2129
2120
|
const meta = await readJsonFile(metaFile);
|
|
2130
|
-
const
|
|
2131
|
-
const snapshot = await snapBunFile.exists() ? await snapBunFile.text() : "";
|
|
2121
|
+
const snapshot = pathExists(snapFile) ? readText(snapFile) : "";
|
|
2132
2122
|
if (meta?.timestamp && snapshot) {
|
|
2133
2123
|
const age = cacheAge(meta.timestamp, now);
|
|
2134
2124
|
if (age < TTL_SECONDS$2 && meta.config_hash === cfgHash) {
|
|
@@ -2164,9 +2154,9 @@ async function buildDocsContext(entries, docsDir, now) {
|
|
|
2164
2154
|
if (age > maxAge) maxAge = age;
|
|
2165
2155
|
if (!entry.hash || seen.has(entry.hash)) continue;
|
|
2166
2156
|
seen.add(entry.hash);
|
|
2167
|
-
const
|
|
2168
|
-
if (!
|
|
2169
|
-
const content =
|
|
2157
|
+
const docPath = join(docsDir, `${entry.hash}.md`);
|
|
2158
|
+
if (!pathExists(docPath)) continue;
|
|
2159
|
+
const content = readText(docPath);
|
|
2170
2160
|
if (!content) continue;
|
|
2171
2161
|
ctx += `\n${content}\n`;
|
|
2172
2162
|
count++;
|
|
@@ -2202,32 +2192,36 @@ async function injectDocCache(cwd, home = homedir(), now = Date.now()) {
|
|
|
2202
2192
|
* Ported from the ai-pilot plugin's `cache/source-collector.ts` +
|
|
2203
2193
|
* the stack detection in `cache/lesson-helpers.ts` (now removed).
|
|
2204
2194
|
*/
|
|
2205
|
-
/** Source
|
|
2206
|
-
const
|
|
2207
|
-
"
|
|
2208
|
-
"
|
|
2209
|
-
"
|
|
2210
|
-
"
|
|
2211
|
-
];
|
|
2195
|
+
/** Source extensions to collect (monorepo-aware, dot-prefixed for matching). */
|
|
2196
|
+
const SRC_EXTS = /* @__PURE__ */ new Set([
|
|
2197
|
+
".ts",
|
|
2198
|
+
".tsx",
|
|
2199
|
+
".js",
|
|
2200
|
+
".jsx"
|
|
2201
|
+
]);
|
|
2202
|
+
/** Roots walked: `src`, `app`, plus each child `src` under `apps/` and `packages/`. */
|
|
2203
|
+
const TOP_DIRS = ["src", "app"];
|
|
2204
|
+
const NESTED_PARENTS = ["apps", "packages"];
|
|
2205
|
+
/** Collect the existing monorepo `src` roots nested under `apps/` and `packages/`. */
|
|
2206
|
+
function nestedRoots(projectPath) {
|
|
2207
|
+
const roots = [];
|
|
2208
|
+
for (const parent of NESTED_PARENTS) try {
|
|
2209
|
+
for (const e of readdirSync(join(projectPath, parent), { withFileTypes: true })) if (e.isDirectory()) roots.push(join(projectPath, parent, e.name, "src"));
|
|
2210
|
+
} catch {}
|
|
2211
|
+
return roots;
|
|
2212
|
+
}
|
|
2212
2213
|
/**
|
|
2213
2214
|
* Scan source files in `projectPath` (monorepo-aware), capped at `maxFiles`.
|
|
2215
|
+
* Node+Bun portable: walks `node:fs` recursively (replaces the Bun `Glob`).
|
|
2214
2216
|
* @param projectPath - Absolute project root.
|
|
2215
2217
|
* @param maxFiles - Max files to collect (default 200).
|
|
2216
|
-
* @returns Absolute paths matching the source
|
|
2218
|
+
* @returns Absolute paths matching the source extensions.
|
|
2217
2219
|
*/
|
|
2218
2220
|
async function scanSourceFiles(projectPath, maxFiles = 200) {
|
|
2219
2221
|
const files = [];
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
cwd: projectPath,
|
|
2224
|
-
absolute: true
|
|
2225
|
-
})) {
|
|
2226
|
-
if (p.includes("node_modules")) continue;
|
|
2227
|
-
files.push(p);
|
|
2228
|
-
if (files.length >= maxFiles) break;
|
|
2229
|
-
}
|
|
2230
|
-
} catch {}
|
|
2222
|
+
const roots = [...TOP_DIRS.map((d) => join(projectPath, d)), ...nestedRoots(projectPath)];
|
|
2223
|
+
for (const root of roots) {
|
|
2224
|
+
collectFiles(root, SRC_EXTS, files, maxFiles);
|
|
2231
2225
|
if (files.length >= maxFiles) break;
|
|
2232
2226
|
}
|
|
2233
2227
|
return files;
|
|
@@ -2455,7 +2449,7 @@ function projectRootFromPaths(filePaths) {
|
|
|
2455
2449
|
}
|
|
2456
2450
|
/** Extract all absolute file paths from tool_use entries in a JSONL transcript. */
|
|
2457
2451
|
async function transcriptFilePaths(transcriptPath) {
|
|
2458
|
-
const text =
|
|
2452
|
+
const text = readText(transcriptPath);
|
|
2459
2453
|
const paths = /* @__PURE__ */ new Set();
|
|
2460
2454
|
for (const line of text.split("\n").filter(Boolean)) try {
|
|
2461
2455
|
const content = JSON.parse(line)?.message?.content;
|
|
@@ -2470,7 +2464,7 @@ async function transcriptFilePaths(transcriptPath) {
|
|
|
2470
2464
|
}
|
|
2471
2465
|
/** Extract deduplicated Edit tool_use entries (keyed by basename) from a transcript. */
|
|
2472
2466
|
async function transcriptEdits(transcriptPath) {
|
|
2473
|
-
const text =
|
|
2467
|
+
const text = readText(transcriptPath);
|
|
2474
2468
|
const edits = [];
|
|
2475
2469
|
for (const line of text.split("\n").filter(Boolean)) try {
|
|
2476
2470
|
const content = JSON.parse(line)?.message?.content;
|
|
@@ -2487,7 +2481,7 @@ async function transcriptEdits(transcriptPath) {
|
|
|
2487
2481
|
}
|
|
2488
2482
|
/** Extract the last assistant text report (first 500 lines) from a transcript. */
|
|
2489
2483
|
async function transcriptReport(transcriptPath) {
|
|
2490
|
-
const text =
|
|
2484
|
+
const text = readText(transcriptPath);
|
|
2491
2485
|
let lastReport = "";
|
|
2492
2486
|
for (const line of text.split("\n").filter(Boolean)) try {
|
|
2493
2487
|
const entry = JSON.parse(line);
|
|
@@ -2515,7 +2509,7 @@ const RETRY_DELAYS = [
|
|
|
2515
2509
|
];
|
|
2516
2510
|
/** Extract the longest assistant synthesis + queried library ids from a transcript. */
|
|
2517
2511
|
async function extractSynthesis(path) {
|
|
2518
|
-
const lines = (
|
|
2512
|
+
const lines = readText(path).split("\n").filter(Boolean);
|
|
2519
2513
|
const libraries = [];
|
|
2520
2514
|
let synthesis = "";
|
|
2521
2515
|
for (const line of lines) try {
|
|
@@ -2543,14 +2537,14 @@ async function extractSynthesis(path) {
|
|
|
2543
2537
|
* @param home - Home dir (defaults to `~`).
|
|
2544
2538
|
*/
|
|
2545
2539
|
async function cacheDocFromTranscript(transcript, cwd, home = homedir()) {
|
|
2546
|
-
if (!transcript || !
|
|
2540
|
+
if (!transcript || !pathExists(transcript)) return;
|
|
2547
2541
|
const projPath = projectRootFromPaths(await transcriptFilePaths(transcript)) ?? process.env.CLAUDE_PROJECT_DIR ?? cwd;
|
|
2548
2542
|
const cacheDir = cacheDirFor("doc", projPath, home);
|
|
2549
2543
|
const docsDir = join(cacheDir, "docs");
|
|
2550
2544
|
let result = await extractSynthesis(transcript);
|
|
2551
2545
|
for (const delay of RETRY_DELAYS) {
|
|
2552
2546
|
if (result.text.length >= MIN_TEXT_SIZE && result.libraries.length > 0) break;
|
|
2553
|
-
await
|
|
2547
|
+
await sleep(delay);
|
|
2554
2548
|
result = await extractSynthesis(transcript);
|
|
2555
2549
|
}
|
|
2556
2550
|
const { text, libraries } = result;
|
|
@@ -2564,7 +2558,7 @@ async function cacheDocFromTranscript(transcript, cwd, home = homedir()) {
|
|
|
2564
2558
|
const content = text.slice(0, MAX_DOC_SIZE);
|
|
2565
2559
|
const topic = libraries.join(", ");
|
|
2566
2560
|
const docHash = hashText16(topic);
|
|
2567
|
-
|
|
2561
|
+
writeText(join(docsDir, `${docHash}.md`), content);
|
|
2568
2562
|
const sizeKb = Math.floor(content.length / 1024);
|
|
2569
2563
|
for (const lib of libraries) {
|
|
2570
2564
|
index.docs = index.docs.filter((d) => d.library !== lib);
|
|
@@ -2594,7 +2588,7 @@ async function cacheDocFromTranscript(transcript, cwd, home = homedir()) {
|
|
|
2594
2588
|
* @param home - Home dir (defaults to `~`).
|
|
2595
2589
|
*/
|
|
2596
2590
|
async function cacheSniperLessons(transcript, cwd, home = homedir()) {
|
|
2597
|
-
if (!transcript || !
|
|
2591
|
+
if (!transcript || !pathExists(transcript)) return;
|
|
2598
2592
|
const edits = await transcriptEdits(transcript);
|
|
2599
2593
|
if (edits.length === 0) return;
|
|
2600
2594
|
const projectPath = projectRootFromPaths(edits.map((e) => e.file)) ?? process.env.CLAUDE_PROJECT_DIR ?? cwd;
|
|
@@ -2633,7 +2627,7 @@ async function cacheSniperLessons(transcript, cwd, home = homedir()) {
|
|
|
2633
2627
|
*/
|
|
2634
2628
|
/** Extract linter-related command/output text from a JSONL transcript. */
|
|
2635
2629
|
async function extractLinterOutput(path) {
|
|
2636
|
-
const text =
|
|
2630
|
+
const text = readText(path);
|
|
2637
2631
|
const outputs = [];
|
|
2638
2632
|
for (const line of text.split("\n").filter(Boolean)) try {
|
|
2639
2633
|
const content = JSON.parse(line)?.message?.content;
|
|
@@ -2655,7 +2649,7 @@ async function extractLinterOutput(path) {
|
|
|
2655
2649
|
* @param home - Home dir (defaults to `~`).
|
|
2656
2650
|
*/
|
|
2657
2651
|
async function cacheTestResults(transcript, cwd, home = homedir()) {
|
|
2658
|
-
if (!transcript || !
|
|
2652
|
+
if (!transcript || !pathExists(transcript)) return;
|
|
2659
2653
|
const projectPath = projectRootFromPaths(await transcriptFilePaths(transcript)) ?? process.env.CLAUDE_PROJECT_DIR ?? cwd;
|
|
2660
2654
|
const pHash = projectHash(projectPath);
|
|
2661
2655
|
const cacheDir = cacheDirFor("tests", projectPath, home);
|
|
@@ -2715,7 +2709,7 @@ async function acquireLock(lockDir, timeoutMs = 5e3) {
|
|
|
2715
2709
|
} catch {}
|
|
2716
2710
|
};
|
|
2717
2711
|
} catch {
|
|
2718
|
-
await
|
|
2712
|
+
await sleep(100);
|
|
2719
2713
|
}
|
|
2720
2714
|
return null;
|
|
2721
2715
|
}
|
|
@@ -2781,16 +2775,7 @@ async function taskComplete(file, id) {
|
|
|
2781
2775
|
/** True when the project has uncommitted git changes. */
|
|
2782
2776
|
async function hasGitChanges(cwd) {
|
|
2783
2777
|
try {
|
|
2784
|
-
|
|
2785
|
-
"git",
|
|
2786
|
-
"status",
|
|
2787
|
-
"--porcelain"
|
|
2788
|
-
], {
|
|
2789
|
-
cwd,
|
|
2790
|
-
stdout: "pipe",
|
|
2791
|
-
stderr: "ignore"
|
|
2792
|
-
});
|
|
2793
|
-
return (await new Response(proc.stdout).text()).trim().length > 0;
|
|
2778
|
+
return spawnCapture("git", ["status", "--porcelain"], cwd).trim().length > 0;
|
|
2794
2779
|
} catch {
|
|
2795
2780
|
return false;
|
|
2796
2781
|
}
|
package/dist/init/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as codexInit, i as clineInit, n as writeInitFile, o as cursorInit, r as claudeInit, s as geminiInit, t as initFor } from "../run-
|
|
1
|
+
import { a as codexInit, i as clineInit, n as writeInitFile, o as cursorInit, r as claudeInit, s as geminiInit, t as initFor } from "../run-Do2JltgU.mjs";
|
|
2
2
|
export { claudeInit, clineInit, codexInit, cursorInit, geminiInit, initFor, writeInitFile };
|
|
@@ -11,7 +11,7 @@ function claudeInit(command) {
|
|
|
11
11
|
path: ".claude/settings.json",
|
|
12
12
|
content: json({ hooks: {
|
|
13
13
|
PreToolUse: [{
|
|
14
|
-
matcher: "Write|Edit",
|
|
14
|
+
matcher: "Write|Edit|Bash",
|
|
15
15
|
hooks: [{
|
|
16
16
|
type: "command",
|
|
17
17
|
command
|
|
@@ -113,7 +113,7 @@ const RUNNERS = {
|
|
|
113
113
|
* Build the wiring file(s) for a harness, or null when it has no hook
|
|
114
114
|
* integration (cli-mode harnesses use `harness check` in a pre-commit step).
|
|
115
115
|
*/
|
|
116
|
-
function initFor(id, command = `npx harness hook ${id}`) {
|
|
116
|
+
function initFor(id, command = `npx -y @fusengine/harness hook ${id}`) {
|
|
117
117
|
const make = RUNNERS[id];
|
|
118
118
|
if (!make) return null;
|
|
119
119
|
return [...make(command), {
|
package/dist/runtime/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as projectLayout } from "../layout-C0jaaCQC.mjs";
|
|
2
|
-
import { $ as detectSolidProfile, A as dispatchLessons, At as activityFor, B as mergeLines, C as securityStateDir, Ct as trackFile, D as dispatchLifecycle, Dt as mcpPostStore, E as trackEnrichment, Et as isMcpTool, F as writePluginMap, G as trackSessionChanges, H as getFileDesc, I as generateProjectMap, J as saveApexState, K as validateRulesLoaded, L as isProject, M as lessonsStateFileFor, N as cartoSessionStart, O as aipilotPostToolUse, Ot as mcpPreIntercept, P as generateEcosystemMap, Q as subagentCacheContext, R as writeTree, S as saveSecurityState, St as recordActivity, T as todayUtc, Tt as MCP_TTL_MS, U as listChildren, V as countFiles, W as postEditTypescript, X as validateTeammateOutput, Y as logToolFailure, Z as trackAgentMemory, _ as trackWatchResearch, _t as sessionStatePath, a as TRIVIAL_BUDGET, at as pruneEmptyDirs, b as isoUtc, bt as taskContext, c as detectDuplication, ct as trimLogFile, d as lifecycleStdout, dt as projectContext, et as solidDetectStart, f as postEditContext, ft as claudeHome, g as postTrackingSideEffects, gt as saveSessionState, h as securityAdvisory, ht as sanitizeSessionId, i as REQUIRED_AGENTS, it as sessionStartCore, j as lessonsFileFor, k as dispatchAipilot, kt as queryOf, l as dryGate, lt as devContext, m as seoPostToolUseResponse, mt as loadSessionState, n as handlePre, nt as readRules, o as gate, ot as purgeTtlTree, p as seoPostToolUse, pt as fusengineCache, q as cleanupSession, r as DEFAULT_WINDOW_MS, rt as runSessionStartCleanups, s as preCommitGate, st as removeOldFiles, t as handleHook, tt as injectRules, u as extractSymbols, ut as gitContext, v as trackMcpResearch, vt as sessionsDir, w as securityStatePath, wt as normalizeEvent, x as loadSecurityState, xt as respond, y as trackSkillRead, yt as promptSubmitContext, z as loadEnriched } from "../handle-
|
|
2
|
+
import { $ as detectSolidProfile, A as dispatchLessons, At as activityFor, B as mergeLines, C as securityStateDir, Ct as trackFile, D as dispatchLifecycle, Dt as mcpPostStore, E as trackEnrichment, Et as isMcpTool, F as writePluginMap, G as trackSessionChanges, H as getFileDesc, I as generateProjectMap, J as saveApexState, K as validateRulesLoaded, L as isProject, M as lessonsStateFileFor, N as cartoSessionStart, O as aipilotPostToolUse, Ot as mcpPreIntercept, P as generateEcosystemMap, Q as subagentCacheContext, R as writeTree, S as saveSecurityState, St as recordActivity, T as todayUtc, Tt as MCP_TTL_MS, U as listChildren, V as countFiles, W as postEditTypescript, X as validateTeammateOutput, Y as logToolFailure, Z as trackAgentMemory, _ as trackWatchResearch, _t as sessionStatePath, a as TRIVIAL_BUDGET, at as pruneEmptyDirs, b as isoUtc, bt as taskContext, c as detectDuplication, ct as trimLogFile, d as lifecycleStdout, dt as projectContext, et as solidDetectStart, f as postEditContext, ft as claudeHome, g as postTrackingSideEffects, gt as saveSessionState, h as securityAdvisory, ht as sanitizeSessionId, i as REQUIRED_AGENTS, it as sessionStartCore, j as lessonsFileFor, k as dispatchAipilot, kt as queryOf, l as dryGate, lt as devContext, m as seoPostToolUseResponse, mt as loadSessionState, n as handlePre, nt as readRules, o as gate, ot as purgeTtlTree, p as seoPostToolUse, pt as fusengineCache, q as cleanupSession, r as DEFAULT_WINDOW_MS, rt as runSessionStartCleanups, s as preCommitGate, st as removeOldFiles, t as handleHook, tt as injectRules, u as extractSymbols, ut as gitContext, v as trackMcpResearch, vt as sessionsDir, w as securityStatePath, wt as normalizeEvent, x as loadSecurityState, xt as respond, y as trackSkillRead, yt as promptSubmitContext, z as loadEnriched } from "../handle-Cxgzd4pZ.mjs";
|
|
3
3
|
//#region src/runtime/storage.ts
|
|
4
4
|
/**
|
|
5
5
|
* The project's single state dir (`<root>/.harness`) — neutral + harness-agnostic,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusengine/harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
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",
|
package/dist/claude-BWZcrZbS.mjs
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { t as evaluate } from "./evaluate-9ch1K2kt.mjs";
|
|
2
|
-
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
3
|
-
//#region src/adapters/claude/index.ts
|
|
4
|
-
/**
|
|
5
|
-
* Claude Code adapter — the thin Claude-only shim over the portable policy core.
|
|
6
|
-
* Reads the hook stdin payload and emits hookSpecificOutput responses.
|
|
7
|
-
*/
|
|
8
|
-
/** Read & parse the Claude hook payload from stdin (empty object on bad input). */
|
|
9
|
-
async function readClaudeInput() {
|
|
10
|
-
const text = await Bun.stdin.text();
|
|
11
|
-
if (!text.trim()) return {};
|
|
12
|
-
try {
|
|
13
|
-
const parsed = JSON.parse(text);
|
|
14
|
-
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
15
|
-
} catch {
|
|
16
|
-
return {};
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
/** A `deny` hook response for a given event. */
|
|
20
|
-
function denyResponse(event, reason) {
|
|
21
|
-
return JSON.stringify({ hookSpecificOutput: {
|
|
22
|
-
hookEventName: event,
|
|
23
|
-
permissionDecision: "deny",
|
|
24
|
-
permissionDecisionReason: reason
|
|
25
|
-
} });
|
|
26
|
-
}
|
|
27
|
-
/** An `additionalContext` injection response. */
|
|
28
|
-
function contextResponse(event, text) {
|
|
29
|
-
return JSON.stringify({ hookSpecificOutput: {
|
|
30
|
-
hookEventName: event,
|
|
31
|
-
additionalContext: text
|
|
32
|
-
} });
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Render a portable {@link Prompt} as a Claude Code hook response:
|
|
36
|
-
* `block` → `permissionDecision: deny`, `ask` → `permissionDecision: ask`
|
|
37
|
-
* (interactive confirm), `inform` → `additionalContext`.
|
|
38
|
-
*/
|
|
39
|
-
function toClaudeResponse(event, prompt) {
|
|
40
|
-
const reason = formatPrompt(prompt);
|
|
41
|
-
if (prompt.kind === "block") return denyResponse(event, reason);
|
|
42
|
-
if (prompt.kind === "ask") return JSON.stringify({ hookSpecificOutput: {
|
|
43
|
-
hookEventName: event,
|
|
44
|
-
permissionDecision: "ask",
|
|
45
|
-
permissionDecisionReason: reason
|
|
46
|
-
} });
|
|
47
|
-
return contextResponse(event, reason);
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Run the bundled policy over a Claude payload and return the native response
|
|
51
|
-
* string (deny/ask/additionalContext), or null to allow.
|
|
52
|
-
*/
|
|
53
|
-
function guard(input) {
|
|
54
|
-
const result = evaluate({
|
|
55
|
-
tool: input.tool_name ?? "Write",
|
|
56
|
-
filePath: input.tool_input?.file_path,
|
|
57
|
-
content: input.tool_input?.content ?? input.tool_input?.new_string,
|
|
58
|
-
command: input.tool_input?.command
|
|
59
|
-
});
|
|
60
|
-
if (result.decision === "allow" || !result.prompt) return null;
|
|
61
|
-
return toClaudeResponse(input.hook_event_name ?? "PreToolUse", result.prompt);
|
|
62
|
-
}
|
|
63
|
-
/** @deprecated use {@link guard}. Kept for back-compat. */
|
|
64
|
-
const fileSizeGuard = guard;
|
|
65
|
-
//#endregion
|
|
66
|
-
export { readClaudeInput as a, guard as i, denyResponse as n, toClaudeResponse as o, fileSizeGuard as r, contextResponse as t };
|