@fusengine/harness 0.1.5 → 0.1.7

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,25 @@
1
- import { t as preToolUse } from "../../cline-BxslHtBG.mjs";
1
+ import { t as evaluate } from "../../evaluate-CsYyUucy.mjs";
2
+ import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
3
+ //#region src/adapters/cline/index.ts
4
+ /**
5
+ * Cline adapter (hook-mode). Schema per docs.cline.bot / .clinerules/hooks (2026):
6
+ * `PreToolUse` blocks via `{ cancel: true }`; it cannot modify tool parameters.
7
+ */
8
+ /** Evaluate a tool use; cancel on a hard block, otherwise inject context. */
9
+ function preToolUse(input) {
10
+ const t = input.preToolUse;
11
+ const r = evaluate({
12
+ tool: t?.toolName ?? "write_to_file",
13
+ filePath: t?.parameters?.path,
14
+ content: t?.parameters?.content,
15
+ command: t?.parameters?.command
16
+ });
17
+ if (r.decision === "allow" || !r.prompt) return {};
18
+ const msg = formatPrompt(r.prompt);
19
+ return r.prompt.kind === "block" ? {
20
+ cancel: true,
21
+ errorMessage: msg
22
+ } : { contextModification: msg };
23
+ }
24
+ //#endregion
2
25
  export { preToolUse };
@@ -1,2 +1,37 @@
1
- import { n as beforeShellExecution, t as afterFileEdit } from "../../cursor-Bh7eh9y_.mjs";
1
+ import { t as evaluate } from "../../evaluate-CsYyUucy.mjs";
2
+ import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
3
+ //#region src/adapters/cursor/index.ts
4
+ /**
5
+ * Cursor adapter (hook-mode). Schemas per cursor.com/docs/hooks (2026):
6
+ * `beforeShellExecution` can block; `afterFileEdit` is observe-only.
7
+ */
8
+ function toPermission(kind) {
9
+ return kind === "block" ? "deny" : kind === "ask" ? "ask" : "allow";
10
+ }
11
+ /** Guard a shell command (git/install policies). */
12
+ function beforeShellExecution(payload) {
13
+ const r = evaluate({
14
+ tool: "Bash",
15
+ command: payload.command
16
+ });
17
+ if (r.decision === "allow" || !r.prompt) return { permission: "allow" };
18
+ const msg = formatPrompt(r.prompt);
19
+ return {
20
+ permission: toPermission(r.prompt.kind),
21
+ continue: false,
22
+ userMessage: msg,
23
+ agentMessage: msg
24
+ };
25
+ }
26
+ /** Observe a file edit (Cursor cannot block here). Returns the verdict for logging. */
27
+ function afterFileEdit(payload) {
28
+ const content = payload.edits?.map((e) => e.new_string).join("\n") ?? "";
29
+ const r = evaluate({
30
+ tool: "Edit",
31
+ filePath: payload.file_path,
32
+ content
33
+ });
34
+ return { violation: r.decision === "deny" ? r.message : null };
35
+ }
36
+ //#endregion
2
37
  export { afterFileEdit, beforeShellExecution };
@@ -1,2 +1,25 @@
1
- import { t as beforeTool } from "../../gemini-SrK_fFAr.mjs";
1
+ import { t as evaluate } from "../../evaluate-CsYyUucy.mjs";
2
+ import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
3
+ //#region src/adapters/gemini/index.ts
4
+ /**
5
+ * Gemini CLI adapter (hook-mode). Schema per google-gemini/gemini-cli docs/hooks (2026):
6
+ * `BeforeTool` blocks via `{ decision: "deny", reason }` (or exit 2).
7
+ */
8
+ /** Evaluate a tool use; deny on a hard block, otherwise inject context. */
9
+ function beforeTool(input) {
10
+ const i = input.tool_input;
11
+ const r = evaluate({
12
+ tool: input.tool_name ?? "write_file",
13
+ filePath: i?.path,
14
+ content: i?.content,
15
+ command: i?.command
16
+ });
17
+ if (r.decision === "allow" || !r.prompt) return {};
18
+ const msg = formatPrompt(r.prompt);
19
+ return r.prompt.kind === "block" ? {
20
+ decision: "deny",
21
+ reason: msg
22
+ } : { hookSpecificOutput: { additionalContext: msg } };
23
+ }
24
+ //#endregion
2
25
  export { beforeTool };
package/dist/cli/bin.mjs CHANGED
@@ -2,49 +2,7 @@
2
2
  import { t as detectHarness } from "../harness-C8Nxxyn_.mjs";
3
3
  import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-h_2LNEA8.mjs";
4
4
  import { n as writeInitFile, t as initFor } from "../run-Cc98348q.mjs";
5
- import { i as guard } from "../claude-DbzjbxmO.mjs";
6
- import { n as beforeShellExecution, t as afterFileEdit } from "../cursor-Bh7eh9y_.mjs";
7
- import { t as preToolUse } from "../cline-BxslHtBG.mjs";
8
- import { t as beforeTool } from "../gemini-SrK_fFAr.mjs";
9
- //#region src/cli/hook.ts
10
- /**
11
- * Route a harness hook payload to its adapter and produce the native response.
12
- * The deny/ask decision lives in `stdout` (the harness parses it); exit stays 0.
13
- */
14
- function dispatchHook(id, payload) {
15
- switch (id) {
16
- case "claude-code":
17
- case "codex": return {
18
- stdout: guard(payload) ?? "",
19
- exit: 0
20
- };
21
- case "cursor":
22
- if (payload.hook_event_name === "afterFileEdit") {
23
- afterFileEdit(payload);
24
- return {
25
- stdout: "",
26
- exit: 0
27
- };
28
- }
29
- return {
30
- stdout: JSON.stringify(beforeShellExecution(payload)),
31
- exit: 0
32
- };
33
- case "cline": return {
34
- stdout: JSON.stringify(preToolUse(payload)),
35
- exit: 0
36
- };
37
- case "gemini-cli": return {
38
- stdout: JSON.stringify(beforeTool(payload)),
39
- exit: 0
40
- };
41
- default: return {
42
- stdout: "",
43
- exit: 0
44
- };
45
- }
46
- }
47
- //#endregion
5
+ import { t as handleHook } from "../handle-BGe0QZvQ.mjs";
48
6
  //#region src/cli/bin.ts
49
7
  /**
50
8
  * harness — CLI for @fusengine/harness.
@@ -66,7 +24,10 @@ async function readStdin() {
66
24
  }
67
25
  const cmd = process.argv[2];
68
26
  if (cmd === "hook") {
69
- const outcome = dispatchHook(process.argv[3] ?? detectHarness().id, await readStdin());
27
+ const outcome = await handleHook(process.argv[3] ?? detectHarness().id, await readStdin(), {
28
+ now: Date.now(),
29
+ cwd: process.cwd()
30
+ });
70
31
  if (outcome.stdout) process.stdout.write(outcome.stdout);
71
32
  process.exit(outcome.exit);
72
33
  } else if (cmd === "init") {
@@ -0,0 +1,224 @@
1
+ import { l as detectFramework, t as evaluate } from "./evaluate-CsYyUucy.mjs";
2
+ import { r as evaluateApex } from "./apex-gGrHzvM2.mjs";
3
+ import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
4
+ import { a as recordAgent, n as saveTrack, o as recordDoc, r as agentsFresh, s as recordRefRead, t as loadTrack } from "./store-BWvwnnf6.mjs";
5
+ import { join } from "node:path";
6
+ import { tmpdir } from "node:os";
7
+ //#region src/runtime/activity.ts
8
+ /** Read tools across harnesses (Claude `Read`, Gemini/Cline `read_file`, …). */
9
+ const READ_TOOLS = /* @__PURE__ */ new Set([
10
+ "Read",
11
+ "read_file",
12
+ "read_many_files"
13
+ ]);
14
+ /**
15
+ * Map a live tool-use to the activity to record, or null when nothing is
16
+ * tracked. Works across harnesses — tool names are globally distinct:
17
+ * - MCP doc calls (`context7` / `exa`, any separator) → `doc`
18
+ * - `Task` + `subagent_type` (Claude/Cursor) → `agent` (bare agent name)
19
+ * - a read tool opening a `.md` reference → `ref`
20
+ */
21
+ function activityFor(event) {
22
+ if (/context7|exa/i.test(event.tool)) return {
23
+ kind: "doc",
24
+ framework: event.framework,
25
+ sessionId: event.sessionId,
26
+ source: /exa/i.test(event.tool) ? "exa" : "context7"
27
+ };
28
+ if (event.tool === "Task") {
29
+ const name = String(event.input?.subagent_type ?? "").split(":").pop() ?? "";
30
+ return name ? {
31
+ kind: "agent",
32
+ name,
33
+ ts: event.now
34
+ } : null;
35
+ }
36
+ if (READ_TOOLS.has(event.tool)) {
37
+ const path = String(event.input?.file_path ?? event.input?.path ?? "");
38
+ if (path.endsWith(".md")) return {
39
+ kind: "ref",
40
+ path
41
+ };
42
+ }
43
+ return null;
44
+ }
45
+ //#endregion
46
+ //#region src/runtime/gate.ts
47
+ /** Prior agents the freshness gate requires before a code edit. */
48
+ const REQUIRED_AGENTS = ["explore-codebase", "research-expert"];
49
+ /** Default freshness window for {@link REQUIRED_AGENTS} (4 min, the APEX TTL). */
50
+ const DEFAULT_WINDOW_MS = 24e4;
51
+ /**
52
+ * Full gate: the stateless guards (file-size, git) first, then the stateful
53
+ * APEX gates fed from the session track. Returns the first blocking prompt, or
54
+ * null to allow. APEX gates apply only to code edits (a `filePath`).
55
+ */
56
+ async function gate(input) {
57
+ const quick = evaluate({
58
+ tool: input.tool,
59
+ filePath: input.filePath,
60
+ content: input.content,
61
+ command: input.command
62
+ });
63
+ if (quick.decision !== "allow" && quick.prompt) return quick.prompt;
64
+ if (!input.filePath) return null;
65
+ const track = await loadTrack(input.trackFile);
66
+ return evaluateApex({
67
+ sessionId: input.sessionId,
68
+ framework: input.framework,
69
+ filePath: input.filePath,
70
+ content: input.content ?? "",
71
+ authorizations: track.authorizations,
72
+ refs: input.refs,
73
+ refsRead: track.refsRead,
74
+ agentsFresh: agentsFresh(track, [...REQUIRED_AGENTS], input.windowMs ?? 24e4, input.now)
75
+ });
76
+ }
77
+ //#endregion
78
+ //#region src/runtime/normalize.ts
79
+ function str(v) {
80
+ return typeof v === "string" ? v : void 0;
81
+ }
82
+ /**
83
+ * Normalize a harness hook payload into a uniform event. Handles Cline's nested
84
+ * `preToolUse`/`postToolUse` shape and the top-level `tool_name`/`tool_input`
85
+ * shape used by Claude, Codex, Gemini, and Cursor.
86
+ */
87
+ function normalizeEvent(id, payload) {
88
+ if (id === "cline") {
89
+ const post = payload.postToolUse;
90
+ const node = post ?? payload.preToolUse ?? {};
91
+ const params = node.parameters ?? {};
92
+ return {
93
+ phase: post ? "post" : "pre",
94
+ tool: str(node.toolName) ?? "",
95
+ input: params,
96
+ sessionId: str(payload.taskId) ?? "",
97
+ filePath: str(params.path),
98
+ content: str(params.content),
99
+ command: str(params.command)
100
+ };
101
+ }
102
+ const event = str(payload.hook_event_name) ?? "";
103
+ const input = payload.tool_input ?? payload;
104
+ return {
105
+ phase: /post|after/i.test(event) ? "post" : "pre",
106
+ tool: str(payload.tool_name) ?? "",
107
+ input,
108
+ sessionId: str(payload.session_id) ?? str(payload.conversation_id) ?? "",
109
+ filePath: str(input.file_path) ?? str(input.path) ?? str(payload.file_path),
110
+ content: str(input.content) ?? str(input.new_string),
111
+ command: str(input.command) ?? str(payload.command)
112
+ };
113
+ }
114
+ //#endregion
115
+ //#region src/runtime/paths.ts
116
+ /** Path to a session's track file (under a per-tool base dir). */
117
+ function trackFile(sessionId, baseDir = join(tmpdir(), "fuse-harness")) {
118
+ return join(baseDir, `track-${sessionId.replace(/[^A-Za-z0-9_-]/g, "_") || "default"}.json`);
119
+ }
120
+ //#endregion
121
+ //#region src/runtime/record.ts
122
+ /** Apply an activity to a session's track and persist it (PostToolUse path). */
123
+ async function recordActivity(file, activity) {
124
+ const track = await loadTrack(file);
125
+ await saveTrack(file, activity.kind === "agent" ? recordAgent(track, activity.name, activity.ts) : activity.kind === "doc" ? recordDoc(track, activity.framework, activity.sessionId, activity.source) : recordRefRead(track, activity.path));
126
+ }
127
+ //#endregion
128
+ //#region src/runtime/respond.ts
129
+ /**
130
+ * Map a portable {@link Prompt} to a harness's native hook response. `block`
131
+ * denies; anything else asks/injects context. (Codex/Cursor parse but ignore
132
+ * `ask` — they only honor deny.)
133
+ */
134
+ function respond(id, prompt) {
135
+ const message = formatPrompt(prompt);
136
+ const deny = prompt.kind === "block";
137
+ switch (id) {
138
+ case "claude-code":
139
+ case "codex": return JSON.stringify({ hookSpecificOutput: {
140
+ hookEventName: "PreToolUse",
141
+ permissionDecision: deny ? "deny" : "ask",
142
+ permissionDecisionReason: message
143
+ } });
144
+ case "gemini-cli": return JSON.stringify(deny ? {
145
+ decision: "deny",
146
+ reason: message
147
+ } : { hookSpecificOutput: { additionalContext: message } });
148
+ case "cursor": return JSON.stringify({
149
+ permission: deny ? "deny" : "ask",
150
+ continue: false,
151
+ userMessage: message,
152
+ agentMessage: message
153
+ });
154
+ case "cline": return JSON.stringify(deny ? {
155
+ cancel: true,
156
+ errorMessage: message
157
+ } : { contextModification: message });
158
+ default: return "";
159
+ }
160
+ }
161
+ //#endregion
162
+ //#region src/runtime/storage.ts
163
+ /** Per-harness config dir (relative to the project root) where state lives. */
164
+ const STATE_DIR = {
165
+ "claude-code": ".claude",
166
+ codex: ".codex",
167
+ cursor: ".cursor",
168
+ "gemini-cli": ".gemini",
169
+ cline: ".clinerules"
170
+ };
171
+ /**
172
+ * Directory where a harness's fuse-harness track files live — under that
173
+ * harness's own config dir (`.claude`, `.codex`, …) so state sits next to its
174
+ * hooks. Falls back to `.fuse-harness` for harnesses without a known config dir.
175
+ */
176
+ function harnessTrackDir(id, projectRoot) {
177
+ return join(projectRoot, STATE_DIR[id] ?? ".fuse-harness", "harness");
178
+ }
179
+ //#endregion
180
+ //#region src/runtime/handle.ts
181
+ /**
182
+ * The full hook handler: on a PRE event it gates the tool-use (stateless guards
183
+ * then APEX gates from the session track) and returns the native response; on a
184
+ * POST event it records the activity into the track. The loop that makes the
185
+ * package behave like the Claude plugin, on any harness.
186
+ */
187
+ async function handleHook(id, payload, opts) {
188
+ const event = normalizeEvent(id, payload);
189
+ const file = trackFile(event.sessionId, harnessTrackDir(id, opts.cwd));
190
+ const framework = detectFramework(event.filePath ?? "", event.content ?? "");
191
+ if (event.phase === "post") {
192
+ const activity = activityFor({
193
+ tool: event.tool,
194
+ input: event.input,
195
+ sessionId: event.sessionId,
196
+ framework,
197
+ now: opts.now
198
+ });
199
+ if (activity) await recordActivity(file, activity);
200
+ return {
201
+ stdout: "",
202
+ exit: 0
203
+ };
204
+ }
205
+ const prompt = await gate({
206
+ sessionId: event.sessionId,
207
+ framework,
208
+ tool: event.tool,
209
+ filePath: event.filePath,
210
+ content: event.content,
211
+ command: event.command,
212
+ now: opts.now,
213
+ trackFile: file
214
+ });
215
+ return prompt ? {
216
+ stdout: respond(id, prompt),
217
+ exit: 0
218
+ } : {
219
+ stdout: "",
220
+ exit: 0
221
+ };
222
+ }
223
+ //#endregion
224
+ export { trackFile as a, REQUIRED_AGENTS as c, recordActivity as i, gate as l, harnessTrackDir as n, normalizeEvent as o, respond as r, DEFAULT_WINDOW_MS as s, handleHook as t, activityFor as u };
@@ -1,10 +1,19 @@
1
1
  import { t as Prompt } from "../types-D56jSgD9.mjs";
2
+ import { t as HarnessId } from "../harness-DwJskkz_.mjs";
2
3
  import { t as RefMeta } from "../types-CY5qT2X1.mjs";
3
4
 
4
5
  //#region src/runtime/paths.d.ts
5
6
  /** Path to a session's track file (under a per-tool base dir). */
6
7
  declare function trackFile(sessionId: string, baseDir?: string): string;
7
8
  //#endregion
9
+ //#region src/runtime/storage.d.ts
10
+ /**
11
+ * Directory where a harness's fuse-harness track files live — under that
12
+ * harness's own config dir (`.claude`, `.codex`, …) so state sits next to its
13
+ * hooks. Falls back to `.fuse-harness` for harnesses without a known config dir.
14
+ */
15
+ declare function harnessTrackDir(id: HarnessId, projectRoot: string): string;
16
+ //#endregion
8
17
  //#region src/runtime/record.d.ts
9
18
  /** A unit of session activity to record (discriminated union on `kind`). */
10
19
  type Activity = {
@@ -23,6 +32,25 @@ type Activity = {
23
32
  /** Apply an activity to a session's track and persist it (PostToolUse path). */
24
33
  declare function recordActivity(file: string, activity: Activity): Promise<void>;
25
34
  //#endregion
35
+ //#region src/runtime/activity.d.ts
36
+ /** A live tool-use, already normalized to `tool` + `input` by the adapter. */
37
+ interface ToolEvent {
38
+ /** The harness's `tool_name` (Cline: `preToolUse.toolName`), as a plain string. */
39
+ tool: string;
40
+ input?: Record<string, unknown>;
41
+ sessionId: string;
42
+ framework: string;
43
+ now: number;
44
+ }
45
+ /**
46
+ * Map a live tool-use to the activity to record, or null when nothing is
47
+ * tracked. Works across harnesses — tool names are globally distinct:
48
+ * - MCP doc calls (`context7` / `exa`, any separator) → `doc`
49
+ * - `Task` + `subagent_type` (Claude/Cursor) → `agent` (bare agent name)
50
+ * - a read tool opening a `.md` reference → `ref`
51
+ */
52
+ declare function activityFor(event: ToolEvent): Activity | null;
53
+ //#endregion
26
54
  //#region src/runtime/gate.d.ts
27
55
  /** Prior agents the freshness gate requires before a code edit. */
28
56
  declare const REQUIRED_AGENTS: ReadonlyArray<string>;
@@ -48,4 +76,49 @@ interface GateInput {
48
76
  */
49
77
  declare function gate(input: GateInput): Promise<Prompt | null>;
50
78
  //#endregion
51
- export { Activity, DEFAULT_WINDOW_MS, GateInput, REQUIRED_AGENTS, gate, recordActivity, trackFile };
79
+ //#region src/runtime/normalize.d.ts
80
+ /** A hook event normalized across harnesses. */
81
+ interface NormalizedEvent {
82
+ phase: "pre" | "post";
83
+ tool: string;
84
+ input: Record<string, unknown>;
85
+ sessionId: string;
86
+ filePath?: string;
87
+ content?: string;
88
+ command?: string;
89
+ }
90
+ /**
91
+ * Normalize a harness hook payload into a uniform event. Handles Cline's nested
92
+ * `preToolUse`/`postToolUse` shape and the top-level `tool_name`/`tool_input`
93
+ * shape used by Claude, Codex, Gemini, and Cursor.
94
+ */
95
+ declare function normalizeEvent(id: string, payload: Record<string, unknown>): NormalizedEvent;
96
+ //#endregion
97
+ //#region src/runtime/respond.d.ts
98
+ /**
99
+ * Map a portable {@link Prompt} to a harness's native hook response. `block`
100
+ * denies; anything else asks/injects context. (Codex/Cursor parse but ignore
101
+ * `ask` — they only honor deny.)
102
+ */
103
+ declare function respond(id: string, prompt: Prompt): string;
104
+ //#endregion
105
+ //#region src/runtime/handle.d.ts
106
+ /** Options for {@link handleHook} (caller supplies the clock + project root). */
107
+ interface HandleOptions {
108
+ now: number;
109
+ cwd: string;
110
+ }
111
+ /** What the hook bin should print + exit with. */
112
+ interface HandleOutcome {
113
+ stdout: string;
114
+ exit: number;
115
+ }
116
+ /**
117
+ * The full hook handler: on a PRE event it gates the tool-use (stateless guards
118
+ * then APEX gates from the session track) and returns the native response; on a
119
+ * POST event it records the activity into the track. The loop that makes the
120
+ * package behave like the Claude plugin, on any harness.
121
+ */
122
+ declare function handleHook(id: string, payload: Record<string, unknown>, opts: HandleOptions): Promise<HandleOutcome>;
123
+ //#endregion
124
+ export { Activity, DEFAULT_WINDOW_MS, GateInput, HandleOptions, HandleOutcome, NormalizedEvent, REQUIRED_AGENTS, ToolEvent, activityFor, gate, handleHook, harnessTrackDir, normalizeEvent, recordActivity, respond, trackFile };
@@ -1,51 +1,2 @@
1
- import { t as evaluate } from "../evaluate-CsYyUucy.mjs";
2
- import { r as evaluateApex } from "../apex-gGrHzvM2.mjs";
3
- import { a as recordAgent, n as saveTrack, o as recordDoc, r as agentsFresh, s as recordRefRead, t as loadTrack } from "../store-BWvwnnf6.mjs";
4
- import { join } from "node:path";
5
- import { tmpdir } from "node:os";
6
- //#region src/runtime/paths.ts
7
- /** Path to a session's track file (under a per-tool base dir). */
8
- function trackFile(sessionId, baseDir = join(tmpdir(), "fuse-harness")) {
9
- return join(baseDir, `track-${sessionId.replace(/[^A-Za-z0-9_-]/g, "_") || "default"}.json`);
10
- }
11
- //#endregion
12
- //#region src/runtime/record.ts
13
- /** Apply an activity to a session's track and persist it (PostToolUse path). */
14
- async function recordActivity(file, activity) {
15
- const track = await loadTrack(file);
16
- await saveTrack(file, activity.kind === "agent" ? recordAgent(track, activity.name, activity.ts) : activity.kind === "doc" ? recordDoc(track, activity.framework, activity.sessionId, activity.source) : recordRefRead(track, activity.path));
17
- }
18
- //#endregion
19
- //#region src/runtime/gate.ts
20
- /** Prior agents the freshness gate requires before a code edit. */
21
- const REQUIRED_AGENTS = ["explore-codebase", "research-expert"];
22
- /** Default freshness window for {@link REQUIRED_AGENTS} (4 min, the APEX TTL). */
23
- const DEFAULT_WINDOW_MS = 24e4;
24
- /**
25
- * Full gate: the stateless guards (file-size, git) first, then the stateful
26
- * APEX gates fed from the session track. Returns the first blocking prompt, or
27
- * null to allow. APEX gates apply only to code edits (a `filePath`).
28
- */
29
- async function gate(input) {
30
- const quick = evaluate({
31
- tool: input.tool,
32
- filePath: input.filePath,
33
- content: input.content,
34
- command: input.command
35
- });
36
- if (quick.decision !== "allow" && quick.prompt) return quick.prompt;
37
- if (!input.filePath) return null;
38
- const track = await loadTrack(input.trackFile);
39
- return evaluateApex({
40
- sessionId: input.sessionId,
41
- framework: input.framework,
42
- filePath: input.filePath,
43
- content: input.content ?? "",
44
- authorizations: track.authorizations,
45
- refs: input.refs,
46
- refsRead: track.refsRead,
47
- agentsFresh: agentsFresh(track, [...REQUIRED_AGENTS], input.windowMs ?? 24e4, input.now)
48
- });
49
- }
50
- //#endregion
51
- export { DEFAULT_WINDOW_MS, REQUIRED_AGENTS, gate, recordActivity, trackFile };
1
+ import { a as trackFile, c as REQUIRED_AGENTS, i as recordActivity, l as gate, n as harnessTrackDir, o as normalizeEvent, r as respond, s as DEFAULT_WINDOW_MS, t as handleHook, u as activityFor } from "../handle-BGe0QZvQ.mjs";
2
+ export { DEFAULT_WINDOW_MS, REQUIRED_AGENTS, activityFor, gate, handleHook, harnessTrackDir, normalizeEvent, recordActivity, respond, trackFile };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusengine/harness",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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,25 +0,0 @@
1
- import { t as evaluate } from "./evaluate-CsYyUucy.mjs";
2
- import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
3
- //#region src/adapters/cline/index.ts
4
- /**
5
- * Cline adapter (hook-mode). Schema per docs.cline.bot / .clinerules/hooks (2026):
6
- * `PreToolUse` blocks via `{ cancel: true }`; it cannot modify tool parameters.
7
- */
8
- /** Evaluate a tool use; cancel on a hard block, otherwise inject context. */
9
- function preToolUse(input) {
10
- const t = input.preToolUse;
11
- const r = evaluate({
12
- tool: t?.toolName ?? "write_to_file",
13
- filePath: t?.parameters?.path,
14
- content: t?.parameters?.content,
15
- command: t?.parameters?.command
16
- });
17
- if (r.decision === "allow" || !r.prompt) return {};
18
- const msg = formatPrompt(r.prompt);
19
- return r.prompt.kind === "block" ? {
20
- cancel: true,
21
- errorMessage: msg
22
- } : { contextModification: msg };
23
- }
24
- //#endregion
25
- export { preToolUse as t };
@@ -1,37 +0,0 @@
1
- import { t as evaluate } from "./evaluate-CsYyUucy.mjs";
2
- import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
3
- //#region src/adapters/cursor/index.ts
4
- /**
5
- * Cursor adapter (hook-mode). Schemas per cursor.com/docs/hooks (2026):
6
- * `beforeShellExecution` can block; `afterFileEdit` is observe-only.
7
- */
8
- function toPermission(kind) {
9
- return kind === "block" ? "deny" : kind === "ask" ? "ask" : "allow";
10
- }
11
- /** Guard a shell command (git/install policies). */
12
- function beforeShellExecution(payload) {
13
- const r = evaluate({
14
- tool: "Bash",
15
- command: payload.command
16
- });
17
- if (r.decision === "allow" || !r.prompt) return { permission: "allow" };
18
- const msg = formatPrompt(r.prompt);
19
- return {
20
- permission: toPermission(r.prompt.kind),
21
- continue: false,
22
- userMessage: msg,
23
- agentMessage: msg
24
- };
25
- }
26
- /** Observe a file edit (Cursor cannot block here). Returns the verdict for logging. */
27
- function afterFileEdit(payload) {
28
- const content = payload.edits?.map((e) => e.new_string).join("\n") ?? "";
29
- const r = evaluate({
30
- tool: "Edit",
31
- filePath: payload.file_path,
32
- content
33
- });
34
- return { violation: r.decision === "deny" ? r.message : null };
35
- }
36
- //#endregion
37
- export { beforeShellExecution as n, afterFileEdit as t };
@@ -1,25 +0,0 @@
1
- import { t as evaluate } from "./evaluate-CsYyUucy.mjs";
2
- import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
3
- //#region src/adapters/gemini/index.ts
4
- /**
5
- * Gemini CLI adapter (hook-mode). Schema per google-gemini/gemini-cli docs/hooks (2026):
6
- * `BeforeTool` blocks via `{ decision: "deny", reason }` (or exit 2).
7
- */
8
- /** Evaluate a tool use; deny on a hard block, otherwise inject context. */
9
- function beforeTool(input) {
10
- const i = input.tool_input;
11
- const r = evaluate({
12
- tool: input.tool_name ?? "write_file",
13
- filePath: i?.path,
14
- content: i?.content,
15
- command: i?.command
16
- });
17
- if (r.decision === "allow" || !r.prompt) return {};
18
- const msg = formatPrompt(r.prompt);
19
- return r.prompt.kind === "block" ? {
20
- decision: "deny",
21
- reason: msg
22
- } : { hookSpecificOutput: { additionalContext: msg } };
23
- }
24
- //#endregion
25
- export { beforeTool as t };