@fusengine/harness 0.1.28 → 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.
- 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/{claude-phC5Uh_W.mjs → claude-B9FYp0Yw.mjs} +1 -1
- package/dist/cli/bin.mjs +2 -2
- package/dist/cli/index.mjs +1 -1
- package/dist/{evaluate-CFYPF3re.mjs → evaluate-j3gRJ_ng.mjs} +14 -2
- package/dist/handle-nu3GYVek.mjs +1090 -0
- package/dist/{index-DNAzITvw.d.mts → index-BqdCjaT9.d.mts} +71 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +3 -4
- 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 +33 -8
- package/dist/runtime/index.mjs +2 -2
- package/dist/skill-triggers-BZxov1es.mjs +676 -0
- package/package.json +1 -1
- package/dist/handle-DnOw05K8.mjs +0 -347
- 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 };
|
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 };
|
|
@@ -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 };
|