@fusengine/harness 0.1.28 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude/index.mjs +1 -1
- package/dist/adapters/cline/index.mjs +1 -1
- package/dist/adapters/codex/index.mjs +1 -1
- package/dist/adapters/cursor/index.mjs +1 -1
- package/dist/adapters/gemini/index.mjs +1 -1
- package/dist/cache/index.mjs +2 -2
- package/dist/{cache-BzbX-ztL.mjs → cache-C9z9LclL.mjs} +1 -31
- package/dist/{claude-phC5Uh_W.mjs → claude-B9FYp0Yw.mjs} +1 -1
- package/dist/cli/bin.mjs +14 -4
- package/dist/cli/index.mjs +1 -1
- package/dist/describe-CPtgUzFS.mjs +1038 -0
- package/dist/{evaluate-CFYPF3re.mjs → evaluate-j3gRJ_ng.mjs} +14 -2
- package/dist/freshness/index.mjs +1 -1
- package/dist/{freshness-CezohJHo.mjs → freshness-otdUpuvP.mjs} +1 -1
- package/dist/handle-USWK4NSE.mjs +2300 -0
- package/dist/index-mISsk0ff.d.mts +438 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +7 -8
- package/dist/{json-io-xpTDuvtn.mjs → json-io-CAn72gI4.mjs} +1 -1
- package/dist/policy/index.d.mts +2 -2
- package/dist/policy/index.mjs +4 -4
- package/dist/policy-la_KkjCS.mjs +1 -0
- package/dist/{run-B8n-H5hA.mjs → run-CXsV-wIJ.mjs} +1 -1
- package/dist/runtime/index.d.mts +454 -8
- package/dist/runtime/index.mjs +2 -2
- package/dist/state/index.mjs +1 -1
- package/dist/{state-Cs0Y0MG_.mjs → state-ByhLeKyD.mjs} +1 -1
- package/dist/{store-BnHpq2ZB.mjs → store-D-ge2ZPI.mjs} +1 -1
- package/dist/{store-DeIsfMg5.mjs → store-PrNPm6So.mjs} +30 -1
- package/dist/tracking/index.mjs +1 -1
- package/package.json +10 -3
- package/dist/handle-DnOw05K8.mjs +0 -347
- package/dist/index-DNAzITvw.d.mts +0 -227
- package/dist/policy-EuVJ_5hS.mjs +0 -33
- package/dist/verbosity-CXpf3aQQ.mjs +0 -98
package/dist/handle-DnOw05K8.mjs
DELETED
|
@@ -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-CFYPF3re.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,227 +0,0 @@
|
|
|
1
|
-
import { t as Prompt } from "./types-D56jSgD9.mjs";
|
|
2
|
-
import { t as AuthEntry } from "./doc-helpers-CG1nuf-c.mjs";
|
|
3
|
-
import { t as RefMeta } from "./types-CY5qT2X1.mjs";
|
|
4
|
-
|
|
5
|
-
//#region src/policy/detect-project.d.ts
|
|
6
|
-
/** Project types detected from filesystem indicators. */
|
|
7
|
-
type ProjectType = "nextjs" | "nuxt" | "angular" | "svelte" | "vue" | "react" | "tailwind" | "laravel" | "rails" | "django" | "python" | "go" | "rust" | "swift" | "java" | "scala" | "elixir" | "ruby" | "generic";
|
|
8
|
-
/** Keywords that signal a development task (APEX trigger). */
|
|
9
|
-
declare const DEV_KEYWORDS: RegExp;
|
|
10
|
-
/** True when the prompt invokes the /apex command. */
|
|
11
|
-
declare function isApexCommand(prompt: string): boolean;
|
|
12
|
-
/** Detect the project type by scanning config files in `dir`. */
|
|
13
|
-
declare function detectProjectType(dir: string): ProjectType;
|
|
14
|
-
//#endregion
|
|
15
|
-
//#region src/policy/detect-framework.d.ts
|
|
16
|
-
/**
|
|
17
|
-
* Detect the framework from a file path extension + content patterns.
|
|
18
|
-
* Aligned with the fusengine require-solid-read detection (distinct from
|
|
19
|
-
* {@link detectProjectType}, which scans config files on disk).
|
|
20
|
-
*/
|
|
21
|
-
declare function detectFramework(filePath: string, content: string): string;
|
|
22
|
-
//#endregion
|
|
23
|
-
//#region src/policy/file-size.d.ts
|
|
24
|
-
/** Verdict from {@link evaluateFileSize}. */
|
|
25
|
-
interface FileSizeVerdict {
|
|
26
|
-
ok: boolean;
|
|
27
|
-
lines: number;
|
|
28
|
-
max: number;
|
|
29
|
-
message: string | null;
|
|
30
|
-
}
|
|
31
|
-
/** Count lines in file content (empty string = 0). */
|
|
32
|
-
declare function countLines(content: string): number;
|
|
33
|
-
/**
|
|
34
|
-
* Evaluate a file's line count against the SOLID limit.
|
|
35
|
-
* @param lines - the file's line count
|
|
36
|
-
* @param max - the limit (defaults to `resolveMaxLines()`)
|
|
37
|
-
*/
|
|
38
|
-
declare function evaluateFileSize(lines: number, max?: number): FileSizeVerdict;
|
|
39
|
-
//#endregion
|
|
40
|
-
//#region src/policy/patterns.d.ts
|
|
41
|
-
/**
|
|
42
|
-
* Guard pattern data, ported verbatim from the fusengine git/install guards.
|
|
43
|
-
* Note (faithful): `git push.*--force` also matches `--force-with-lease` —
|
|
44
|
-
* preserved from the source guard.
|
|
45
|
-
*/
|
|
46
|
-
/** Destructive git operations to block outright. */
|
|
47
|
-
declare const GIT_BLOCKED: ReadonlyArray<RegExp>;
|
|
48
|
-
/** Git operations that warrant a confirmation prompt. */
|
|
49
|
-
declare const GIT_ASK: ReadonlyArray<RegExp>;
|
|
50
|
-
/** System-level package installs (need confirmation). */
|
|
51
|
-
declare const SYSTEM_INSTALL: ReadonlyArray<RegExp>;
|
|
52
|
-
/** Project-level package installs. */
|
|
53
|
-
declare const PROJECT_INSTALL: ReadonlyArray<RegExp>;
|
|
54
|
-
/** True when `cmd` matches any pattern in `patterns`. */
|
|
55
|
-
declare function matchPatterns(cmd: string, patterns: ReadonlyArray<RegExp>): boolean;
|
|
56
|
-
//#endregion
|
|
57
|
-
//#region src/policy/evaluate.d.ts
|
|
58
|
-
/** Harness-agnostic input to {@link evaluate}. */
|
|
59
|
-
interface PolicyContext {
|
|
60
|
-
/** Tool name (e.g. "Write", "Edit", "Bash"). */
|
|
61
|
-
tool: string;
|
|
62
|
-
filePath?: string;
|
|
63
|
-
content?: string;
|
|
64
|
-
command?: string;
|
|
65
|
-
/** Optional override for the SOLID max-lines limit. */
|
|
66
|
-
maxLines?: number;
|
|
67
|
-
/** Subagent type — `Explore`/`Plan` are exempt from the file-size gate. */
|
|
68
|
-
agentType?: string;
|
|
69
|
-
/** Line count of the existing on-disk file (so an Edit on an oversized file blocks). */
|
|
70
|
-
existingLines?: number;
|
|
71
|
-
}
|
|
72
|
-
/** Harness-agnostic policy decision (+ a portable prompt for adapters to render). */
|
|
73
|
-
interface PolicyResult {
|
|
74
|
-
decision: "allow" | "deny" | "warn";
|
|
75
|
-
message: string | null;
|
|
76
|
-
prompt?: Prompt;
|
|
77
|
-
meta?: Record<string, unknown>;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Evaluate a single tool-use against the bundled policies, returning a pure
|
|
81
|
-
* decision plus a portable {@link Prompt}. Adapters translate the prompt into
|
|
82
|
-
* their harness's native response (Claude `permissionDecision`, etc.).
|
|
83
|
-
*/
|
|
84
|
-
declare function evaluate(ctx: PolicyContext): PolicyResult;
|
|
85
|
-
//#endregion
|
|
86
|
-
//#region src/policy/apex.d.ts
|
|
87
|
-
/**
|
|
88
|
-
* Session context for the stateful APEX gates. The harness adapter supplies this
|
|
89
|
-
* (the package owns the gate LOGIC; recording the session activity is the
|
|
90
|
-
* adapter's tracking layer).
|
|
91
|
-
*/
|
|
92
|
-
interface ApexContext {
|
|
93
|
-
sessionId: string;
|
|
94
|
-
framework: string;
|
|
95
|
-
filePath: string;
|
|
96
|
-
content: string;
|
|
97
|
-
/** Doc-consultation authorizations from session state (Context7/Exa). */
|
|
98
|
-
authorizations?: Record<string, AuthEntry>;
|
|
99
|
-
/** Available SOLID references for the framework's skill. */
|
|
100
|
-
refs?: RefMeta[];
|
|
101
|
-
/** Absolute paths of SOLID refs already read this session. */
|
|
102
|
-
refsRead?: string[];
|
|
103
|
-
/** Whether the required prior agents (explore + research) ran within the freshness window. */
|
|
104
|
-
agentsFresh?: boolean;
|
|
105
|
-
/** Whether brainstorming is required for this edit (creation intent on a new file). */
|
|
106
|
-
brainstormRequired?: boolean;
|
|
107
|
-
/** Whether the brainstorming agent ran within the window. */
|
|
108
|
-
brainstormFresh?: boolean;
|
|
109
|
-
}
|
|
110
|
-
/** A single APEX gate: returns a blocking {@link Prompt}, or null to pass. */
|
|
111
|
-
type ApexGate = (ctx: ApexContext) => Prompt | null;
|
|
112
|
-
/** Gate: Context7 + Exa must have been consulted this session. */
|
|
113
|
-
declare const docConsultedGate: ApexGate;
|
|
114
|
-
/** Gate: the routed SOLID references for this edit must have been read. */
|
|
115
|
-
declare const solidReadGate: ApexGate;
|
|
116
|
-
/** Gate: the required prior agents (explore + research) must have run within the window. */
|
|
117
|
-
declare const freshnessGate: ApexGate;
|
|
118
|
-
/** Gate: brainstorming must precede creating new files when flagged. */
|
|
119
|
-
declare const brainstormGate: ApexGate;
|
|
120
|
-
/** Default APEX gate chain (brainstorm, freshness, docs, SOLID refs). */
|
|
121
|
-
declare const APEX_GATES: ReadonlyArray<ApexGate>;
|
|
122
|
-
/**
|
|
123
|
-
* Run the APEX gates (chain-of-responsibility): the first failing gate's prompt
|
|
124
|
-
* wins; null means every gate passed (allow).
|
|
125
|
-
*/
|
|
126
|
-
declare function evaluateApex(ctx: ApexContext, gates?: ReadonlyArray<ApexGate>): Prompt | null;
|
|
127
|
-
//#endregion
|
|
128
|
-
//#region src/policy/guards/context.d.ts
|
|
129
|
-
/** Context handed to every guard in the chain. */
|
|
130
|
-
interface GuardContext {
|
|
131
|
-
tool: string;
|
|
132
|
-
filePath?: string;
|
|
133
|
-
content?: string;
|
|
134
|
-
command?: string;
|
|
135
|
-
}
|
|
136
|
-
/** A single guard: returns a blocking/asking Prompt, or null to continue. */
|
|
137
|
-
type Guard = (ctx: GuardContext) => Prompt | null;
|
|
138
|
-
//#endregion
|
|
139
|
-
//#region src/policy/guards/security.d.ts
|
|
140
|
-
/** Critical patterns that must always be blocked. */
|
|
141
|
-
declare const CRITICAL_PATTERNS: RegExp[];
|
|
142
|
-
/** Patterns that warrant explicit confirmation before running. */
|
|
143
|
-
declare const ASK_PATTERNS: RegExp[];
|
|
144
|
-
/** Guards against dangerous Bash commands (critical → block, sensitive → ask). */
|
|
145
|
-
declare function securityGuard(ctx: GuardContext): Prompt | null;
|
|
146
|
-
//#endregion
|
|
147
|
-
//#region src/policy/guards/protected-path.d.ts
|
|
148
|
-
/** Path fragments that mark a location as internal/generated state (off-limits to Write/Edit). */
|
|
149
|
-
declare const PROTECTED_FRAGMENTS: readonly string[];
|
|
150
|
-
/** Blocks direct edits to internal/generated state directories. */
|
|
151
|
-
declare function protectedPathGuard(ctx: GuardContext): Prompt | null;
|
|
152
|
-
//#endregion
|
|
153
|
-
//#region src/policy/guards/bash-write.d.ts
|
|
154
|
-
/** Redirect (`>`/`>>`) targeting a code-file extension. */
|
|
155
|
-
declare const CODE_REDIRECT: RegExp;
|
|
156
|
-
/** Interpreters / tools that mutate source in place, plus heredoc-into-file. */
|
|
157
|
-
declare const CODE_MUTATORS: RegExp;
|
|
158
|
-
/** Redirect to a non-code file, or other ambiguous file writers (ASK). */
|
|
159
|
-
declare const ASK_WRITERS: RegExp;
|
|
160
|
-
/**
|
|
161
|
-
* Blocks shell commands that mutate code files in place (and heredocs/redirects
|
|
162
|
-
* to source files); asks before other file-writing shell commands. Forces use
|
|
163
|
-
* of the Write/Edit tool so APEX/SOLID checks are not bypassed.
|
|
164
|
-
*/
|
|
165
|
-
declare function bashWriteGuard(ctx: GuardContext): Prompt | null;
|
|
166
|
-
//#endregion
|
|
167
|
-
//#region src/policy/guards/interface-separation.d.ts
|
|
168
|
-
/** TS/JS component files: top-level `interface`/`type Foo`. */
|
|
169
|
-
declare const TS_DECL_RE: RegExp;
|
|
170
|
-
/** Python view models: class subclassing a schema/protocol base. */
|
|
171
|
-
declare const PY_MODEL_RE: RegExp;
|
|
172
|
-
/** PHP controllers: top-level `interface` / `abstract class`. */
|
|
173
|
-
declare const PHP_DECL_RE: RegExp;
|
|
174
|
-
/** Swift views: top-level `protocol Foo`. */
|
|
175
|
-
declare const SWIFT_PROTO_RE: RegExp;
|
|
176
|
-
/** Go handlers/controllers: top-level `type Foo interface`. */
|
|
177
|
-
declare const GO_DECL_RE: RegExp;
|
|
178
|
-
/** Java/Kotlin controllers/handlers: top-level `interface`/`record`. */
|
|
179
|
-
declare const JAVA_DECL_RE: RegExp;
|
|
180
|
-
/**
|
|
181
|
-
* Blocks top-level interface/type/protocol declarations in component, view or
|
|
182
|
-
* controller files (Interface Segregation). Fires only when BOTH the path
|
|
183
|
-
* category AND the content pattern match.
|
|
184
|
-
*/
|
|
185
|
-
declare function interfaceSeparationGuard(ctx: GuardContext): Prompt | null;
|
|
186
|
-
//#endregion
|
|
187
|
-
//#region src/policy/guards/install.d.ts
|
|
188
|
-
/** Asks for confirmation before a dependency or system package install. */
|
|
189
|
-
declare function installGuard(ctx: GuardContext): Prompt | null;
|
|
190
|
-
//#endregion
|
|
191
|
-
//#region src/policy/guards/index.d.ts
|
|
192
|
-
/** Ordered guard chain: critical/security + protected first, then writes/installs. */
|
|
193
|
-
declare const GUARDS: ReadonlyArray<Guard>;
|
|
194
|
-
/** Block prompt returned when a guard or gate throws (fail-closed). */
|
|
195
|
-
declare const FAIL_CLOSED: Prompt;
|
|
196
|
-
/** Register a user guard — runs AFTER the privileged core chain (two-tier). */
|
|
197
|
-
declare function registerGuard(guard: Guard): void;
|
|
198
|
-
/** Remove all registered user guards (mainly for tests). */
|
|
199
|
-
declare function clearUserGuards(): void;
|
|
200
|
-
/**
|
|
201
|
-
* Run the guard chain — privileged core guards first, then user guards — and
|
|
202
|
-
* return the first firing Prompt, else null. Fail-closed: a guard that throws
|
|
203
|
-
* blocks (never silently passes).
|
|
204
|
-
*/
|
|
205
|
-
declare function runGuards(ctx: GuardContext): Prompt | null;
|
|
206
|
-
//#endregion
|
|
207
|
-
//#region src/policy/creation-intent.d.ts
|
|
208
|
-
/**
|
|
209
|
-
* True when a prompt expresses creation intent (a new feature/component) and is
|
|
210
|
-
* not a fix/refactor — the signal that brainstorming should precede creation.
|
|
211
|
-
* The harness calls this on UserPromptSubmit, then `recordBrainstormRequired`.
|
|
212
|
-
*/
|
|
213
|
-
declare function detectCreationIntent(prompt: string): boolean;
|
|
214
|
-
//#endregion
|
|
215
|
-
//#region src/policy/verbosity.d.ts
|
|
216
|
-
/** Max results an exa MCP call may request. */
|
|
217
|
-
declare const MAX_EXA_RESULTS = 3;
|
|
218
|
-
/** Max token budget for exa `tokensNum` / context7 `tokens`. */
|
|
219
|
-
declare const MAX_TOKENS = 2e3;
|
|
220
|
-
/**
|
|
221
|
-
* Cap an MCP call's verbosity — exa `numResults` ≤ 3 (+ `tokensNum` ≤ 2000),
|
|
222
|
-
* Context7 `tokens` ≤ 2000. Returns the capped input (a mutation for the harness
|
|
223
|
-
* to apply) when a change is needed, else null.
|
|
224
|
-
*/
|
|
225
|
-
declare function capVerbosity(tool: string, input: Record<string, unknown>): Record<string, unknown> | null;
|
|
226
|
-
//#endregion
|
|
227
|
-
export { ApexContext as A, GIT_ASK as B, protectedPathGuard as C, Guard as D, securityGuard as E, freshnessGate as F, FileSizeVerdict as G, PROJECT_INSTALL as H, solidReadGate as I, detectFramework as J, countLines as K, PolicyContext as L, brainstormGate as M, docConsultedGate as N, GuardContext as O, evaluateApex as P, isApexCommand as Q, PolicyResult as R, PROTECTED_FRAGMENTS as S, CRITICAL_PATTERNS as T, SYSTEM_INSTALL as U, GIT_BLOCKED as V, matchPatterns as W, ProjectType as X, DEV_KEYWORDS as Y, detectProjectType as Z, interfaceSeparationGuard as _, FAIL_CLOSED as a, CODE_REDIRECT as b, registerGuard as c, GO_DECL_RE as d, JAVA_DECL_RE as f, TS_DECL_RE as g, SWIFT_PROTO_RE as h, detectCreationIntent as i, ApexGate as j, APEX_GATES as k, runGuards as l, PY_MODEL_RE as m, MAX_TOKENS as n, GUARDS as o, PHP_DECL_RE as p, evaluateFileSize as q, capVerbosity as r, clearUserGuards as s, MAX_EXA_RESULTS as t, installGuard as u, ASK_WRITERS as v, ASK_PATTERNS as w, bashWriteGuard as x, CODE_MUTATORS as y, evaluate as z };
|
package/dist/policy-EuVJ_5hS.mjs
DELETED
|
@@ -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 };
|