@fusengine/harness 0.1.37 → 0.1.40
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.d.mts +2 -2
- package/dist/adapters/claude/index.mjs +2 -2
- package/dist/adapters/cline/index.mjs +1 -1
- package/dist/adapters/codex/index.d.mts +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.d.mts +2 -2
- package/dist/cache/index.mjs +3 -3
- package/dist/cache-la_KkjCS.mjs +1 -0
- package/dist/{claude-3PqBGt_7.mjs → claude-BnFrSfMo.mjs} +17 -3
- package/dist/cli/bin.mjs +4 -3
- package/dist/cli/index.mjs +1 -1
- package/dist/config/index.d.mts +2 -2
- package/dist/config/index.mjs +3 -2
- package/dist/{doc-helpers-Dd_x1-tZ.mjs → doc-helpers-B4XYL1v8.mjs} +8 -6
- package/dist/{doc-helpers-CG1nuf-c.d.mts → doc-helpers-D14nkD5D.d.mts} +5 -3
- package/dist/{dotenv-TLNMiSjP.mjs → dotenv-DGyLln7U.mjs} +1 -20
- package/dist/{evaluate-zyxeVZPB.mjs → evaluate-B1n1ti0N.mjs} +89 -50
- package/dist/freshness/index.d.mts +1 -1
- package/dist/freshness/index.mjs +1 -1
- package/dist/{handle-DL2sZiWH.mjs → handle-gYSq5znf.mjs} +587 -153
- package/dist/index-BX-xcvhY.d.mts +75 -0
- package/dist/{index-DLUfyYmU.d.mts → index-COBvvc3L.d.mts} +3 -1
- package/dist/{index-FrWgmkbP.d.mts → index-CwOdFBOr.d.mts} +11 -2
- package/dist/{index-CPoF_hLP.d.mts → index-D7GpOmkl.d.mts} +1 -1
- package/dist/{index-QzK2dv0V.d.mts → index-DTrjSNmI.d.mts} +38 -13
- package/dist/{index-DL8MxjuP.d.mts → index-mmRF3KNp.d.mts} +4 -4
- package/dist/index.d.mts +7 -7
- package/dist/index.mjs +9 -9
- package/dist/{loader-CyAoJv2W.mjs → loader-Bn-DbZmt.mjs} +1 -1
- package/dist/mcp-store-CDUVqtJ0.mjs +238 -0
- package/dist/policy/index.d.mts +2 -2
- package/dist/policy/index.mjs +3 -3
- package/dist/refs/index.d.mts +1 -1
- package/dist/refs/index.mjs +2 -2
- package/dist/{router-D8cVrI-s.mjs → router-BfX0hJg8.mjs} +13 -20
- package/dist/{run-CUL70W0k.mjs → run-D-Ydrw3D.mjs} +1 -1
- package/dist/runtime/index.d.mts +32 -12
- package/dist/runtime/index.mjs +2 -2
- package/dist/{session-state-DxNkIBZ2.d.mts → session-state-Dzq6yrw7.d.mts} +1 -1
- package/dist/state/index.d.mts +1 -1
- package/dist/{store-7j02oGjt.mjs → store-3WBr37Xz.mjs} +1 -1
- package/dist/tracking/index.d.mts +1 -1
- package/dist/tracking/index.mjs +1 -1
- package/dist/ttl-BG55s6HZ.mjs +20 -0
- package/dist/{validate-DLrWtaDR.mjs → validate-BCRqp9XG.mjs} +15 -9
- package/package.json +1 -1
- package/dist/cache-C9z9LclL.mjs +0 -56
- package/dist/index-DPkCX_AR.d.mts +0 -40
- package/dist/store-PrNPm6So.mjs +0 -89
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { r as resolveMaxLines } from "./limits-CHn8AIL1.mjs";
|
|
2
2
|
import { r as projectLayout } from "./layout-C0jaaCQC.mjs";
|
|
3
|
+
import { t as HOME_DIR } from "./dotenv-DGyLln7U.mjs";
|
|
3
4
|
import { i as walkUpFor, n as projectRoot, r as projectRootOrNull, t as isCodeFile } from "./project-root-3kk7gCOp.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { t as detectHarness } from "./harness-C8Nxxyn_.mjs";
|
|
6
|
+
import { A as evaluateApex, E as detectCreationIntent, L as requiredArchSkill, P as detectModularArchitecture, S as frameworkSolidGate, T as capVerbosity, _ as buildClaudeMdContext, b as skillTriggerGate, c as parseBodyDesc, d as PROJECT_INDICATORS, l as parseField, n as missingSeoElements, o as parseEnrichment, p as buildApexTaskInjection, r as descFromText, s as parseEntry, t as isHtmlLike, u as EXCLUDE_DIRS$1 } from "./validate-BCRqp9XG.mjs";
|
|
7
|
+
import { N as detectFramework, j as countLines, n as FAIL_CLOSED, t as evaluate } from "./evaluate-B1n1ti0N.mjs";
|
|
8
|
+
import { r as isDocConsulted } from "./doc-helpers-B4XYL1v8.mjs";
|
|
6
9
|
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
7
10
|
import { a as nowStamp, l as throttleMs, n as readRoots, o as readState, s as setStateField, t as addRoot } from "./registry-BkoEbdec.mjs";
|
|
8
|
-
import {
|
|
11
|
+
import { c as extractText, i as cacheLookupSubstring, l as loadIndex, n as webfetchCacheWrite, r as cacheLookup, t as mcpCacheWrite } from "./mcp-store-CDUVqtJ0.mjs";
|
|
9
12
|
import { a as writeJsonFile, i as readJsonFile, r as hashText, t as atomicWrite } from "./json-io-CvSumjtz.mjs";
|
|
10
|
-
import { t as loadRefs } from "./loader-
|
|
11
|
-
import {
|
|
12
|
-
import { c as
|
|
13
|
+
import { t as loadRefs } from "./loader-Bn-DbZmt.mjs";
|
|
14
|
+
import { c as recordDoc, d as trivialCount, i as agentsFresh, l as recordRefRead, n as saveTrack, o as recordAgent, r as verifyTrack, s as recordBrainstormRequired, t as loadTrack, u as recordTrivialEdit } from "./store-3WBr37Xz.mjs";
|
|
15
|
+
import { c as collectFiles, d as sleep, f as spawnCapture, l as pathExists, n as contextResponse, p as writeText, t as blockResponse, u as readText } from "./claude-BnFrSfMo.mjs";
|
|
13
16
|
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
14
17
|
import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
15
18
|
import { homedir } from "node:os";
|
|
@@ -98,20 +101,117 @@ function saveSecurityState(state, now = Date.now(), home = homedir()) {
|
|
|
98
101
|
writeFileSync(securityStatePath(now, home), JSON.stringify(state, null, 2), "utf-8");
|
|
99
102
|
}
|
|
100
103
|
//#endregion
|
|
104
|
+
//#region src/freshness/explore-tools.ts
|
|
105
|
+
/** Native exploration tools (parity `apex_constants.EXPLORE_TOOLS`). */
|
|
106
|
+
const EXPLORE_TOOLS = /* @__PURE__ */ new Set(["Glob", "Grep"]);
|
|
107
|
+
/** Research tools — MCP docs + web (parity `apex_constants.RESEARCH_TOOLS`). */
|
|
108
|
+
const RESEARCH_TOOLS = /* @__PURE__ */ new Set([
|
|
109
|
+
"mcp__context7__query-docs",
|
|
110
|
+
"mcp__context7__resolve-library-id",
|
|
111
|
+
"mcp__exa__web_search_exa",
|
|
112
|
+
"mcp__exa__get_code_context_exa",
|
|
113
|
+
"mcp__exa__deep_researcher_start",
|
|
114
|
+
"WebSearch",
|
|
115
|
+
"WebFetch"
|
|
116
|
+
]);
|
|
117
|
+
/** Bash executables that count as exploration (parity `EXPLORE_BASH_CMDS`). */
|
|
118
|
+
const EXPLORE_BASH_CMDS = /* @__PURE__ */ new Set([
|
|
119
|
+
"grep",
|
|
120
|
+
"rg",
|
|
121
|
+
"find",
|
|
122
|
+
"ls",
|
|
123
|
+
"fd",
|
|
124
|
+
"ast-grep",
|
|
125
|
+
"tree",
|
|
126
|
+
"cat",
|
|
127
|
+
"head",
|
|
128
|
+
"tail"
|
|
129
|
+
]);
|
|
130
|
+
/** Cached MCP result reads count as research (parity `CACHE_READ_RE` + doc-helpers). */
|
|
131
|
+
const CACHE_READ_RE = /\/context\/mcp\/(exa-search|exa-code-context|context7)-/;
|
|
132
|
+
/**
|
|
133
|
+
* First non-assignment shell token's basename, or "" — mirrors Python
|
|
134
|
+
* `_bash_executable` (skips leading `VAR=value` env prefixes, e.g. `FOO=1 grep`).
|
|
135
|
+
* @param cmd - Raw Bash `command` string.
|
|
136
|
+
* @returns The executable basename, or "" when none.
|
|
137
|
+
*/
|
|
138
|
+
function bashExecutable(cmd) {
|
|
139
|
+
for (const token of cmd.trim().split(/\s+/)) {
|
|
140
|
+
if (!token) continue;
|
|
141
|
+
const last = token.split("/").pop() ?? token;
|
|
142
|
+
if (!last.includes("=")) return last;
|
|
143
|
+
}
|
|
144
|
+
return "";
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Classify a direct tool use into an APEX phase, or `null` when it is neither
|
|
148
|
+
* exploration nor research. Mirrors Python `track-subagent-research._classify`.
|
|
149
|
+
* @param tool - Harness tool name (e.g. "Glob", "Bash", "WebSearch").
|
|
150
|
+
* @param input - Tool input payload.
|
|
151
|
+
* @returns The credited phase + cache flag, or `null`.
|
|
152
|
+
*/
|
|
153
|
+
function classifyExplore(tool, input) {
|
|
154
|
+
if (RESEARCH_TOOLS.has(tool)) return {
|
|
155
|
+
phase: "research-expert",
|
|
156
|
+
cacheHit: false
|
|
157
|
+
};
|
|
158
|
+
if (EXPLORE_TOOLS.has(tool)) return {
|
|
159
|
+
phase: "explore-codebase",
|
|
160
|
+
cacheHit: false
|
|
161
|
+
};
|
|
162
|
+
if (tool === "Read") {
|
|
163
|
+
const path = String(input?.file_path ?? input?.path ?? "");
|
|
164
|
+
if (path && CACHE_READ_RE.test(path)) return {
|
|
165
|
+
phase: "research-expert",
|
|
166
|
+
cacheHit: true
|
|
167
|
+
};
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
if (tool === "Bash") {
|
|
171
|
+
const cmd = String(input?.command ?? "").trim();
|
|
172
|
+
if (EXPLORE_BASH_CMDS.has(bashExecutable(cmd))) return {
|
|
173
|
+
phase: "explore-codebase",
|
|
174
|
+
cacheHit: false
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
101
180
|
//#region src/runtime/activity.ts
|
|
102
181
|
/** Min response length (chars) for a lead agent call to count as `sufficient`. */
|
|
103
182
|
const AGENT_QUALITY_MIN = 500;
|
|
183
|
+
/** Min response length (chars) for direct exploration/research (parity Python `> 50`). */
|
|
184
|
+
const EXPLORE_QUALITY_MIN = 50;
|
|
104
185
|
/** Read tools across harnesses (Claude `Read`, Gemini/Cline `read_file`, …). */
|
|
105
186
|
const READ_TOOLS = /* @__PURE__ */ new Set([
|
|
106
187
|
"Read",
|
|
107
188
|
"read_file",
|
|
108
189
|
"read_many_files"
|
|
109
190
|
]);
|
|
191
|
+
/** Grade a response length against a threshold; `undefined` when length is unknown. */
|
|
192
|
+
function qualityFor(len, min) {
|
|
193
|
+
return len === void 0 ? void 0 : len > min ? "sufficient" : "insufficient";
|
|
194
|
+
}
|
|
195
|
+
/** Build an `agent` activity, attaching `quality` only when graded. */
|
|
196
|
+
function agentActivity(name, ts, quality) {
|
|
197
|
+
return quality ? {
|
|
198
|
+
kind: "agent",
|
|
199
|
+
name,
|
|
200
|
+
ts,
|
|
201
|
+
quality
|
|
202
|
+
} : {
|
|
203
|
+
kind: "agent",
|
|
204
|
+
name,
|
|
205
|
+
ts
|
|
206
|
+
};
|
|
207
|
+
}
|
|
110
208
|
/**
|
|
111
209
|
* Map a live tool-use to the activity to record, or null when nothing is
|
|
112
210
|
* tracked. Works across harnesses — tool names are globally distinct:
|
|
113
211
|
* - MCP doc calls (`context7` / `exa`, any separator) → `doc`
|
|
114
|
-
* - `Task` + `subagent_type` (Claude/Cursor) → `agent` (bare agent name)
|
|
212
|
+
* - `Task`/`Agent` + `subagent_type` (Claude/Cursor) → `agent` (bare agent name)
|
|
213
|
+
* - direct exploration/research (Glob/Grep, explore Bash, web, MCP cache reads)
|
|
214
|
+
* → `agent` credited to the matching REQUIRED_AGENTS phase
|
|
115
215
|
* - a read tool opening a `.md` reference → `ref`
|
|
116
216
|
*/
|
|
117
217
|
function activityFor(event) {
|
|
@@ -121,20 +221,15 @@ function activityFor(event) {
|
|
|
121
221
|
sessionId: event.sessionId,
|
|
122
222
|
source: /exa/i.test(event.tool) ? "exa" : "context7"
|
|
123
223
|
};
|
|
124
|
-
if (event.tool === "Task") {
|
|
125
|
-
const name = String(event.input?.subagent_type ?? "").split(":").pop() ?? "";
|
|
224
|
+
if (event.tool === "Task" || event.tool === "Agent") {
|
|
225
|
+
const name = String(event.input?.subagent_type ?? event.input?.name ?? "").split(":").pop() ?? "";
|
|
126
226
|
if (!name) return null;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
} : {
|
|
134
|
-
kind: "agent",
|
|
135
|
-
name,
|
|
136
|
-
ts: event.now
|
|
137
|
-
};
|
|
227
|
+
return agentActivity(name, event.now, qualityFor(event.responseLength, AGENT_QUALITY_MIN));
|
|
228
|
+
}
|
|
229
|
+
const hit = classifyExplore(event.tool, event.input);
|
|
230
|
+
if (hit) {
|
|
231
|
+
const quality = hit.cacheHit ? "sufficient" : qualityFor(event.responseLength, EXPLORE_QUALITY_MIN);
|
|
232
|
+
return agentActivity(hit.phase, event.now, quality);
|
|
138
233
|
}
|
|
139
234
|
if (READ_TOOLS.has(event.tool)) {
|
|
140
235
|
const path = String(event.input?.file_path ?? event.input?.path ?? "");
|
|
@@ -146,9 +241,18 @@ function activityFor(event) {
|
|
|
146
241
|
return null;
|
|
147
242
|
}
|
|
148
243
|
//#endregion
|
|
149
|
-
//#region src/runtime/mcp.ts
|
|
150
|
-
/**
|
|
244
|
+
//#region src/runtime/mcp-key.ts
|
|
245
|
+
/** Cache-key + tool-class helpers for the MCP/WebFetch interception pipeline. */
|
|
246
|
+
/** Default freshness for cached MCP doc results (48h). */
|
|
151
247
|
const MCP_TTL_MS = 1728e5;
|
|
248
|
+
/** WebFetch freshness — pages go stale faster than docs (24h, parity with Python). */
|
|
249
|
+
const WEBFETCH_TTL_MS = 864e5;
|
|
250
|
+
/** WebFetch prompt slice folded into its cache key (parity with Python prompt[:500]). */
|
|
251
|
+
const PROMPT_TRUNC = 500;
|
|
252
|
+
/** True for the WebFetch tool (exact name). */
|
|
253
|
+
function isWebFetch(tool) {
|
|
254
|
+
return tool === "WebFetch";
|
|
255
|
+
}
|
|
152
256
|
/** MCP doc tools + WebFetch whose calls are cached / verbosity-capped. */
|
|
153
257
|
function isMcpTool(tool) {
|
|
154
258
|
return /context7|exa|webfetch|web_fetch/i.test(tool) || tool === "WebFetch";
|
|
@@ -158,6 +262,17 @@ function queryOf(input) {
|
|
|
158
262
|
const q = input.query ?? input.url ?? input.libraryId ?? "";
|
|
159
263
|
return typeof q === "string" ? q : JSON.stringify(q);
|
|
160
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* The string that keys the cache for `tool`. WebFetch folds `url + "\n" +
|
|
267
|
+
* prompt[:500]` (parity with Python) so distinct prompts on the same URL never
|
|
268
|
+
* collide; every other tool keys on {@link queryOf}.
|
|
269
|
+
*/
|
|
270
|
+
function cacheQueryOf(tool, input) {
|
|
271
|
+
if (!isWebFetch(tool)) return queryOf(input);
|
|
272
|
+
return `${typeof input.url === "string" ? input.url.trim() : ""}\n${(typeof input.prompt === "string" ? input.prompt : "").slice(0, PROMPT_TRUNC)}`;
|
|
273
|
+
}
|
|
274
|
+
//#endregion
|
|
275
|
+
//#region src/runtime/mcp.ts
|
|
161
276
|
function denyWith(id, content) {
|
|
162
277
|
if (id === "claude-code" || id === "codex") return JSON.stringify({ hookSpecificOutput: {
|
|
163
278
|
hookEventName: "PreToolUse",
|
|
@@ -191,7 +306,10 @@ function docSourceOf(tool) {
|
|
|
191
306
|
*/
|
|
192
307
|
function mcpPreIntercept(id, tool, input, dir, ttlMs, now) {
|
|
193
308
|
if (!isMcpTool(tool)) return null;
|
|
194
|
-
const
|
|
309
|
+
const web = isWebFetch(tool);
|
|
310
|
+
const ttl = web ? WEBFETCH_TTL_MS : ttlMs;
|
|
311
|
+
const key = cacheQueryOf(tool, input);
|
|
312
|
+
const cached = cacheLookup(dir, tool, key, ttl, now) ?? (web ? null : cacheLookupSubstring(dir, key, ttl, now));
|
|
195
313
|
if (cached) {
|
|
196
314
|
const served = denyWith(id, cached);
|
|
197
315
|
if (served) return {
|
|
@@ -206,10 +324,18 @@ function mcpPreIntercept(id, tool, input, dir, ttlMs, now) {
|
|
|
206
324
|
}
|
|
207
325
|
return null;
|
|
208
326
|
}
|
|
209
|
-
/**
|
|
210
|
-
|
|
327
|
+
/**
|
|
328
|
+
* Post-event: persist the MCP/WebFetch response (extracted to markdown). MCP docs
|
|
329
|
+
* go through {@link mcpCacheWrite} (compact + Jaccard-dedup + `index.json`);
|
|
330
|
+
* WebFetch uses {@link webfetchCacheWrite} (compact, exact key, no index).
|
|
331
|
+
* @param now - Current epoch ms (timestamp source; defaults to `Date.now()`).
|
|
332
|
+
*/
|
|
333
|
+
function mcpPostStore(tool, input, response, dir, now = Date.now()) {
|
|
211
334
|
if (!isMcpTool(tool)) return;
|
|
212
|
-
|
|
335
|
+
const text = extractText(response);
|
|
336
|
+
const key = cacheQueryOf(tool, input);
|
|
337
|
+
if (isWebFetch(tool)) webfetchCacheWrite(dir, tool, key, text, now);
|
|
338
|
+
else mcpCacheWrite(dir, tool, key, text, now);
|
|
213
339
|
}
|
|
214
340
|
//#endregion
|
|
215
341
|
//#region src/runtime/normalize.ts
|
|
@@ -536,6 +662,117 @@ function runDesignChecks(content) {
|
|
|
536
662
|
];
|
|
537
663
|
}
|
|
538
664
|
//#endregion
|
|
665
|
+
//#region src/policy/design/skill-gate.ts
|
|
666
|
+
/** UI source files the gate polices (.html/.css are handled by the pipeline gate, not here). */
|
|
667
|
+
const UI_FILE_RE = /\.(tsx|jsx|scss|vue|svelte)$/;
|
|
668
|
+
/** Generated/vendored paths that never need a design skill (match at start or mid-path). */
|
|
669
|
+
const EXEMPT_RE = /(^|\/)(node_modules|dist|build)\/|\.claude\//;
|
|
670
|
+
/** UI-intent path fragments (ports the Python `UI_PATH_PATTERNS`). */
|
|
671
|
+
const UI_PATH_RE = /(components|ui|styles|page|layout|content|view|feature|section|hero|footer|header|sidebar|nav|modal|dialog)/;
|
|
672
|
+
/** Tailwind utility usage in JSX `className` (ports the Python `has_jsx_tailwind`). */
|
|
673
|
+
const TAILWIND_RE = /className\s*=.*(?:flex|grid|p-|m-|bg-|text-|rounded|shadow|border|gap-|w-|h-)/;
|
|
674
|
+
/** A read reference path that counts as "a design skill was consulted". */
|
|
675
|
+
const DESIGN_SKILL_RE = /skills\/([0-9]-)?(designing-systems|generating-components|solid-react|solid-nextjs|design|impeccable|frontend-design)/i;
|
|
676
|
+
/** True when this Write/Edit targets a UI file in a UI-ish path or with Tailwind classes. */
|
|
677
|
+
function isUiWrite(tool, filePath, content) {
|
|
678
|
+
if (tool !== "Write" && tool !== "Edit") return false;
|
|
679
|
+
if (!UI_FILE_RE.test(filePath) || EXEMPT_RE.test(filePath)) return false;
|
|
680
|
+
return UI_PATH_RE.test(filePath) || TAILWIND_RE.test(content);
|
|
681
|
+
}
|
|
682
|
+
/** True when a design skill reference was read this session (scans the `refsRead` track). */
|
|
683
|
+
function designSkillRead(refsRead) {
|
|
684
|
+
return refsRead.some((p) => DESIGN_SKILL_RE.test(p));
|
|
685
|
+
}
|
|
686
|
+
const block$1 = (reason, action) => ({
|
|
687
|
+
kind: "block",
|
|
688
|
+
title: "Design skill",
|
|
689
|
+
reason,
|
|
690
|
+
actions: [action]
|
|
691
|
+
});
|
|
692
|
+
/**
|
|
693
|
+
* Gate a UI write: require a design-skill read AND any doc source. Returns a
|
|
694
|
+
* blocking {@link Prompt}, or `null` to allow. NEVER requires Gemini.
|
|
695
|
+
* @param tool - the tool name ("Write" | "Edit" | ...).
|
|
696
|
+
* @param filePath - the written file path.
|
|
697
|
+
* @param content - the written content (snippet on Edit, full file on Write).
|
|
698
|
+
* @param ev - the session evidence (refsRead + docConsulted).
|
|
699
|
+
*/
|
|
700
|
+
function uiDesignSkillGate(tool, filePath, content, ev) {
|
|
701
|
+
if (!isUiWrite(tool, filePath, content)) return null;
|
|
702
|
+
if (!designSkillRead(ev.refsRead)) return block$1("BLOCKED: design skill not consulted before writing UI. Read a design SKILL.md (solid-react, generating-components, or designing-systems), then retry.", "Read a design skill reference, then retry");
|
|
703
|
+
if (!ev.docConsulted) return block$1("BLOCKED: no documentation consulted. Use ANY ONE of mcp__context7__query-docs, mcp__exa__web_search_exa, WebSearch, or WebFetch. Gemini is NOT required.", "Consult Context7, Exa, or web docs (Gemini not required), then retry");
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region src/policy/design/skill-evidence.ts
|
|
708
|
+
/**
|
|
709
|
+
* @module design/skill-evidence
|
|
710
|
+
* IO side of the UI design-skill gate: read the verified session track and
|
|
711
|
+
* derive {@link DesignEvidence} (read references + doc-consulted) for
|
|
712
|
+
* {@link uiDesignSkillGate}. Kept separate from the pure gate logic (SRP).
|
|
713
|
+
* @packageDocumentation
|
|
714
|
+
*/
|
|
715
|
+
/**
|
|
716
|
+
* Read the verified session track and derive {@link DesignEvidence}. Fail-closed
|
|
717
|
+
* on a missing/corrupt/tampered track (no evidence → the gate blocks).
|
|
718
|
+
* @param sessionId - the Claude session id.
|
|
719
|
+
* @param cwd - the project root (selects the per-project state dir).
|
|
720
|
+
* @param baseDir - override the track base dir (tests); defaults to the project state dir.
|
|
721
|
+
*/
|
|
722
|
+
function collectDesignEvidence(sessionId, cwd, baseDir = defaultStateDir(cwd)) {
|
|
723
|
+
let track = null;
|
|
724
|
+
try {
|
|
725
|
+
track = verifyTrack(JSON.parse(readFileSync(trackFile(sessionId, baseDir), "utf8")));
|
|
726
|
+
} catch {
|
|
727
|
+
track = null;
|
|
728
|
+
}
|
|
729
|
+
return {
|
|
730
|
+
refsRead: track?.refsRead ?? [],
|
|
731
|
+
docConsulted: track ? isDocConsulted(track.authorizations, sessionId) : false
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
//#endregion
|
|
735
|
+
//#region src/runtime/design-helpers.ts
|
|
736
|
+
/**
|
|
737
|
+
* @module design-helpers
|
|
738
|
+
* Effectful helpers for the design pipeline gate: locate design-system.md and
|
|
739
|
+
* apply PostToolUse fuse-browser transitions to the design state. Split out of
|
|
740
|
+
* `design.ts` to keep that file within the SOLID size budget (SRP).
|
|
741
|
+
* @packageDocumentation
|
|
742
|
+
*/
|
|
743
|
+
const NAV$1 = "mcp__fuse-browser__browser_navigate";
|
|
744
|
+
const SHOT$1 = "mcp__fuse-browser__browser_screenshot";
|
|
745
|
+
const SCROLL = "mcp__fuse-browser__browser_scroll";
|
|
746
|
+
const GEMINI$1 = "mcp__gemini-design__create_frontend";
|
|
747
|
+
/** Read design-system.md walking up to 6 parents from `cwd` ("" if absent/unreadable). */
|
|
748
|
+
function findDesignSystem(cwd) {
|
|
749
|
+
let dir = cwd;
|
|
750
|
+
for (let i = 0; i < 6; i++) {
|
|
751
|
+
const p = join(dir, "design-system.md");
|
|
752
|
+
if (existsSync(p)) try {
|
|
753
|
+
return readFileSync(p, "utf8");
|
|
754
|
+
} catch {
|
|
755
|
+
return "";
|
|
756
|
+
}
|
|
757
|
+
const parent = dirname(dir);
|
|
758
|
+
if (parent === dir) break;
|
|
759
|
+
dir = parent;
|
|
760
|
+
}
|
|
761
|
+
return "";
|
|
762
|
+
}
|
|
763
|
+
/** Apply a PostToolUse fuse-browser transition to the design state. */
|
|
764
|
+
function recordPost(event, cacheDir, state) {
|
|
765
|
+
if (event.tool === SHOT$1) saveDesignState(cacheDir, recordScreenshot(state, MIN_SCREENSHOTS[state.mode]));
|
|
766
|
+
else if (event.tool === NAV$1) saveDesignState(cacheDir, recordNavigate(state));
|
|
767
|
+
else if (event.tool === SCROLL) saveDesignState(cacheDir, recordScroll(state));
|
|
768
|
+
else if (event.tool === GEMINI$1) saveDesignState(cacheDir, {
|
|
769
|
+
...state,
|
|
770
|
+
geminiCalls: state.geminiCalls + 1
|
|
771
|
+
});
|
|
772
|
+
else if (event.tool === "Read") saveDesignState(cacheDir, recordRead(state, event.filePath ?? ""));
|
|
773
|
+
else if ((event.tool === "Write" || event.tool === "Edit") && (event.filePath ?? "").endsWith("design-system.md")) saveDesignState(cacheDir, recordValidDesignSystem(state));
|
|
774
|
+
}
|
|
775
|
+
//#endregion
|
|
539
776
|
//#region src/policy/design/gates.ts
|
|
540
777
|
const ALLOWED_WRITE = /\.(html|css|md|json)$/;
|
|
541
778
|
const EXEMPT_DIRS = [
|
|
@@ -620,48 +857,28 @@ function geminiEnabled() {
|
|
|
620
857
|
//#region src/runtime/design.ts
|
|
621
858
|
const NAV = "mcp__fuse-browser__browser_navigate";
|
|
622
859
|
const SHOT = "mcp__fuse-browser__browser_screenshot";
|
|
623
|
-
const SCROLL = "mcp__fuse-browser__browser_scroll";
|
|
624
860
|
const GEMINI = "mcp__gemini-design__create_frontend";
|
|
625
|
-
/** Read design-system.md walking up to 6 parents from `cwd` ("" if absent/unreadable). */
|
|
626
|
-
function findDesignSystem(cwd) {
|
|
627
|
-
let dir = cwd;
|
|
628
|
-
for (let i = 0; i < 6; i++) {
|
|
629
|
-
const p = join(dir, "design-system.md");
|
|
630
|
-
if (existsSync(p)) try {
|
|
631
|
-
return readFileSync(p, "utf8");
|
|
632
|
-
} catch {
|
|
633
|
-
return "";
|
|
634
|
-
}
|
|
635
|
-
const parent = dirname(dir);
|
|
636
|
-
if (parent === dir) break;
|
|
637
|
-
dir = parent;
|
|
638
|
-
}
|
|
639
|
-
return "";
|
|
640
|
-
}
|
|
641
|
-
/** Apply a PostToolUse fuse-browser transition to the design state. */
|
|
642
|
-
function recordPost(event, cacheDir, state) {
|
|
643
|
-
if (event.tool === SHOT) saveDesignState(cacheDir, recordScreenshot(state, MIN_SCREENSHOTS[state.mode]));
|
|
644
|
-
else if (event.tool === NAV) saveDesignState(cacheDir, recordNavigate(state));
|
|
645
|
-
else if (event.tool === SCROLL) saveDesignState(cacheDir, recordScroll(state));
|
|
646
|
-
else if (event.tool === GEMINI) saveDesignState(cacheDir, {
|
|
647
|
-
...state,
|
|
648
|
-
geminiCalls: state.geminiCalls + 1
|
|
649
|
-
});
|
|
650
|
-
else if (event.tool === "Read") saveDesignState(cacheDir, recordRead(state, event.filePath ?? ""));
|
|
651
|
-
else if ((event.tool === "Write" || event.tool === "Edit") && (event.filePath ?? "").endsWith("design-system.md")) saveDesignState(cacheDir, recordValidDesignSystem(state));
|
|
652
|
-
}
|
|
653
861
|
/**
|
|
654
862
|
* Design-pipeline gate (effectful: reads/writes the design state + design-system.md).
|
|
655
863
|
* Returns a Prompt to block, or null when this isn't a design-agent context / nothing fires.
|
|
656
864
|
*/
|
|
657
865
|
function designGate(payload, event, cacheDir, cwd) {
|
|
866
|
+
if (event.phase !== "post" && (event.tool === "Write" || event.tool === "Edit")) {
|
|
867
|
+
const skillBlock = uiDesignSkillGate(event.tool, event.filePath ?? "", event.content ?? "", collectDesignEvidence(event.sessionId, cwd));
|
|
868
|
+
if (skillBlock) return skillBlock;
|
|
869
|
+
}
|
|
658
870
|
const agentId = typeof payload.agent_id === "string" ? payload.agent_id : "";
|
|
659
871
|
const active = activeDesignAgent(cacheDir);
|
|
660
872
|
if (active && agentId && agentId !== active) return null;
|
|
661
873
|
const id = active || agentId;
|
|
662
874
|
if (!id) return null;
|
|
663
|
-
|
|
664
|
-
if (!state)
|
|
875
|
+
let state = loadDesignState(cacheDir, id);
|
|
876
|
+
if (!state) {
|
|
877
|
+
if (!active) return null;
|
|
878
|
+
const dsExists = findDesignSystem(cwd) !== "";
|
|
879
|
+
state = initDesignState(id, dsExists ? "page" : "full", dsExists);
|
|
880
|
+
saveDesignState(cacheDir, state);
|
|
881
|
+
}
|
|
665
882
|
if (event.phase === "post") {
|
|
666
883
|
recordPost(event, cacheDir, state);
|
|
667
884
|
if ((event.tool === "Write" || event.tool === "Edit") && /\.(tsx|jsx|css)$/.test(event.filePath ?? "")) {
|
|
@@ -1129,23 +1346,24 @@ function recordHistory(home, agentId, agentType, ts) {
|
|
|
1129
1346
|
function trackAgentMemory(data, home = homedir(), now = Date.now()) {
|
|
1130
1347
|
mkdirSync(sessionsDir(home), { recursive: true });
|
|
1131
1348
|
const agentType = String(data.agent_type ?? data.subagent_type ?? "unknown");
|
|
1132
|
-
const sessionId =
|
|
1349
|
+
const sessionId = sanitizeSessionId(data.session_id) ?? "unknown";
|
|
1133
1350
|
const ts = new Date(now).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1134
1351
|
recordHistory(home, String(data.agent_id ?? "unknown"), agentType, ts);
|
|
1135
1352
|
if (SKIP_AGENTS.test(agentType)) return JSON.stringify({ message: `Agent ${agentType} completed` });
|
|
1136
|
-
const
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1353
|
+
const state = loadSessionState(sessionId, home);
|
|
1354
|
+
const changes = state.changes;
|
|
1355
|
+
const count = changes?.cumulativeCodeFiles ?? 0;
|
|
1356
|
+
if (count > 0) {
|
|
1357
|
+
const files = (changes?.modifiedFiles ?? []).join(", ");
|
|
1358
|
+
saveSessionState(sessionId, {
|
|
1359
|
+
...state,
|
|
1360
|
+
changes: {
|
|
1361
|
+
...changes,
|
|
1144
1362
|
cumulativeCodeFiles: 0
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1363
|
+
}
|
|
1364
|
+
}, home);
|
|
1365
|
+
return contextResponse("SubagentStop", `SNIPER VALIDATION REQUIRED: Agent '${agentType}' modified ${count} code file(s): ${files}. Run sniper agent now.`);
|
|
1366
|
+
}
|
|
1149
1367
|
return JSON.stringify({ message: `Agent ${agentType} completed (no code changes)` });
|
|
1150
1368
|
}
|
|
1151
1369
|
//#endregion
|
|
@@ -1160,14 +1378,9 @@ function trackAgentMemory(data, home = homedir(), now = Date.now()) {
|
|
|
1160
1378
|
*/
|
|
1161
1379
|
function validateTeammateOutput(data, home = homedir()) {
|
|
1162
1380
|
const teammate = String(data.teammate_name ?? "unknown");
|
|
1163
|
-
const
|
|
1164
|
-
const
|
|
1165
|
-
if (
|
|
1166
|
-
try {
|
|
1167
|
-
const state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
1168
|
-
const count = state.cumulativeCodeFiles ?? 0;
|
|
1169
|
-
if (count > 0) return contextResponse("TeammateIdle", `Teammate '${teammate}' going idle after modifying ${count} code file(s): ${(state.modifiedFiles ?? []).slice(0, 5).join(", ")}. Consider running sniper validation.`);
|
|
1170
|
-
} catch {}
|
|
1381
|
+
const changes = loadSessionState(sanitizeSessionId(data.session_id) ?? "unknown", home).changes;
|
|
1382
|
+
const count = changes?.cumulativeCodeFiles ?? 0;
|
|
1383
|
+
if (count > 0) return contextResponse("TeammateIdle", `Teammate '${teammate}' going idle after modifying ${count} code file(s): ${(changes?.modifiedFiles ?? []).slice(0, 5).join(", ")}. Consider running sniper validation.`);
|
|
1171
1384
|
return "";
|
|
1172
1385
|
}
|
|
1173
1386
|
//#endregion
|
|
@@ -1260,7 +1473,7 @@ function validateRulesLoaded(data, home = homedir()) {
|
|
|
1260
1473
|
//#endregion
|
|
1261
1474
|
//#region src/runtime/lifecycle/track-changes.ts
|
|
1262
1475
|
/** Code-file extensions tracked for sniper (mirrors track-session-changes.py). */
|
|
1263
|
-
const CODE_EXT = /\.(ts|tsx|js|jsx|py|go|rs|java|php|cpp|c|rb|swift|kt|vue|svelte|astro)$/;
|
|
1476
|
+
const CODE_EXT$1 = /\.(ts|tsx|js|jsx|py|go|rs|java|php|cpp|c|rb|swift|kt|vue|svelte|astro)$/;
|
|
1264
1477
|
/**
|
|
1265
1478
|
* Handle PostToolUse Write/Edit: track the cumulative set of modified code
|
|
1266
1479
|
* files per session and emit the mandatory "SNIPER VALIDATION REQUIRED"
|
|
@@ -1272,7 +1485,7 @@ const CODE_EXT = /\.(ts|tsx|js|jsx|py|go|rs|java|php|cpp|c|rb|swift|kt|vue|svelt
|
|
|
1272
1485
|
* @returns The native hook stdout (possibly empty when not a code file).
|
|
1273
1486
|
*/
|
|
1274
1487
|
function trackSessionChanges(sessionIdRaw, filePath, home = homedir(), now = Date.now()) {
|
|
1275
|
-
if (!filePath || !CODE_EXT.test(filePath)) return "";
|
|
1488
|
+
if (!filePath || !CODE_EXT$1.test(filePath)) return "";
|
|
1276
1489
|
const sid = sanitizeSessionId(sessionIdRaw) ?? "unknown";
|
|
1277
1490
|
const state = loadSessionState(sid, home);
|
|
1278
1491
|
const prev = state.changes ?? {
|
|
@@ -1354,6 +1567,66 @@ function postEditTypescript(filePath) {
|
|
|
1354
1567
|
return contextResponse("PostToolUse", `Lint issues in ${basename(filePath)}: ${issues.join(" | ")}`);
|
|
1355
1568
|
}
|
|
1356
1569
|
//#endregion
|
|
1570
|
+
//#region src/runtime/lifecycle/task-completed.ts
|
|
1571
|
+
/** Code-file extensions audited on task completion (mirrors validate-task-solid.py). */
|
|
1572
|
+
const CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1573
|
+
".ts",
|
|
1574
|
+
".tsx",
|
|
1575
|
+
".js",
|
|
1576
|
+
".jsx",
|
|
1577
|
+
".py",
|
|
1578
|
+
".go",
|
|
1579
|
+
".rs",
|
|
1580
|
+
".java",
|
|
1581
|
+
".php",
|
|
1582
|
+
".cpp",
|
|
1583
|
+
".c",
|
|
1584
|
+
".rb",
|
|
1585
|
+
".swift",
|
|
1586
|
+
".kt",
|
|
1587
|
+
".dart",
|
|
1588
|
+
".vue",
|
|
1589
|
+
".svelte",
|
|
1590
|
+
".astro"
|
|
1591
|
+
]);
|
|
1592
|
+
/**
|
|
1593
|
+
* Re-count physical lines of every modified code file and collect SOLID
|
|
1594
|
+
* violations (`<basename>: <n> lines (max <max>)`) for those exceeding `max`.
|
|
1595
|
+
* @param files - Candidate modified file paths.
|
|
1596
|
+
* @param max - The SOLID line ceiling.
|
|
1597
|
+
* @returns The list of violation strings (empty when all files comply).
|
|
1598
|
+
*/
|
|
1599
|
+
function collectViolations(files, max) {
|
|
1600
|
+
const violations = [];
|
|
1601
|
+
for (const fp of files) {
|
|
1602
|
+
if (!CODE_EXTENSIONS.has(extname(fp)) || !existsSync(fp)) continue;
|
|
1603
|
+
try {
|
|
1604
|
+
const lines = countLines(readFileSync(fp, "utf-8"));
|
|
1605
|
+
if (lines > max) violations.push(`${basename(fp)}: ${lines} lines (max ${max})`);
|
|
1606
|
+
} catch {}
|
|
1607
|
+
}
|
|
1608
|
+
return violations;
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Handle TaskCompleted: re-measure the session's modified code files and emit a
|
|
1612
|
+
* `SOLID VIOLATION` additionalContext listing any file over the line ceiling.
|
|
1613
|
+
* Ports `task-completed/validate-task-solid.py`.
|
|
1614
|
+
* @param payload - The TaskCompleted hook payload (`task_id`, `task_subject`, `session_id`).
|
|
1615
|
+
* @param home - Home dir (defaults to `~`).
|
|
1616
|
+
* @returns The native hook stdout, or `""` when there are no violations.
|
|
1617
|
+
*/
|
|
1618
|
+
function validateTaskSolid(payload, home = homedir()) {
|
|
1619
|
+
const sid = sanitizeSessionId(payload.session_id ?? "unknown");
|
|
1620
|
+
if (!sid) return "";
|
|
1621
|
+
const files = loadSessionState(sid, home).changes?.modifiedFiles ?? [];
|
|
1622
|
+
if (files.length === 0) return "";
|
|
1623
|
+
const max = resolveMaxLines();
|
|
1624
|
+
const violations = collectViolations(files, max);
|
|
1625
|
+
if (violations.length === 0) return "";
|
|
1626
|
+
const taskId = String(payload.task_id ?? "");
|
|
1627
|
+
return contextResponse("TaskCompleted", `SOLID VIOLATION in task '${String(payload.task_subject ?? "")}' (${taskId}): ${violations.length} file(s) exceed ${max} lines: ` + violations.slice(0, 5).join("; "));
|
|
1628
|
+
}
|
|
1629
|
+
//#endregion
|
|
1357
1630
|
//#region src/runtime/lifecycle/cartographer/fs-util.ts
|
|
1358
1631
|
/**
|
|
1359
1632
|
* Filesystem helpers for the cartographer tree walk. Ports the fs parts of
|
|
@@ -1575,18 +1848,63 @@ function readPluginMeta(pluginPath) {
|
|
|
1575
1848
|
/**
|
|
1576
1849
|
* Auto-detect the marketplace `plugins` dir that contains `cartographer`,
|
|
1577
1850
|
* falling back to the first marketplace with a `plugins` dir, else `cwd`.
|
|
1578
|
-
* Ports `find_marketplace_plugins
|
|
1851
|
+
* Ports `find_marketplace_plugins`, but harness-agnostic: the config dir is
|
|
1852
|
+
* derived from the detected harness (`.claude`, `.codex`, `.cursor`, …) via the
|
|
1853
|
+
* shared `HOME_DIR` mapping instead of a hardcoded `.claude`.
|
|
1579
1854
|
* @param home - Home directory (defaults to `~`).
|
|
1855
|
+
* @param id - Detected harness id (defaults to runtime detection).
|
|
1580
1856
|
* @returns The resolved plugins directory.
|
|
1581
1857
|
*/
|
|
1582
|
-
function findMarketplacePlugins(home = homedir()) {
|
|
1583
|
-
const mp = join(home, ".claude", "plugins", "marketplaces");
|
|
1858
|
+
function findMarketplacePlugins(home = homedir(), id = detectHarness().id) {
|
|
1859
|
+
const mp = join(home, HOME_DIR[id] ?? ".claude", "plugins", "marketplaces");
|
|
1584
1860
|
const markets = sortedNames$1(mp);
|
|
1585
1861
|
for (const m of markets) if (existsSync(join(mp, m, "plugins", "cartographer"))) return join(mp, m, "plugins");
|
|
1586
1862
|
for (const m of markets) if (existsSync(join(mp, m, "plugins"))) return join(mp, m, "plugins");
|
|
1587
1863
|
return process.cwd();
|
|
1588
1864
|
}
|
|
1589
1865
|
//#endregion
|
|
1866
|
+
//#region src/runtime/lifecycle/cartographer/scan-hooks.ts
|
|
1867
|
+
/**
|
|
1868
|
+
* Hook scanning (fs). Ports `_scan_hooks` from `scan_plugins.py`: reads a
|
|
1869
|
+
* plugin's `hooks/hooks.json` and reduces it to a single `("hooks", "<events>", "")`
|
|
1870
|
+
* row. The `hooks` value (or the whole document) is either a MAP of event →
|
|
1871
|
+
* entries (keys are the events) or a LIST of entries each carrying an `event`.
|
|
1872
|
+
*/
|
|
1873
|
+
/** True for a non-null plain object (excludes arrays), mirroring Python `isinstance(x, dict)`. */
|
|
1874
|
+
function isPlainObject(v) {
|
|
1875
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1876
|
+
}
|
|
1877
|
+
/** Sort unique non-empty strings alpha (byte-order). */
|
|
1878
|
+
function sortedEvents(events) {
|
|
1879
|
+
return [...new Set([...events].filter((e) => e))].sort((a, b) => a.localeCompare(b, "en"));
|
|
1880
|
+
}
|
|
1881
|
+
/** Derive hook event names from the `hooks.json` data (map keys, or list `event` fields). */
|
|
1882
|
+
function hookEvents(hooksData) {
|
|
1883
|
+
if (isPlainObject(hooksData)) return sortedEvents(Object.keys(hooksData).filter((k) => !k.startsWith("_")));
|
|
1884
|
+
if (Array.isArray(hooksData)) return sortedEvents(hooksData.map((h) => isPlainObject(h) ? String(h.event ?? "") : ""));
|
|
1885
|
+
return [];
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Scan `hooks/hooks.json` into a single `("hooks", "<events>", "")` row.
|
|
1889
|
+
* @param root - Absolute plugin directory.
|
|
1890
|
+
* @returns The single hooks row, or `[]` when absent/empty/unreadable.
|
|
1891
|
+
*/
|
|
1892
|
+
function scanHooks(root) {
|
|
1893
|
+
const file = join(root, "hooks", "hooks.json");
|
|
1894
|
+
if (!existsSync(file)) return [];
|
|
1895
|
+
try {
|
|
1896
|
+
const raw = JSON.parse(readFileSync(file, "utf-8"));
|
|
1897
|
+
const events = hookEvents(isPlainObject(raw) ? raw.hooks ?? raw : {});
|
|
1898
|
+
return events.length ? [[
|
|
1899
|
+
"hooks",
|
|
1900
|
+
events.join(", "),
|
|
1901
|
+
""
|
|
1902
|
+
]] : [];
|
|
1903
|
+
} catch {
|
|
1904
|
+
return [];
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
//#endregion
|
|
1590
1908
|
//#region src/runtime/lifecycle/cartographer/scan.ts
|
|
1591
1909
|
/**
|
|
1592
1910
|
* Plugin scanning (fs). Ports `scan_plugins.py`: turns a plugin's
|
|
@@ -1655,24 +1973,6 @@ function scanCommands(root) {
|
|
|
1655
1973
|
fileField(join(dir, n), "description").slice(0, 50)
|
|
1656
1974
|
]);
|
|
1657
1975
|
}
|
|
1658
|
-
/** Scan `hooks/hooks.json` → a single `("hooks", "<events>", "")` row, or none. */
|
|
1659
|
-
function scanHooks(root) {
|
|
1660
|
-
const file = join(root, "hooks", "hooks.json");
|
|
1661
|
-
if (!existsSync(file)) return [];
|
|
1662
|
-
try {
|
|
1663
|
-
const raw = JSON.parse(readFileSync(file, "utf-8"));
|
|
1664
|
-
const data = raw && typeof raw === "object" ? raw : {};
|
|
1665
|
-
const hooks = data.hooks && typeof data.hooks === "object" ? data.hooks : data;
|
|
1666
|
-
const events = Object.keys(hooks).filter((k) => !k.startsWith("_")).sort((a, b) => a.localeCompare(b, "en"));
|
|
1667
|
-
return events.length ? [[
|
|
1668
|
-
"hooks",
|
|
1669
|
-
events.join(", "),
|
|
1670
|
-
""
|
|
1671
|
-
]] : [];
|
|
1672
|
-
} catch {
|
|
1673
|
-
return [];
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
1976
|
/**
|
|
1677
1977
|
* Scan a single plugin directory into ordered `[type, name, desc]` rows.
|
|
1678
1978
|
* @param pluginDir - Absolute plugin directory.
|
|
@@ -1853,6 +2153,15 @@ function generateEcosystemMap(now, pluginsDirOverride) {
|
|
|
1853
2153
|
* navigation context from the latter as additionalContext.
|
|
1854
2154
|
*/
|
|
1855
2155
|
/**
|
|
2156
|
+
* Resolve the marketplace plugins dir from `CLAUDE_PLUGIN_ROOT`, mirroring the
|
|
2157
|
+
* Python hook which passes `${CLAUDE_PLUGIN_ROOT}/..` to `generate_map.py`.
|
|
2158
|
+
* @returns The plugins dir (env `/..`), or `undefined` to fall back to auto-detect.
|
|
2159
|
+
*/
|
|
2160
|
+
function pluginsDirFromEnv() {
|
|
2161
|
+
const root = process.env.CLAUDE_PLUGIN_ROOT;
|
|
2162
|
+
return root ? resolve(root, "..") : void 0;
|
|
2163
|
+
}
|
|
2164
|
+
/**
|
|
1856
2165
|
* Regenerate the project map + plugin ecosystem map for `cwd` on SessionStart.
|
|
1857
2166
|
* Emits the ecosystem navigation context as additionalContext (or "").
|
|
1858
2167
|
* @param cwd - The working directory.
|
|
@@ -1861,7 +2170,7 @@ function generateEcosystemMap(now, pluginsDirOverride) {
|
|
|
1861
2170
|
*/
|
|
1862
2171
|
function cartoSessionStart(cwd, now = Date.now()) {
|
|
1863
2172
|
generateProjectMap(cwd);
|
|
1864
|
-
const ctx = generateEcosystemMap(now);
|
|
2173
|
+
const ctx = generateEcosystemMap(now, pluginsDirFromEnv());
|
|
1865
2174
|
return ctx ? contextResponse("SessionStart", ctx) : "";
|
|
1866
2175
|
}
|
|
1867
2176
|
//#endregion
|
|
@@ -2658,12 +2967,101 @@ async function cacheDocFromTranscript(transcript, cwd, home = homedir()) {
|
|
|
2658
2967
|
await writeJsonFile(indexFile, index, true);
|
|
2659
2968
|
}
|
|
2660
2969
|
//#endregion
|
|
2970
|
+
//#region src/runtime/lifecycle/aipilot/promote-global-lessons.ts
|
|
2971
|
+
/**
|
|
2972
|
+
* Promote high-frequency project lessons (occurrence count >= 3) to the global,
|
|
2973
|
+
* stack-scoped cache (`fusengine-cache/lessons/_global/<stack>.json`, cap 25).
|
|
2974
|
+
* Ports the ai-pilot `promote-global-lessons.ts` (a spawned subprocess) as an
|
|
2975
|
+
* in-process call from `cacheSniperLessons`. Pure side-effect.
|
|
2976
|
+
*/
|
|
2977
|
+
const MIN_COUNT = 3;
|
|
2978
|
+
const MAX_GLOBAL = 25;
|
|
2979
|
+
/** Group lessons by `error_type:pattern`. */
|
|
2980
|
+
function groupByKey(lessons) {
|
|
2981
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2982
|
+
for (const l of lessons) {
|
|
2983
|
+
const key = `${l.error_type}:${l.pattern}`;
|
|
2984
|
+
(groups.get(key) ?? groups.set(key, []).get(key)).push(l);
|
|
2985
|
+
}
|
|
2986
|
+
return [...groups.values()];
|
|
2987
|
+
}
|
|
2988
|
+
/** Read every project lesson file's `errors` into one flat (un-deduped) list. */
|
|
2989
|
+
async function aggregateErrors(cacheDir) {
|
|
2990
|
+
let files;
|
|
2991
|
+
try {
|
|
2992
|
+
files = readdirSync(cacheDir).filter((f) => f.endsWith(".json"));
|
|
2993
|
+
} catch {
|
|
2994
|
+
return [];
|
|
2995
|
+
}
|
|
2996
|
+
const all = [];
|
|
2997
|
+
for (const f of files) {
|
|
2998
|
+
const data = await readJsonFile(join(cacheDir, f));
|
|
2999
|
+
if (data?.errors) all.push(...data.errors);
|
|
3000
|
+
}
|
|
3001
|
+
return all;
|
|
3002
|
+
}
|
|
3003
|
+
/** Keep groups seen >= MIN_COUNT times, collapsed to a single lesson each. */
|
|
3004
|
+
function filterFrequent(lessons) {
|
|
3005
|
+
return groupByKey(lessons).filter((e) => e.length >= MIN_COUNT).map((entries) => {
|
|
3006
|
+
const first = entries[0];
|
|
3007
|
+
return {
|
|
3008
|
+
error_type: first.error_type,
|
|
3009
|
+
pattern: first.pattern,
|
|
3010
|
+
fix: first.fix,
|
|
3011
|
+
last_seen: first.last_seen,
|
|
3012
|
+
count: entries.length,
|
|
3013
|
+
files: [...new Set(entries.flatMap((e) => e.files))],
|
|
3014
|
+
code: { line: [...new Set(entries.flatMap((e) => e.code?.line ?? []))].slice(0, 5) }
|
|
3015
|
+
};
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
3018
|
+
/** Merge tagged candidates into existing global lessons; dedup, sum, sort, cap. */
|
|
3019
|
+
function mergeGlobal(existing, candidates, projHash) {
|
|
3020
|
+
const tagged = candidates.map((c) => ({
|
|
3021
|
+
...c,
|
|
3022
|
+
source_projects: [projHash]
|
|
3023
|
+
}));
|
|
3024
|
+
return groupByKey([...existing, ...tagged]).map((entries) => {
|
|
3025
|
+
const first = entries[0];
|
|
3026
|
+
return {
|
|
3027
|
+
error_type: first.error_type,
|
|
3028
|
+
pattern: first.pattern,
|
|
3029
|
+
fix: first.fix,
|
|
3030
|
+
last_seen: first.last_seen,
|
|
3031
|
+
count: entries.reduce((sum, e) => sum + e.count, 0),
|
|
3032
|
+
files: [...new Set(entries.flatMap((e) => e.files))],
|
|
3033
|
+
code: { line: [...new Set(entries.flatMap((e) => e.code?.line ?? []))].slice(0, 5) },
|
|
3034
|
+
source_projects: [...new Set(entries.flatMap((e) => e.source_projects ?? []))]
|
|
3035
|
+
};
|
|
3036
|
+
}).sort((a, b) => b.count - a.count).slice(0, MAX_GLOBAL);
|
|
3037
|
+
}
|
|
3038
|
+
/**
|
|
3039
|
+
* Promote high-frequency lessons from `cacheDir` to the global stack file.
|
|
3040
|
+
* @param cacheDir - Per-project lessons cache dir to scan.
|
|
3041
|
+
* @param stack - Detected project stack (`nextjs`, `laravel`, …, `universal`).
|
|
3042
|
+
* @param projHash - Source project hash (tags promoted entries).
|
|
3043
|
+
* @param home - Home dir (defaults to `~`).
|
|
3044
|
+
*/
|
|
3045
|
+
async function promoteGlobalLessons(cacheDir, stack, projHash, home = homedir()) {
|
|
3046
|
+
const candidates = filterFrequent(await aggregateErrors(cacheDir));
|
|
3047
|
+
if (candidates.length === 0) return;
|
|
3048
|
+
const globalDir = join(cacheBaseDir(home), "lessons", "_global");
|
|
3049
|
+
mkdirSync(globalDir, { recursive: true });
|
|
3050
|
+
const globalFile = join(globalDir, `${stack}.json`);
|
|
3051
|
+
const merged = mergeGlobal(await readJsonFile(globalFile) ?? [], candidates, projHash);
|
|
3052
|
+
await writeJsonFile(globalFile, merged, true);
|
|
3053
|
+
logCacheEvent("lessons", "promoted", projHash, {
|
|
3054
|
+
count: merged.length,
|
|
3055
|
+
stack
|
|
3056
|
+
}, home);
|
|
3057
|
+
}
|
|
3058
|
+
//#endregion
|
|
2661
3059
|
//#region src/runtime/lifecycle/aipilot/cache-lessons.ts
|
|
2662
3060
|
/**
|
|
2663
3061
|
* SubagentStop (matcher "sniper") for the ai-pilot scope: capture error
|
|
2664
3062
|
* patterns + corrected code from the sniper transcript as lessons. Ports
|
|
2665
|
-
* `cache-sniper-lessons.ts
|
|
2666
|
-
*
|
|
3063
|
+
* `cache-sniper-lessons.ts`, including the high-frequency promotion to the
|
|
3064
|
+
* global stack cache (`promoteGlobalLessons`). Pure side-effect.
|
|
2667
3065
|
*/
|
|
2668
3066
|
/**
|
|
2669
3067
|
* SubagentStop sniper: extract lessons from the transcript and persist them.
|
|
@@ -2700,6 +3098,7 @@ async function cacheSniperLessons(transcript, cwd, home = homedir()) {
|
|
|
2700
3098
|
timestamp,
|
|
2701
3099
|
errors
|
|
2702
3100
|
}, true);
|
|
3101
|
+
await promoteGlobalLessons(cacheDir, detectStack(projectPath), pHash, home);
|
|
2703
3102
|
logCacheEvent("lessons", "hit", pHash, { count: edits.length }, home);
|
|
2704
3103
|
}
|
|
2705
3104
|
//#endregion
|
|
@@ -2924,12 +3323,12 @@ function contextTextOf(response) {
|
|
|
2924
3323
|
}
|
|
2925
3324
|
}
|
|
2926
3325
|
/**
|
|
2927
|
-
* Combine
|
|
2928
|
-
*
|
|
2929
|
-
* call must do that join itself). Returns "" when
|
|
3326
|
+
* Combine N SubagentStart responses into one (Claude concatenates every hook's
|
|
3327
|
+
* additionalContext; collapsing the matcher-"" + type-specific scripts into one
|
|
3328
|
+
* dispatch call must do that join itself). Returns "" when all are empty.
|
|
2930
3329
|
*/
|
|
2931
|
-
function combineContext(
|
|
2932
|
-
const parts =
|
|
3330
|
+
function combineContext(...responses) {
|
|
3331
|
+
const parts = responses.map(contextTextOf).filter(Boolean);
|
|
2933
3332
|
return parts.length ? contextResponse("SubagentStart", parts.join("\n\n")) : "";
|
|
2934
3333
|
}
|
|
2935
3334
|
/** The transcript path a SubagentStop payload reports (Stop-only field). */
|
|
@@ -2937,13 +3336,22 @@ function transcriptOf(payload) {
|
|
|
2937
3336
|
const t = payload.agent_transcript_path;
|
|
2938
3337
|
return typeof t === "string" ? t : void 0;
|
|
2939
3338
|
}
|
|
2940
|
-
/** SubagentStart
|
|
2941
|
-
async function
|
|
2942
|
-
const agent = agentTypeOf(payload);
|
|
3339
|
+
/** Type-specific SubagentStart cache for an agent, or "" when none applies. */
|
|
3340
|
+
async function typeSpecificCache(agent, cwd, now) {
|
|
2943
3341
|
if (agent.includes("explore-codebase")) return injectExploreCache(cwd, void 0, now);
|
|
2944
3342
|
if (agent.includes("research-expert")) return injectDocCache(cwd, void 0, now);
|
|
2945
3343
|
if (agent.includes("sniper")) return injectTestCache(cwd, void 0, now);
|
|
2946
|
-
return
|
|
3344
|
+
return "";
|
|
3345
|
+
}
|
|
3346
|
+
/**
|
|
3347
|
+
* SubagentStart routing. Parity with the Python ai-pilot hooks: the two
|
|
3348
|
+
* matcher-"" entries (APEX context + lessons "known issues") fire for EVERY
|
|
3349
|
+
* sub-agent, then the type-specific cache (explore/doc/test) is concatenated on
|
|
3350
|
+
* top. Sniper must still receive the lessons block — hence no early return.
|
|
3351
|
+
*/
|
|
3352
|
+
async function onSubagentStart(payload, cwd, now) {
|
|
3353
|
+
const agent = agentTypeOf(payload);
|
|
3354
|
+
return combineContext(await injectApexSubagentContext(cwd), await injectLessonsCache(cwd, void 0, now), await typeSpecificCache(agent, cwd, now));
|
|
2947
3355
|
}
|
|
2948
3356
|
/** SubagentStop routing: transcript-driven cache writers (side-effects). */
|
|
2949
3357
|
async function onSubagentStop(payload, cwd) {
|
|
@@ -3014,6 +3422,7 @@ function dispatchLifecycle(input) {
|
|
|
3014
3422
|
case "InstructionsLoaded":
|
|
3015
3423
|
validateRulesLoaded(input.payload);
|
|
3016
3424
|
return "";
|
|
3425
|
+
case "TaskCompleted": return validateTaskSolid(input.payload);
|
|
3017
3426
|
default: return null;
|
|
3018
3427
|
}
|
|
3019
3428
|
}
|
|
@@ -3465,11 +3874,11 @@ async function dispatchMemory(event, payload, cwd, now) {
|
|
|
3465
3874
|
* missing. Opt-in only — silent when no marker / non-HTML / file unreadable.
|
|
3466
3875
|
*/
|
|
3467
3876
|
/**
|
|
3468
|
-
* Validate the edited file's SEO completeness. Returns a
|
|
3469
|
-
* `
|
|
3877
|
+
* Validate the edited file's SEO completeness. Returns a block message (for a
|
|
3878
|
+
* top-level `decision: block` response) when HTML-like, opted-in, and missing
|
|
3470
3879
|
* elements; otherwise `null` (allow).
|
|
3471
3880
|
* @param payload - The raw PostToolUse payload.
|
|
3472
|
-
* @returns The
|
|
3881
|
+
* @returns The block reason string, or `null` to allow.
|
|
3473
3882
|
*/
|
|
3474
3883
|
function seoPostToolUse(payload) {
|
|
3475
3884
|
const path = payload.tool_input?.file_path;
|
|
@@ -3484,14 +3893,16 @@ function seoPostToolUse(payload) {
|
|
|
3484
3893
|
}
|
|
3485
3894
|
}
|
|
3486
3895
|
/**
|
|
3487
|
-
* SEO PostToolUse as a ready native response: a `
|
|
3896
|
+
* SEO PostToolUse as a ready native response: a top-level `decision: block`
|
|
3488
3897
|
* string when the edited file is missing SEO elements, else `null` (allow).
|
|
3898
|
+
* PostToolUse ignores `permissionDecision` (PreToolUse-only), so this must use
|
|
3899
|
+
* the `decision`/`reason` keys to actually feed the failure back to Claude.
|
|
3489
3900
|
* @param payload - The raw PostToolUse payload.
|
|
3490
|
-
* @returns The
|
|
3901
|
+
* @returns The block response string, or `null` to allow.
|
|
3491
3902
|
*/
|
|
3492
3903
|
function seoPostToolUseResponse(payload) {
|
|
3493
|
-
const
|
|
3494
|
-
return
|
|
3904
|
+
const block = seoPostToolUse(payload);
|
|
3905
|
+
return block ? blockResponse(block) : null;
|
|
3495
3906
|
}
|
|
3496
3907
|
//#endregion
|
|
3497
3908
|
//#region src/runtime/lifecycle-bridge.ts
|
|
@@ -3535,6 +3946,26 @@ function postEditContext(scope, event, now) {
|
|
|
3535
3946
|
}
|
|
3536
3947
|
//#endregion
|
|
3537
3948
|
//#region src/runtime/gate-helpers.ts
|
|
3949
|
+
/** Source extensions the APEX gates apply to (parity: require-apex-agents.py CODE_EXT). */
|
|
3950
|
+
const CODE_EXT = /\.(ts|tsx|js|jsx|py|go|rs|java|php|cpp|c|rb|swift|kt|dart|vue|svelte|astro)$/;
|
|
3951
|
+
/** Paths exempt from the APEX gates even when they are code (parity: require-apex-agents.py EXEMPT_PATTERNS). */
|
|
3952
|
+
const EXEMPT_PATTERNS = [
|
|
3953
|
+
/\.claude-plugin\//,
|
|
3954
|
+
/CHANGELOG\.md$/,
|
|
3955
|
+
/marketplace\.json$/,
|
|
3956
|
+
/\/\.claude\/(apex|memory|logs|fusengine-cache)\//
|
|
3957
|
+
];
|
|
3958
|
+
/**
|
|
3959
|
+
* True when `filePath` is a code file the APEX freshness/doc/SOLID gates should
|
|
3960
|
+
* police — i.e. it matches a tracked source extension AND no exempt pattern.
|
|
3961
|
+
* Mirrors require-apex-agents.py so non-code (and exempt) paths skip those gates.
|
|
3962
|
+
* @param filePath - Absolute path of the edit target (or undefined).
|
|
3963
|
+
* @returns Whether the APEX gates apply to this path.
|
|
3964
|
+
*/
|
|
3965
|
+
function isApexScoped(filePath) {
|
|
3966
|
+
if (!filePath || !CODE_EXT.test(filePath)) return false;
|
|
3967
|
+
return !EXEMPT_PATTERNS.some((p) => p.test(filePath));
|
|
3968
|
+
}
|
|
3538
3969
|
/**
|
|
3539
3970
|
* Code-only line count of the existing on-disk file (undefined if
|
|
3540
3971
|
* absent/unreadable). Uses {@link countLines} (skips blank/comment lines) so a
|
|
@@ -3556,7 +3987,7 @@ function existingLineCount(path) {
|
|
|
3556
3987
|
//#region src/freshness/agent-evidence.ts
|
|
3557
3988
|
/**
|
|
3558
3989
|
* Platform-authored transcript evidence for APEX agent freshness.
|
|
3559
|
-
* Parses the Claude Code session JSONL transcript to find genuine Task
|
|
3990
|
+
* Parses the Claude Code session JSONL transcript to find genuine `Task`/`Agent`
|
|
3560
3991
|
* tool_use entries — forging this requires writing into the transcript file
|
|
3561
3992
|
* which the Claude Code platform controls, unlike the self-recorded track.
|
|
3562
3993
|
*/
|
|
@@ -3569,9 +4000,9 @@ function parseTs(raw) {
|
|
|
3569
4000
|
}
|
|
3570
4001
|
/**
|
|
3571
4002
|
* Return `true` ONLY when, for EVERY name in `names`, the Claude Code
|
|
3572
|
-
* transcript at `transcriptPath` contains a genuine `tool_use` of the `Task`
|
|
3573
|
-
* tool whose `subagent_type` (or `name`) input field matches
|
|
3574
|
-
* timestamp within `windowMs` of `now`.
|
|
4003
|
+
* transcript at `transcriptPath` contains a genuine `tool_use` of the `Task` or
|
|
4004
|
+
* `Agent` tool whose `subagent_type` (or `name`) input field matches — after
|
|
4005
|
+
* stripping any plugin prefix — with the entry timestamp within `windowMs` of `now`.
|
|
3575
4006
|
*
|
|
3576
4007
|
* **Timestamp note:** when a transcript entry carries no `timestamp` field it
|
|
3577
4008
|
* is counted as within-window (we cannot prove staleness). This is
|
|
@@ -3609,9 +4040,10 @@ function agentsRanFromTranscript(transcriptPath, names, windowMs, now) {
|
|
|
3609
4040
|
const content = entry.message?.content;
|
|
3610
4041
|
if (!Array.isArray(content)) continue;
|
|
3611
4042
|
for (const block of content) {
|
|
3612
|
-
if (block?.type !== "tool_use" || block.name !== "Task") continue;
|
|
3613
|
-
const
|
|
3614
|
-
|
|
4043
|
+
if (block?.type !== "tool_use" || block.name !== "Task" && block.name !== "Agent") continue;
|
|
4044
|
+
const raw = block.input?.subagent_type ?? block.input?.name;
|
|
4045
|
+
const agent = typeof raw === "string" ? raw.split(":").pop() ?? raw : void 0;
|
|
4046
|
+
if (agent !== void 0 && names.includes(agent)) found.add(agent);
|
|
3615
4047
|
}
|
|
3616
4048
|
if (found.size === names.length) return true;
|
|
3617
4049
|
}
|
|
@@ -3958,6 +4390,8 @@ const TRIVIAL_BUDGET = 4;
|
|
|
3958
4390
|
* track. Returns the first blocking prompt, or null to allow.
|
|
3959
4391
|
*/
|
|
3960
4392
|
async function gate(input) {
|
|
4393
|
+
const precommit = preCommitGate(input.tool, input.command, input.cwd);
|
|
4394
|
+
if (precommit) return precommit;
|
|
3961
4395
|
const existingLines = existingLineCount(input.filePath);
|
|
3962
4396
|
let quick;
|
|
3963
4397
|
try {
|
|
@@ -3973,8 +4407,6 @@ async function gate(input) {
|
|
|
3973
4407
|
return FAIL_CLOSED;
|
|
3974
4408
|
}
|
|
3975
4409
|
if (quick.decision !== "allow" && quick.prompt) return quick.prompt;
|
|
3976
|
-
const precommit = preCommitGate(input.tool, input.command, input.cwd);
|
|
3977
|
-
if (precommit) return precommit;
|
|
3978
4410
|
const modular = modularGate(input.tool, input.filePath, input.content, input.cwd);
|
|
3979
4411
|
if (modular) return modular;
|
|
3980
4412
|
if (!input.filePath) return null;
|
|
@@ -3982,29 +4414,31 @@ async function gate(input) {
|
|
|
3982
4414
|
const track = await loadTrack(input.trackFile);
|
|
3983
4415
|
const solidOrSkill = frameworkSkillGate(input, track.refsRead, existingLines);
|
|
3984
4416
|
if (solidOrSkill) return solidOrSkill;
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4417
|
+
if (isApexScoped(input.filePath)) {
|
|
4418
|
+
const lineCount = input.content === void 0 ? Number.POSITIVE_INFINITY : input.content.split("\n").length;
|
|
4419
|
+
if (!input.isReplaceAll && lineCount < 5 && trivialCount(track, window, input.now) < 4) {
|
|
4420
|
+
await saveTrack(input.trackFile, recordTrivialEdit(track, input.now, window, input.now));
|
|
4421
|
+
return null;
|
|
4422
|
+
}
|
|
4423
|
+
const freshnessFor = (names, windowMs = window) => input.transcriptPath ? agentsRanFromTranscript(input.transcriptPath, names, windowMs, input.now) : agentsFresh(track, names, windowMs, input.now);
|
|
4424
|
+
const ctx = {
|
|
4425
|
+
sessionId: input.sessionId,
|
|
4426
|
+
framework: input.framework,
|
|
4427
|
+
filePath: input.filePath,
|
|
4428
|
+
content: input.content ?? "",
|
|
4429
|
+
authorizations: track.authorizations,
|
|
4430
|
+
refs: input.refs,
|
|
4431
|
+
refsRead: track.refsRead,
|
|
4432
|
+
agentsFresh: freshnessFor([...REQUIRED_AGENTS]),
|
|
4433
|
+
brainstormRequired: track.brainstormRequired,
|
|
4434
|
+
brainstormFresh: freshnessFor(["brainstorming"], Number.MAX_SAFE_INTEGER)
|
|
4435
|
+
};
|
|
4436
|
+
try {
|
|
4437
|
+
const apex = evaluateApex(ctx);
|
|
4438
|
+
if (apex) return apex;
|
|
4439
|
+
} catch {
|
|
4440
|
+
return FAIL_CLOSED;
|
|
4441
|
+
}
|
|
4008
4442
|
}
|
|
4009
4443
|
return dryGate(input.tool, input.filePath, input.content, input.cwd);
|
|
4010
4444
|
}
|
|
@@ -4176,4 +4610,4 @@ async function handleHook(id, payload, opts) {
|
|
|
4176
4610
|
});
|
|
4177
4611
|
}
|
|
4178
4612
|
//#endregion
|
|
4179
|
-
export { sessionStartCore as $, writePluginMap as A,
|
|
4613
|
+
export { sessionStartCore as $, writePluginMap as A, fusengineCache as At, trackSessionChanges as B, aipilotPostToolUse as C, isoUtc as Ct, lessonsStateFileFor as D, securityStatePath as Dt, lessonsFileFor as E, securityStateDir as Et, mergeLines as F, sessionsDir as Ft, validateTeammateOutput as G, cleanupSession as H, countFiles as I, detectSolidProfile as J, trackAgentMemory as K, getFileDesc as L, isProject as M, sanitizeSessionId as Mt, writeTree as N, saveSessionState as Nt, cartoSessionStart as O, todayUtc as Ot, loadEnriched as P, sessionStatePath as Pt, runSessionStartCleanups as Q, listChildren as R, dispatchLifecycle as S, activityFor as St, dispatchLessons as T, saveSecurityState as Tt, saveApexState as U, validateRulesLoaded as V, logToolFailure as W, injectRules as X, solidDetectStart as Y, readRules as Z, postTrackingSideEffects as _, MCP_TTL_MS as _t, TRIVIAL_BUDGET as a, gitContext as at, trackSkillRead as b, isMcpTool as bt, detectDuplication as c, taskContext as ct, lifecycleStdout as d, defaultStateDir as dt, pruneEmptyDirs as et, postEditContext as f, projectHash$1 as ft, securityAdvisory as g, mcpPreIntercept as gt, dispatchMemory as h, mcpPostStore as ht, REQUIRED_AGENTS as i, devContext as it, generateProjectMap as j, loadSessionState as jt, generateEcosystemMap as k, claudeHome as kt, dryGate as l, respond as lt, seoPostToolUseResponse as m, normalizeEvent as mt, handlePre as n, removeOldFiles as nt, gate as o, projectContext as ot, seoPostToolUse as p, trackFile as pt, subagentCacheContext as q, DEFAULT_WINDOW_MS as r, trimLogFile as rt, preCommitGate as s, promptSubmitContext as st, handleHook as t, purgeTtlTree as tt, extractSymbols as u, recordActivity as ut, trackWatchResearch as v, WEBFETCH_TTL_MS as vt, dispatchAipilot as w, loadSecurityState as wt, trackEnrichment as x, queryOf as xt, trackMcpResearch as y, cacheQueryOf as yt, postEditTypescript as z };
|