@fusengine/harness 0.1.27 → 0.1.29

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,347 +0,0 @@
1
- import { r as projectLayout } from "./layout-C0jaaCQC.mjs";
2
- import { j as detectFramework, n as FAIL_CLOSED, t as evaluate } from "./evaluate-BIK60lOR.mjs";
3
- import { c as evaluateApex, i as detectCreationIntent, r as capVerbosity } from "./verbosity-CXpf3aQQ.mjs";
4
- import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
5
- import { a as extractText, r as cacheStore, t as cacheLookup } from "./store-DeIsfMg5.mjs";
6
- import { t as loadRefs } from "./loader-CyAoJv2W.mjs";
7
- 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-BnHpq2ZB.mjs";
8
- import { join } from "node:path";
9
- import { existsSync, readFileSync } from "node:fs";
10
- import { tmpdir } from "node:os";
11
- //#region src/runtime/activity.ts
12
- /** Min response length (chars) for a lead agent call to count as `sufficient`. */
13
- const AGENT_QUALITY_MIN = 500;
14
- /** Read tools across harnesses (Claude `Read`, Gemini/Cline `read_file`, …). */
15
- const READ_TOOLS = /* @__PURE__ */ new Set([
16
- "Read",
17
- "read_file",
18
- "read_many_files"
19
- ]);
20
- /**
21
- * Map a live tool-use to the activity to record, or null when nothing is
22
- * tracked. Works across harnesses — tool names are globally distinct:
23
- * - MCP doc calls (`context7` / `exa`, any separator) → `doc`
24
- * - `Task` + `subagent_type` (Claude/Cursor) → `agent` (bare agent name)
25
- * - a read tool opening a `.md` reference → `ref`
26
- */
27
- function activityFor(event) {
28
- if (/context7|exa/i.test(event.tool)) return {
29
- kind: "doc",
30
- framework: event.framework,
31
- sessionId: event.sessionId,
32
- source: /exa/i.test(event.tool) ? "exa" : "context7"
33
- };
34
- if (event.tool === "Task") {
35
- const name = String(event.input?.subagent_type ?? "").split(":").pop() ?? "";
36
- if (!name) return null;
37
- const quality = event.responseLength === void 0 ? void 0 : event.responseLength > AGENT_QUALITY_MIN ? "sufficient" : "insufficient";
38
- return quality ? {
39
- kind: "agent",
40
- name,
41
- ts: event.now,
42
- quality
43
- } : {
44
- kind: "agent",
45
- name,
46
- ts: event.now
47
- };
48
- }
49
- if (READ_TOOLS.has(event.tool)) {
50
- const path = String(event.input?.file_path ?? event.input?.path ?? "");
51
- if (path.endsWith(".md")) return {
52
- kind: "ref",
53
- path
54
- };
55
- }
56
- return null;
57
- }
58
- //#endregion
59
- //#region src/runtime/gate.ts
60
- /** Prior agents the freshness gate requires before a code edit. */
61
- const REQUIRED_AGENTS = ["explore-codebase", "research-expert"];
62
- /** Default freshness window for {@link REQUIRED_AGENTS} (2 min — matches the plugin's `FUSE_ENFORCE_TTL_SEC` default). */
63
- const DEFAULT_WINDOW_MS = 12e4;
64
- /** Trivial edits allowed within the window before the full APEX gates apply. */
65
- const TRIVIAL_BUDGET = 4;
66
- /** Line count of the existing on-disk file (undefined if absent/unreadable). */
67
- function existingLineCount(path) {
68
- if (!path) return void 0;
69
- try {
70
- return existsSync(path) ? readFileSync(path, "utf8").split("\n").length : void 0;
71
- } catch {
72
- return;
73
- }
74
- }
75
- /**
76
- * Full gate: the stateless guards (file-size, git, security...) first, then a
77
- * trivial-edit fast path, then the stateful APEX gates fed from the session
78
- * track. Returns the first blocking prompt, or null to allow.
79
- */
80
- async function gate(input) {
81
- let quick;
82
- try {
83
- quick = evaluate({
84
- tool: input.tool,
85
- filePath: input.filePath,
86
- content: input.content,
87
- command: input.command,
88
- agentType: input.agentType,
89
- existingLines: existingLineCount(input.filePath)
90
- });
91
- } catch {
92
- return FAIL_CLOSED;
93
- }
94
- if (quick.decision !== "allow" && quick.prompt) return quick.prompt;
95
- if (!input.filePath) return null;
96
- const window = input.windowMs ?? 12e4;
97
- const track = await loadTrack(input.trackFile);
98
- const lineCount = input.content === void 0 ? Number.POSITIVE_INFINITY : input.content.split("\n").length;
99
- if (!input.isReplaceAll && lineCount < 5 && trivialCount(track, window, input.now) < 4) {
100
- await saveTrack(input.trackFile, recordTrivialEdit(track, input.now, window, input.now));
101
- return null;
102
- }
103
- const ctx = {
104
- sessionId: input.sessionId,
105
- framework: input.framework,
106
- filePath: input.filePath,
107
- content: input.content ?? "",
108
- authorizations: track.authorizations,
109
- refs: input.refs,
110
- refsRead: track.refsRead,
111
- agentsFresh: agentsFresh(track, [...REQUIRED_AGENTS], window, input.now),
112
- brainstormRequired: track.brainstormRequired,
113
- brainstormFresh: agentsFresh(track, ["brainstorming"], window, input.now)
114
- };
115
- try {
116
- return evaluateApex(ctx);
117
- } catch {
118
- return FAIL_CLOSED;
119
- }
120
- }
121
- //#endregion
122
- //#region src/runtime/mcp.ts
123
- /** Default freshness for cached MCP/WebFetch results (48h). */
124
- const MCP_TTL_MS = 1728e5;
125
- /** MCP doc tools + WebFetch whose calls are cached / verbosity-capped. */
126
- function isMcpTool(tool) {
127
- return /context7|exa|webfetch|web_fetch/i.test(tool) || tool === "WebFetch";
128
- }
129
- /** The query/url that keys the cache. */
130
- function queryOf(input) {
131
- const q = input.query ?? input.url ?? input.libraryId ?? "";
132
- return typeof q === "string" ? q : JSON.stringify(q);
133
- }
134
- function denyWith(id, content) {
135
- if (id === "claude-code" || id === "codex") return JSON.stringify({ hookSpecificOutput: {
136
- hookEventName: "PreToolUse",
137
- permissionDecision: "deny",
138
- permissionDecisionReason: content
139
- } });
140
- if (id === "gemini-cli") return JSON.stringify({
141
- decision: "deny",
142
- reason: content
143
- });
144
- return "";
145
- }
146
- function mutateWith(id, input) {
147
- if (id === "claude-code" || id === "codex") return JSON.stringify({ hookSpecificOutput: {
148
- hookEventName: "PreToolUse",
149
- permissionDecision: "allow",
150
- updatedInput: input
151
- } });
152
- if (id === "gemini-cli") return JSON.stringify({ hookSpecificOutput: { tool_input: input } });
153
- return "";
154
- }
155
- /** The doc provider a served cache-hit satisfies (`exa`/`context7`), else undefined. */
156
- function docSourceOf(tool) {
157
- if (/exa/i.test(tool)) return "exa";
158
- if (/context7/i.test(tool)) return "context7";
159
- }
160
- /**
161
- * Pre-event MCP interception: serve a fresh cache hit (deny + cached content),
162
- * else cap exa verbosity (allow + mutated input), else null to allow normally.
163
- * Harnesses without input-mutation/cache support fall through to null.
164
- */
165
- function mcpPreIntercept(id, tool, input, dir, ttlMs, now) {
166
- if (!isMcpTool(tool)) return null;
167
- const cached = cacheLookup(dir, tool, queryOf(input), ttlMs, now);
168
- if (cached) {
169
- const served = denyWith(id, cached);
170
- if (served) return {
171
- stdout: served,
172
- docSource: docSourceOf(tool)
173
- };
174
- }
175
- const capped = capVerbosity(tool, input);
176
- if (capped) {
177
- const mutated = mutateWith(id, capped);
178
- if (mutated) return { stdout: mutated };
179
- }
180
- return null;
181
- }
182
- /** Post-event: store the MCP/WebFetch response (extracted to markdown) in the cache. */
183
- function mcpPostStore(tool, input, response, dir) {
184
- if (!isMcpTool(tool)) return;
185
- cacheStore(dir, tool, queryOf(input), extractText(response));
186
- }
187
- //#endregion
188
- //#region src/runtime/normalize.ts
189
- function str(v) {
190
- return typeof v === "string" ? v : void 0;
191
- }
192
- /**
193
- * Normalize a harness hook payload into a uniform event. Handles Cline's nested
194
- * `preToolUse`/`postToolUse` shape and the top-level `tool_name`/`tool_input`
195
- * shape used by Claude, Codex, Gemini, and Cursor.
196
- */
197
- function normalizeEvent(id, payload) {
198
- if (id === "cline") {
199
- const post = payload.postToolUse;
200
- const node = post ?? payload.preToolUse ?? {};
201
- const params = node.parameters ?? {};
202
- return {
203
- phase: post ? "post" : "pre",
204
- tool: str(node.toolName) ?? "",
205
- input: params,
206
- sessionId: str(payload.taskId) ?? "",
207
- filePath: str(params.path),
208
- content: str(params.content),
209
- command: str(params.command)
210
- };
211
- }
212
- const event = str(payload.hook_event_name) ?? "";
213
- const input = payload.tool_input ?? payload;
214
- return {
215
- phase: /post|after/i.test(event) ? "post" : "pre",
216
- tool: str(payload.tool_name) ?? "",
217
- input,
218
- sessionId: str(payload.session_id) ?? str(payload.conversation_id) ?? "",
219
- filePath: str(input.file_path) ?? str(input.path) ?? str(payload.file_path),
220
- content: str(input.content) ?? str(input.new_string),
221
- command: str(input.command) ?? str(payload.command),
222
- agentType: str(payload.agent_type) ?? str(input.subagent_type)
223
- };
224
- }
225
- //#endregion
226
- //#region src/runtime/paths.ts
227
- /** Path to a session's track file (under a per-tool base dir). */
228
- function trackFile(sessionId, baseDir = join(tmpdir(), "fuse-harness")) {
229
- return join(baseDir, `track-${sessionId.replace(/[^A-Za-z0-9_-]/g, "_") || "default"}.json`);
230
- }
231
- //#endregion
232
- //#region src/runtime/record.ts
233
- /** Apply an activity to a session's track and persist it (PostToolUse path). */
234
- async function recordActivity(file, activity) {
235
- const track = await loadTrack(file);
236
- await saveTrack(file, activity.kind === "agent" ? recordAgent(track, activity.name, activity.ts, activity.quality) : activity.kind === "doc" ? recordDoc(track, activity.framework, activity.sessionId, activity.source) : recordRefRead(track, activity.path));
237
- }
238
- //#endregion
239
- //#region src/runtime/respond.ts
240
- /**
241
- * Map a portable {@link Prompt} to a harness's native hook response. `block`
242
- * denies; anything else asks/injects context. (Codex/Cursor parse but ignore
243
- * `ask` — they only honor deny.)
244
- */
245
- function respond(id, prompt) {
246
- const message = formatPrompt(prompt);
247
- const deny = prompt.kind === "block";
248
- switch (id) {
249
- case "claude-code":
250
- case "codex": return JSON.stringify({ hookSpecificOutput: {
251
- hookEventName: "PreToolUse",
252
- permissionDecision: deny ? "deny" : "ask",
253
- permissionDecisionReason: message
254
- } });
255
- case "gemini-cli": return JSON.stringify(deny ? {
256
- decision: "deny",
257
- reason: message
258
- } : { hookSpecificOutput: { additionalContext: message } });
259
- case "cursor": return JSON.stringify({
260
- permission: deny ? "deny" : "ask",
261
- continue: false,
262
- userMessage: message,
263
- agentMessage: message
264
- });
265
- case "cline": return JSON.stringify(deny ? {
266
- cancel: true,
267
- errorMessage: message
268
- } : { contextModification: message });
269
- default: return "";
270
- }
271
- }
272
- //#endregion
273
- //#region src/runtime/handle.ts
274
- /**
275
- * The full hook handler: on a PRE event it gates the tool-use (stateless guards
276
- * then APEX gates from the session track) and returns the native response; on a
277
- * POST event it records the activity into the track. The loop that makes the
278
- * package behave like the Claude plugin, on any harness.
279
- */
280
- async function handleHook(id, payload, opts) {
281
- const event = normalizeEvent(id, payload);
282
- const layout = projectLayout(opts.cwd);
283
- const file = trackFile(event.sessionId, layout.trackDir);
284
- const mcpDir = layout.cacheDir;
285
- const framework = detectFramework(event.filePath ?? "", event.content ?? "");
286
- const userPrompt = typeof payload.prompt === "string" ? payload.prompt : void 0;
287
- if (userPrompt !== void 0) {
288
- await saveTrack(file, recordBrainstormRequired(await loadTrack(file), detectCreationIntent(userPrompt)));
289
- return {
290
- stdout: "",
291
- exit: 0
292
- };
293
- }
294
- if (event.phase === "post") {
295
- const response = payload.tool_response ?? payload.tool_output;
296
- mcpPostStore(event.tool, event.input, response, mcpDir);
297
- const activity = activityFor({
298
- tool: event.tool,
299
- input: event.input,
300
- sessionId: event.sessionId,
301
- framework,
302
- now: opts.now,
303
- responseLength: extractText(response).length
304
- });
305
- if (activity) await recordActivity(file, activity);
306
- return {
307
- stdout: "",
308
- exit: 0
309
- };
310
- }
311
- const intercept = mcpPreIntercept(id, event.tool, event.input, mcpDir, MCP_TTL_MS, opts.now);
312
- if (intercept !== null) {
313
- if (intercept.docSource) await recordActivity(file, {
314
- kind: "doc",
315
- framework,
316
- sessionId: event.sessionId,
317
- source: intercept.docSource
318
- });
319
- return {
320
- stdout: intercept.stdout,
321
- exit: 0
322
- };
323
- }
324
- const prompt = await gate({
325
- sessionId: event.sessionId,
326
- framework,
327
- tool: event.tool,
328
- filePath: event.filePath,
329
- content: event.content,
330
- command: event.command,
331
- refs: opts.refsDir ? await loadRefs(opts.refsDir) : void 0,
332
- isReplaceAll: event.input.replace_all === true,
333
- agentType: event.agentType,
334
- windowMs: opts.windowMs,
335
- now: opts.now,
336
- trackFile: file
337
- });
338
- return prompt ? {
339
- stdout: respond(id, prompt),
340
- exit: 0
341
- } : {
342
- stdout: "",
343
- exit: 0
344
- };
345
- }
346
- //#endregion
347
- export { normalizeEvent as a, mcpPostStore as c, DEFAULT_WINDOW_MS as d, REQUIRED_AGENTS as f, activityFor as h, trackFile as i, mcpPreIntercept as l, gate as m, respond as n, MCP_TTL_MS as o, TRIVIAL_BUDGET as p, recordActivity as r, isMcpTool as s, handleHook as t, queryOf as u };
@@ -1,33 +0,0 @@
1
- import { join } from "node:path";
2
- import { existsSync } from "node:fs";
3
- //#region src/policy/detect-project.ts
4
- /** Keywords that signal a development task (APEX trigger). */
5
- const DEV_KEYWORDS = /\b(implement|create|build|fix|add|refactor|develop|feature|bug|update|modify|change|write|code)\b/i;
6
- /** True when the prompt invokes the /apex command. */
7
- function isApexCommand(prompt) {
8
- return /(?:^|\s)\/apex|\/fuse-ai-pilot:apex/i.test(prompt);
9
- }
10
- /** Detect the project type by scanning config files in `dir`. */
11
- function detectProjectType(dir) {
12
- const has = (f) => existsSync(join(dir, f));
13
- if (has("next.config.js") || has("next.config.ts") || has("next.config.mjs")) return "nextjs";
14
- if (has("nuxt.config.ts") || has("nuxt.config.js")) return "nuxt";
15
- if (has("angular.json")) return "angular";
16
- if (has("svelte.config.js") || has("svelte.config.ts")) return "svelte";
17
- if (has("vite.config.ts") && has("src/App.vue")) return "vue";
18
- if (has("vite.config.ts") || has("vite.config.js")) return "react";
19
- if (has("tailwind.config.js") || has("tailwind.config.ts")) return "tailwind";
20
- if (has("composer.json") && has("artisan")) return "laravel";
21
- if (has("Gemfile") && has("config/routes.rb")) return "rails";
22
- if (has("requirements.txt") || has("pyproject.toml") || has("setup.py")) return has("manage.py") ? "django" : "python";
23
- if (has("go.mod")) return "go";
24
- if (has("Cargo.toml")) return "rust";
25
- if (has("Package.swift")) return "swift";
26
- if (has("pom.xml") || has("build.gradle") || has("build.gradle.kts")) return "java";
27
- if (has("build.sbt")) return "scala";
28
- if (has("mix.exs")) return "elixir";
29
- if (has("Gemfile")) return "ruby";
30
- return "generic";
31
- }
32
- //#endregion
33
- export { detectProjectType as n, isApexCommand as r, DEV_KEYWORDS as t };
@@ -1,98 +0,0 @@
1
- import { r as isDocConsulted, t as formatDocDeny } from "./doc-helpers-Dd_x1-tZ.mjs";
2
- import { t as routeReferences } from "./router-D8cVrI-s.mjs";
3
- //#region src/policy/apex.ts
4
- /** Gate: Context7 + Exa must have been consulted this session. */
5
- const docConsultedGate = (ctx) => isDocConsulted(ctx.authorizations, ctx.sessionId) ? null : {
6
- kind: "block",
7
- title: "APEX: documentation not consulted",
8
- reason: formatDocDeny(ctx.framework),
9
- actions: ["Call mcp__context7__query-docs", "Call mcp__exa__web_search_exa"]
10
- };
11
- /** Gate: the routed SOLID references for this edit must have been read. */
12
- const solidReadGate = (ctx) => {
13
- if (!ctx.refs?.length) return null;
14
- const routed = routeReferences(ctx.refs, ctx.filePath, ctx.content);
15
- if (!routed) return null;
16
- const read = new Set(ctx.refsRead ?? []);
17
- const missing = routed.required.map((r) => r.meta.filePath).filter((p) => !read.has(p));
18
- if (missing.length === 0) return null;
19
- return {
20
- kind: "block",
21
- title: `APEX: read SOLID references for ${ctx.framework}`,
22
- reason: `Read these before editing ${ctx.filePath}:`,
23
- actions: missing
24
- };
25
- };
26
- /** Gate: the required prior agents (explore + research) must have run within the window. */
27
- const freshnessGate = (ctx) => ctx.agentsFresh === false ? {
28
- kind: "block",
29
- title: "APEX: explore + research required",
30
- reason: `Run explore-codebase and research-expert (within the freshness window) before editing ${ctx.framework}.`,
31
- actions: ["Launch the explore-codebase agent", "Launch the research-expert agent"]
32
- } : null;
33
- /** Gate: brainstorming must precede creating new files when flagged. */
34
- const brainstormGate = (ctx) => ctx.brainstormRequired && ctx.brainstormFresh === false ? {
35
- kind: "block",
36
- title: "APEX: brainstorm first",
37
- reason: `Creation intent detected — brainstorm before creating new ${ctx.framework} files.`,
38
- actions: ["Launch the brainstorming agent"]
39
- } : null;
40
- /** Default APEX gate chain (brainstorm, freshness, docs, SOLID refs). */
41
- const APEX_GATES = [
42
- brainstormGate,
43
- freshnessGate,
44
- docConsultedGate,
45
- solidReadGate
46
- ];
47
- /**
48
- * Run the APEX gates (chain-of-responsibility): the first failing gate's prompt
49
- * wins; null means every gate passed (allow).
50
- */
51
- function evaluateApex(ctx, gates = APEX_GATES) {
52
- return gates.reduce((hit, gate) => hit ?? gate(ctx), null);
53
- }
54
- //#endregion
55
- //#region src/policy/creation-intent.ts
56
- const CREATE_RE = /\b(?:create|implement|add|build|new|feature|component|generate|make|develop|scaffold)\b/i;
57
- const SKIP_RE = /\b(?:fix|bug|debug|update|refactor|rename|move|delete|remove|commit|push|edit|modify|change)\b/i;
58
- /**
59
- * True when a prompt expresses creation intent (a new feature/component) and is
60
- * not a fix/refactor — the signal that brainstorming should precede creation.
61
- * The harness calls this on UserPromptSubmit, then `recordBrainstormRequired`.
62
- */
63
- function detectCreationIntent(prompt) {
64
- return CREATE_RE.test(prompt) && !SKIP_RE.test(prompt);
65
- }
66
- //#endregion
67
- //#region src/policy/verbosity.ts
68
- /** Exa MCP tools whose result count + token budget are capped. */
69
- const EXA_TOOLS = /exa__web_search|exa__get_code_context|exa_web_search|exa_get_code_context/i;
70
- /** Context7 doc tool whose token budget is capped. */
71
- const CONTEXT7_TOOLS = /context7__query-docs|context7_query-docs|query-docs/i;
72
- /** Max results an exa MCP call may request. */
73
- const MAX_EXA_RESULTS = 3;
74
- /** Max token budget for exa `tokensNum` / context7 `tokens`. */
75
- const MAX_TOKENS = 2e3;
76
- /**
77
- * Cap an MCP call's verbosity — exa `numResults` ≤ 3 (+ `tokensNum` ≤ 2000),
78
- * Context7 `tokens` ≤ 2000. Returns the capped input (a mutation for the harness
79
- * to apply) when a change is needed, else null.
80
- */
81
- function capVerbosity(tool, input) {
82
- const out = { ...input };
83
- let changed = false;
84
- const cap = (key, max, force) => {
85
- const v = out[key];
86
- if (typeof v === "number" && v > max || force && typeof v !== "number") {
87
- out[key] = max;
88
- changed = true;
89
- }
90
- };
91
- if (EXA_TOOLS.test(tool)) {
92
- cap("numResults", 3, true);
93
- cap("tokensNum", MAX_TOKENS, false);
94
- } else if (CONTEXT7_TOOLS.test(tool)) cap("tokens", MAX_TOKENS, false);
95
- return changed ? out : null;
96
- }
97
- //#endregion
98
- export { APEX_GATES as a, evaluateApex as c, detectCreationIntent as i, freshnessGate as l, MAX_TOKENS as n, brainstormGate as o, capVerbosity as r, docConsultedGate as s, MAX_EXA_RESULTS as t, solidReadGate as u };