@fusengine/harness 0.1.0 → 0.1.1
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/cline/index.d.mts +23 -0
- package/dist/adapters/cline/index.mjs +25 -0
- package/dist/adapters/cursor/index.d.mts +31 -0
- package/dist/adapters/cursor/index.mjs +37 -0
- package/dist/adapters/gemini/index.d.mts +23 -0
- package/dist/adapters/gemini/index.mjs +25 -0
- package/dist/cli/bin.d.mts +1 -0
- package/dist/cli/bin.mjs +18 -0
- package/dist/cli/index.d.mts +12 -0
- package/dist/cli/index.mjs +2 -0
- package/dist/run-h_2LNEA8.mjs +32 -0
- package/package.json +11 -4
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/adapters/cline/index.d.ts
|
|
2
|
+
/** `PreToolUse` stdin payload (subset). */
|
|
3
|
+
interface ClineHookInput {
|
|
4
|
+
hookName?: string;
|
|
5
|
+
preToolUse?: {
|
|
6
|
+
toolName?: string;
|
|
7
|
+
parameters?: {
|
|
8
|
+
path?: string;
|
|
9
|
+
content?: string;
|
|
10
|
+
command?: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** Hook stdout response. */
|
|
15
|
+
interface ClineResponse {
|
|
16
|
+
cancel?: boolean;
|
|
17
|
+
errorMessage?: string;
|
|
18
|
+
contextModification?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Evaluate a tool use; cancel on a hard block, otherwise inject context. */
|
|
21
|
+
declare function preToolUse(input: ClineHookInput): ClineResponse;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { ClineHookInput, ClineResponse, preToolUse };
|
|
@@ -0,0 +1,25 @@
|
|
|
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 };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/adapters/cursor/index.d.ts
|
|
2
|
+
/** `beforeShellExecution` stdin payload (subset). */
|
|
3
|
+
interface CursorShellPayload {
|
|
4
|
+
command?: string;
|
|
5
|
+
cwd?: string;
|
|
6
|
+
workspace_roots?: string[];
|
|
7
|
+
hook_event_name?: string;
|
|
8
|
+
}
|
|
9
|
+
/** `afterFileEdit` stdin payload (subset). */
|
|
10
|
+
interface CursorEditPayload {
|
|
11
|
+
file_path?: string;
|
|
12
|
+
edits?: {
|
|
13
|
+
old_string: string;
|
|
14
|
+
new_string: string;
|
|
15
|
+
}[];
|
|
16
|
+
}
|
|
17
|
+
/** `beforeShellExecution` stdout response. */
|
|
18
|
+
interface CursorResponse {
|
|
19
|
+
permission: "allow" | "deny" | "ask";
|
|
20
|
+
continue?: boolean;
|
|
21
|
+
userMessage?: string;
|
|
22
|
+
agentMessage?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Guard a shell command (git/install policies). */
|
|
25
|
+
declare function beforeShellExecution(payload: CursorShellPayload): CursorResponse;
|
|
26
|
+
/** Observe a file edit (Cursor cannot block here). Returns the verdict for logging. */
|
|
27
|
+
declare function afterFileEdit(payload: CursorEditPayload): {
|
|
28
|
+
violation: string | null;
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
export { CursorEditPayload, CursorResponse, CursorShellPayload, afterFileEdit, beforeShellExecution };
|
|
@@ -0,0 +1,37 @@
|
|
|
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 { afterFileEdit, beforeShellExecution };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/adapters/gemini/index.d.ts
|
|
2
|
+
/** `BeforeTool` stdin payload (subset). */
|
|
3
|
+
interface GeminiHookInput {
|
|
4
|
+
tool_name?: string;
|
|
5
|
+
tool_input?: {
|
|
6
|
+
command?: string;
|
|
7
|
+
path?: string;
|
|
8
|
+
content?: string;
|
|
9
|
+
};
|
|
10
|
+
hook_event_name?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Hook stdout response. */
|
|
13
|
+
interface GeminiResponse {
|
|
14
|
+
decision?: "allow" | "deny";
|
|
15
|
+
reason?: string;
|
|
16
|
+
hookSpecificOutput?: {
|
|
17
|
+
additionalContext?: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Evaluate a tool use; deny on a hard block, otherwise inject context. */
|
|
21
|
+
declare function beforeTool(input: GeminiHookInput): GeminiResponse;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { GeminiHookInput, GeminiResponse, beforeTool };
|
|
@@ -0,0 +1,25 @@
|
|
|
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 };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli/bin.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-h_2LNEA8.mjs";
|
|
3
|
+
//#region src/cli/bin.ts
|
|
4
|
+
/**
|
|
5
|
+
* harness-check — cli-mode entry for harnesses without hooks (Aider, Windsurf,
|
|
6
|
+
* OpenHands...). Run it as a pre-commit step: it checks staged files against the
|
|
7
|
+
* policy core and exits non-zero on a violation.
|
|
8
|
+
*/
|
|
9
|
+
const files = stagedFiles();
|
|
10
|
+
if (files.length === 0) process.exit(0);
|
|
11
|
+
const violations = checkStaged(files, stagedContent);
|
|
12
|
+
if (violations.length > 0) {
|
|
13
|
+
process.stderr.write(`harness-check: policy violations\n\n${violations.join("\n\n")}\n`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
process.exit(0);
|
|
17
|
+
//#endregion
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/cli/run.d.ts
|
|
2
|
+
/** Staged files (Added/Copied/Modified/Renamed). Uses `node:child_process` (Bun shell can hang on `git show`). */
|
|
3
|
+
declare function stagedFiles(): string[];
|
|
4
|
+
/** Read a file's staged (index) content — not the working-tree version. */
|
|
5
|
+
declare function stagedContent(path: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Evaluate staged code files against the policy core; return violation blocks.
|
|
8
|
+
* `read` is injected (the real impl is {@link stagedContent}) so this is pure + testable.
|
|
9
|
+
*/
|
|
10
|
+
declare function checkStaged(files: string[], read: (path: string) => string): string[];
|
|
11
|
+
//#endregion
|
|
12
|
+
export { checkStaged, stagedContent, stagedFiles };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { t as isCodeFile } from "./project-root-C4ks_q1G.mjs";
|
|
2
|
+
import { t as evaluate } from "./evaluate-CsYyUucy.mjs";
|
|
3
|
+
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
//#region src/cli/run.ts
|
|
6
|
+
/** Staged files (Added/Copied/Modified/Renamed). Uses `node:child_process` (Bun shell can hang on `git show`). */
|
|
7
|
+
function stagedFiles() {
|
|
8
|
+
return execSync("git diff --cached --name-only --diff-filter=ACMR", { encoding: "utf8" }).trim().split("\n").filter(Boolean);
|
|
9
|
+
}
|
|
10
|
+
/** Read a file's staged (index) content — not the working-tree version. */
|
|
11
|
+
function stagedContent(path) {
|
|
12
|
+
return execSync(`git show ":${path}"`, { encoding: "utf8" });
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Evaluate staged code files against the policy core; return violation blocks.
|
|
16
|
+
* `read` is injected (the real impl is {@link stagedContent}) so this is pure + testable.
|
|
17
|
+
*/
|
|
18
|
+
function checkStaged(files, read) {
|
|
19
|
+
const violations = [];
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
if (!isCodeFile(file)) continue;
|
|
22
|
+
const r = evaluate({
|
|
23
|
+
tool: "Write",
|
|
24
|
+
filePath: file,
|
|
25
|
+
content: read(file)
|
|
26
|
+
});
|
|
27
|
+
if (r.decision === "deny" && r.prompt) violations.push(`${file}\n${formatPrompt(r.prompt)}`);
|
|
28
|
+
}
|
|
29
|
+
return violations;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { stagedContent as n, stagedFiles as r, checkStaged as t };
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusengine/harness",
|
|
3
|
-
"version": "0.1.
|
|
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, and
|
|
3
|
+
"version": "0.1.1",
|
|
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",
|
|
7
7
|
"types": "dist/index.d.mts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"harness-check": "./dist/cli/bin.mjs"
|
|
10
|
+
},
|
|
8
11
|
"exports": {
|
|
9
12
|
".": { "bun": "./src/index.ts", "types": "./dist/index.d.mts", "import": "./dist/index.mjs" },
|
|
10
13
|
"./config": { "bun": "./src/config/index.ts", "types": "./dist/config/index.d.mts", "import": "./dist/config/index.mjs" },
|
|
@@ -18,7 +21,11 @@
|
|
|
18
21
|
"./refs": { "bun": "./src/refs/index.ts", "types": "./dist/refs/index.d.mts", "import": "./dist/refs/index.mjs" },
|
|
19
22
|
"./state": { "bun": "./src/state/index.ts", "types": "./dist/state/index.d.mts", "import": "./dist/state/index.mjs" },
|
|
20
23
|
"./statusline": { "bun": "./src/statusline/index.ts", "types": "./dist/statusline/index.d.mts", "import": "./dist/statusline/index.mjs" },
|
|
21
|
-
"./
|
|
24
|
+
"./cli": { "bun": "./src/cli/index.ts", "types": "./dist/cli/index.d.mts", "import": "./dist/cli/index.mjs" },
|
|
25
|
+
"./adapters/claude": { "bun": "./src/adapters/claude/index.ts", "types": "./dist/adapters/claude/index.d.mts", "import": "./dist/adapters/claude/index.mjs" },
|
|
26
|
+
"./adapters/cursor": { "bun": "./src/adapters/cursor/index.ts", "types": "./dist/adapters/cursor/index.d.mts", "import": "./dist/adapters/cursor/index.mjs" },
|
|
27
|
+
"./adapters/cline": { "bun": "./src/adapters/cline/index.ts", "types": "./dist/adapters/cline/index.d.mts", "import": "./dist/adapters/cline/index.mjs" },
|
|
28
|
+
"./adapters/gemini": { "bun": "./src/adapters/gemini/index.ts", "types": "./dist/adapters/gemini/index.d.mts", "import": "./dist/adapters/gemini/index.mjs" }
|
|
22
29
|
},
|
|
23
30
|
"files": ["dist", "README.md", "LICENSE"],
|
|
24
31
|
"license": "MIT",
|
|
@@ -26,7 +33,7 @@
|
|
|
26
33
|
"scripts": {
|
|
27
34
|
"test": "bun test",
|
|
28
35
|
"typecheck": "tsc --noEmit",
|
|
29
|
-
"build": "tsdown src/index.ts src/config/index.ts src/util/index.ts src/detect/index.ts src/policy/index.ts src/prompt/index.ts src/memory/index.ts src/cache/index.ts src/freshness/index.ts src/refs/index.ts src/state/index.ts src/statusline/index.ts src/adapters/claude/index.ts --dts --format esm --clean --out-dir dist",
|
|
36
|
+
"build": "tsdown src/index.ts src/config/index.ts src/util/index.ts src/detect/index.ts src/policy/index.ts src/prompt/index.ts src/memory/index.ts src/cache/index.ts src/freshness/index.ts src/refs/index.ts src/state/index.ts src/statusline/index.ts src/cli/index.ts src/cli/bin.ts src/adapters/claude/index.ts src/adapters/cursor/index.ts src/adapters/cline/index.ts src/adapters/gemini/index.ts --dts --format esm --clean --out-dir dist",
|
|
30
37
|
"prepublishOnly": "bun test && tsc --noEmit && bun run build"
|
|
31
38
|
},
|
|
32
39
|
"publishConfig": {
|