@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.
@@ -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-BWZcrZbS.mjs";
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-BWZcrZbS.mjs";
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-D91N4ul1.mjs";
6
- import { t as handleHook } from "../handle-DJqHZcml.mjs";
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-BWZcrZbS.mjs";
12
+ import { c as pathExists, d as spawnCapture, f as writeText, l as readText, n as denyResponse, s as collectFiles, t as contextResponse, u as sleep } from "./claude-BatVYnAf.mjs";
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 agentsFile = Bun.file(join(apexDir, "AGENTS.md"));
1914
- const agents = await agentsFile.exists() ? (await agentsFile.text()).slice(0, 4e3) : "";
1912
+ const agentsPath = join(apexDir, "AGENTS.md");
1913
+ const agents = existsSync(agentsPath) ? readText(agentsPath).slice(0, 4e3) : "";
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(await Bun.file(path).text()).digest("hex");
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
- const file = Bun.file(sessionsFile);
2040
- if (!await file.exists()) return;
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
- const kept = entries.filter((e) => e.ts >= cutoff);
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 proc = Bun.spawn([
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 snapBunFile = Bun.file(snapFile);
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 file = Bun.file(join(docsDir, `${entry.hash}.md`));
2168
- if (!await file.exists()) continue;
2169
- const content = await file.text();
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 file glob patterns (monorepo-aware; separate to avoid brace-wildcards). */
2206
- const SRC_PATTERNS = [
2207
- "src/**/*.{ts,tsx,js,jsx}",
2208
- "app/**/*.{ts,tsx,js,jsx}",
2209
- "apps/*/src/**/*.{ts,tsx,js,jsx}",
2210
- "packages/*/src/**/*.{ts,tsx,js,jsx}"
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 patterns.
2218
+ * @returns Absolute paths matching the source extensions.
2217
2219
  */
2218
2220
  async function scanSourceFiles(projectPath, maxFiles = 200) {
2219
2221
  const files = [];
2220
- for (const pattern of SRC_PATTERNS) {
2221
- try {
2222
- for await (const p of new Glob(pattern).scan({
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 = await Bun.file(transcriptPath).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 = await Bun.file(transcriptPath).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 = await Bun.file(transcriptPath).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 = (await Bun.file(path).text()).split("\n").filter(Boolean);
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 || !await Bun.file(transcript).exists()) return;
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 Bun.sleep(delay);
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
- await Bun.write(join(docsDir, `${docHash}.md`), content);
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 || !await Bun.file(transcript).exists()) return;
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 = await Bun.file(path).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 || !await Bun.file(transcript).exists()) return;
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 Bun.sleep(100);
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
- const proc = Bun.spawn([
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
  }
@@ -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-D91N4ul1.mjs";
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), {
@@ -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-DJqHZcml.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-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.32",
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",
@@ -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 };