@fusengine/harness 0.1.39 → 0.1.41
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/README.md +7 -4
- 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-ZgTC5yDS.mjs → claude-BnFrSfMo.mjs} +17 -3
- package/dist/cli/bin.mjs +70 -6
- 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-BhzDmJ18.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-za4vT-p7.mjs → evaluate-B1n1ti0N.mjs} +89 -49
- package/dist/freshness/index.d.mts +1 -1
- package/dist/freshness/index.mjs +1 -1
- package/dist/{handle-t-lQuQc7.mjs → handle-VhWQxvyN.mjs} +612 -163
- 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-DOARDruz.mjs → run-D-Ydrw3D.mjs} +1 -1
- package/dist/runtime/index.d.mts +33 -13
- 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-osn-huLZ.mjs → validate-B3wkqEoN.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-B3wkqEoN.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-BhzDmJ18.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,57 +101,173 @@ 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
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The documentation source a tool satisfies for the doc-consultation gate, or
|
|
210
|
+
* undefined. Context7/Exa are the primary sources; fuse-browser is the fallback
|
|
211
|
+
* when Exa is down (`browser_fetch`/`browser_crawl`/`browser_serp_batch`); the
|
|
212
|
+
* built-in WebSearch/WebFetch also count.
|
|
213
|
+
* @param tool - The harness tool name.
|
|
214
|
+
*/
|
|
215
|
+
function docSourceOf$1(tool) {
|
|
216
|
+
if (/context7/i.test(tool)) return "context7";
|
|
217
|
+
if (/exa/i.test(tool)) return "exa";
|
|
218
|
+
if (/browser_(fetch|crawl|serp)/i.test(tool)) return "fuse-browser";
|
|
219
|
+
if (tool === "WebSearch") return "websearch";
|
|
220
|
+
if (tool === "WebFetch") return "webfetch";
|
|
221
|
+
}
|
|
110
222
|
/**
|
|
111
223
|
* Map a live tool-use to the activity to record, or null when nothing is
|
|
112
224
|
* tracked. Works across harnesses — tool names are globally distinct:
|
|
113
225
|
* - MCP doc calls (`context7` / `exa`, any separator) → `doc`
|
|
114
|
-
* - `Task` + `subagent_type` (Claude/Cursor) → `agent` (bare agent name)
|
|
226
|
+
* - `Task`/`Agent` + `subagent_type` (Claude/Cursor) → `agent` (bare agent name)
|
|
227
|
+
* - direct exploration/research (Glob/Grep, explore Bash, web, MCP cache reads)
|
|
228
|
+
* → `agent` credited to the matching REQUIRED_AGENTS phase
|
|
115
229
|
* - a read tool opening a `.md` reference → `ref`
|
|
116
230
|
*/
|
|
117
231
|
function activityFor(event) {
|
|
118
|
-
|
|
232
|
+
const out = [];
|
|
233
|
+
const docSource = docSourceOf$1(event.tool);
|
|
234
|
+
if (docSource) out.push({
|
|
119
235
|
kind: "doc",
|
|
120
236
|
framework: event.framework,
|
|
121
237
|
sessionId: event.sessionId,
|
|
122
|
-
source:
|
|
123
|
-
};
|
|
124
|
-
if (event.tool === "Task") {
|
|
125
|
-
const name = String(event.input?.subagent_type ?? "").split(":").pop() ?? "";
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
return quality ? {
|
|
129
|
-
kind: "agent",
|
|
130
|
-
name,
|
|
131
|
-
ts: event.now,
|
|
132
|
-
quality
|
|
133
|
-
} : {
|
|
134
|
-
kind: "agent",
|
|
135
|
-
name,
|
|
136
|
-
ts: event.now
|
|
137
|
-
};
|
|
238
|
+
source: docSource
|
|
239
|
+
});
|
|
240
|
+
if (event.tool === "Task" || event.tool === "Agent") {
|
|
241
|
+
const name = String(event.input?.subagent_type ?? event.input?.name ?? "").split(":").pop() ?? "";
|
|
242
|
+
if (name) out.push(agentActivity(name, event.now, qualityFor(event.responseLength, AGENT_QUALITY_MIN)));
|
|
243
|
+
return out;
|
|
138
244
|
}
|
|
139
|
-
|
|
245
|
+
const hit = classifyExplore(event.tool, event.input);
|
|
246
|
+
if (hit) {
|
|
247
|
+
const quality = hit.cacheHit ? "sufficient" : qualityFor(event.responseLength, EXPLORE_QUALITY_MIN);
|
|
248
|
+
out.push(agentActivity(hit.phase, event.now, quality));
|
|
249
|
+
} else if (READ_TOOLS.has(event.tool)) {
|
|
140
250
|
const path = String(event.input?.file_path ?? event.input?.path ?? "");
|
|
141
|
-
if (path.endsWith(".md"))
|
|
251
|
+
if (path.endsWith(".md")) out.push({
|
|
142
252
|
kind: "ref",
|
|
143
253
|
path
|
|
144
|
-
};
|
|
254
|
+
});
|
|
145
255
|
}
|
|
146
|
-
return
|
|
256
|
+
return out;
|
|
147
257
|
}
|
|
148
258
|
//#endregion
|
|
149
|
-
//#region src/runtime/mcp.ts
|
|
150
|
-
/**
|
|
259
|
+
//#region src/runtime/mcp-key.ts
|
|
260
|
+
/** Cache-key + tool-class helpers for the MCP/WebFetch interception pipeline. */
|
|
261
|
+
/** Default freshness for cached MCP doc results (48h). */
|
|
151
262
|
const MCP_TTL_MS = 1728e5;
|
|
263
|
+
/** WebFetch freshness — pages go stale faster than docs (24h, parity with Python). */
|
|
264
|
+
const WEBFETCH_TTL_MS = 864e5;
|
|
265
|
+
/** WebFetch prompt slice folded into its cache key (parity with Python prompt[:500]). */
|
|
266
|
+
const PROMPT_TRUNC = 500;
|
|
267
|
+
/** True for the WebFetch tool (exact name). */
|
|
268
|
+
function isWebFetch(tool) {
|
|
269
|
+
return tool === "WebFetch";
|
|
270
|
+
}
|
|
152
271
|
/** MCP doc tools + WebFetch whose calls are cached / verbosity-capped. */
|
|
153
272
|
function isMcpTool(tool) {
|
|
154
273
|
return /context7|exa|webfetch|web_fetch/i.test(tool) || tool === "WebFetch";
|
|
@@ -158,6 +277,17 @@ function queryOf(input) {
|
|
|
158
277
|
const q = input.query ?? input.url ?? input.libraryId ?? "";
|
|
159
278
|
return typeof q === "string" ? q : JSON.stringify(q);
|
|
160
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* The string that keys the cache for `tool`. WebFetch folds `url + "\n" +
|
|
282
|
+
* prompt[:500]` (parity with Python) so distinct prompts on the same URL never
|
|
283
|
+
* collide; every other tool keys on {@link queryOf}.
|
|
284
|
+
*/
|
|
285
|
+
function cacheQueryOf(tool, input) {
|
|
286
|
+
if (!isWebFetch(tool)) return queryOf(input);
|
|
287
|
+
return `${typeof input.url === "string" ? input.url.trim() : ""}\n${(typeof input.prompt === "string" ? input.prompt : "").slice(0, PROMPT_TRUNC)}`;
|
|
288
|
+
}
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/runtime/mcp.ts
|
|
161
291
|
function denyWith(id, content) {
|
|
162
292
|
if (id === "claude-code" || id === "codex") return JSON.stringify({ hookSpecificOutput: {
|
|
163
293
|
hookEventName: "PreToolUse",
|
|
@@ -191,7 +321,10 @@ function docSourceOf(tool) {
|
|
|
191
321
|
*/
|
|
192
322
|
function mcpPreIntercept(id, tool, input, dir, ttlMs, now) {
|
|
193
323
|
if (!isMcpTool(tool)) return null;
|
|
194
|
-
const
|
|
324
|
+
const web = isWebFetch(tool);
|
|
325
|
+
const ttl = web ? WEBFETCH_TTL_MS : ttlMs;
|
|
326
|
+
const key = cacheQueryOf(tool, input);
|
|
327
|
+
const cached = cacheLookup(dir, tool, key, ttl, now) ?? (web ? null : cacheLookupSubstring(dir, key, ttl, now));
|
|
195
328
|
if (cached) {
|
|
196
329
|
const served = denyWith(id, cached);
|
|
197
330
|
if (served) return {
|
|
@@ -206,10 +339,18 @@ function mcpPreIntercept(id, tool, input, dir, ttlMs, now) {
|
|
|
206
339
|
}
|
|
207
340
|
return null;
|
|
208
341
|
}
|
|
209
|
-
/**
|
|
210
|
-
|
|
342
|
+
/**
|
|
343
|
+
* Post-event: persist the MCP/WebFetch response (extracted to markdown). MCP docs
|
|
344
|
+
* go through {@link mcpCacheWrite} (compact + Jaccard-dedup + `index.json`);
|
|
345
|
+
* WebFetch uses {@link webfetchCacheWrite} (compact, exact key, no index).
|
|
346
|
+
* @param now - Current epoch ms (timestamp source; defaults to `Date.now()`).
|
|
347
|
+
*/
|
|
348
|
+
function mcpPostStore(tool, input, response, dir, now = Date.now()) {
|
|
211
349
|
if (!isMcpTool(tool)) return;
|
|
212
|
-
|
|
350
|
+
const text = extractText(response);
|
|
351
|
+
const key = cacheQueryOf(tool, input);
|
|
352
|
+
if (isWebFetch(tool)) webfetchCacheWrite(dir, tool, key, text, now);
|
|
353
|
+
else mcpCacheWrite(dir, tool, key, text, now);
|
|
213
354
|
}
|
|
214
355
|
//#endregion
|
|
215
356
|
//#region src/runtime/normalize.ts
|
|
@@ -536,6 +677,117 @@ function runDesignChecks(content) {
|
|
|
536
677
|
];
|
|
537
678
|
}
|
|
538
679
|
//#endregion
|
|
680
|
+
//#region src/policy/design/skill-gate.ts
|
|
681
|
+
/** UI source files the gate polices (.html/.css are handled by the pipeline gate, not here). */
|
|
682
|
+
const UI_FILE_RE = /\.(tsx|jsx|scss|vue|svelte)$/;
|
|
683
|
+
/** Generated/vendored paths that never need a design skill (match at start or mid-path). */
|
|
684
|
+
const EXEMPT_RE = /(^|\/)(node_modules|dist|build)\/|\.claude\//;
|
|
685
|
+
/** UI-intent path fragments (ports the Python `UI_PATH_PATTERNS`). */
|
|
686
|
+
const UI_PATH_RE = /(components|ui|styles|page|layout|content|view|feature|section|hero|footer|header|sidebar|nav|modal|dialog)/;
|
|
687
|
+
/** Tailwind utility usage in JSX `className` (ports the Python `has_jsx_tailwind`). */
|
|
688
|
+
const TAILWIND_RE = /className\s*=.*(?:flex|grid|p-|m-|bg-|text-|rounded|shadow|border|gap-|w-|h-)/;
|
|
689
|
+
/** A read reference path that counts as "a design skill was consulted". */
|
|
690
|
+
const DESIGN_SKILL_RE = /skills\/([0-9]-)?(designing-systems|generating-components|solid-react|solid-nextjs|design|impeccable|frontend-design)/i;
|
|
691
|
+
/** True when this Write/Edit targets a UI file in a UI-ish path or with Tailwind classes. */
|
|
692
|
+
function isUiWrite(tool, filePath, content) {
|
|
693
|
+
if (tool !== "Write" && tool !== "Edit") return false;
|
|
694
|
+
if (!UI_FILE_RE.test(filePath) || EXEMPT_RE.test(filePath)) return false;
|
|
695
|
+
return UI_PATH_RE.test(filePath) || TAILWIND_RE.test(content);
|
|
696
|
+
}
|
|
697
|
+
/** True when a design skill reference was read this session (scans the `refsRead` track). */
|
|
698
|
+
function designSkillRead(refsRead) {
|
|
699
|
+
return refsRead.some((p) => DESIGN_SKILL_RE.test(p));
|
|
700
|
+
}
|
|
701
|
+
const block$1 = (reason, action) => ({
|
|
702
|
+
kind: "block",
|
|
703
|
+
title: "Design skill",
|
|
704
|
+
reason,
|
|
705
|
+
actions: [action]
|
|
706
|
+
});
|
|
707
|
+
/**
|
|
708
|
+
* Gate a UI write: require a design-skill read AND any doc source. Returns a
|
|
709
|
+
* blocking {@link Prompt}, or `null` to allow. NEVER requires Gemini.
|
|
710
|
+
* @param tool - the tool name ("Write" | "Edit" | ...).
|
|
711
|
+
* @param filePath - the written file path.
|
|
712
|
+
* @param content - the written content (snippet on Edit, full file on Write).
|
|
713
|
+
* @param ev - the session evidence (refsRead + docConsulted).
|
|
714
|
+
*/
|
|
715
|
+
function uiDesignSkillGate(tool, filePath, content, ev) {
|
|
716
|
+
if (!isUiWrite(tool, filePath, content)) return null;
|
|
717
|
+
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");
|
|
718
|
+
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");
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
//#endregion
|
|
722
|
+
//#region src/policy/design/skill-evidence.ts
|
|
723
|
+
/**
|
|
724
|
+
* @module design/skill-evidence
|
|
725
|
+
* IO side of the UI design-skill gate: read the verified session track and
|
|
726
|
+
* derive {@link DesignEvidence} (read references + doc-consulted) for
|
|
727
|
+
* {@link uiDesignSkillGate}. Kept separate from the pure gate logic (SRP).
|
|
728
|
+
* @packageDocumentation
|
|
729
|
+
*/
|
|
730
|
+
/**
|
|
731
|
+
* Read the verified session track and derive {@link DesignEvidence}. Fail-closed
|
|
732
|
+
* on a missing/corrupt/tampered track (no evidence → the gate blocks).
|
|
733
|
+
* @param sessionId - the Claude session id.
|
|
734
|
+
* @param cwd - the project root (selects the per-project state dir).
|
|
735
|
+
* @param baseDir - override the track base dir (tests); defaults to the project state dir.
|
|
736
|
+
*/
|
|
737
|
+
function collectDesignEvidence(sessionId, cwd, baseDir = defaultStateDir(cwd)) {
|
|
738
|
+
let track = null;
|
|
739
|
+
try {
|
|
740
|
+
track = verifyTrack(JSON.parse(readFileSync(trackFile(sessionId, baseDir), "utf8")));
|
|
741
|
+
} catch {
|
|
742
|
+
track = null;
|
|
743
|
+
}
|
|
744
|
+
return {
|
|
745
|
+
refsRead: track?.refsRead ?? [],
|
|
746
|
+
docConsulted: track ? isDocConsulted(track.authorizations, sessionId) : false
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
//#endregion
|
|
750
|
+
//#region src/runtime/design-helpers.ts
|
|
751
|
+
/**
|
|
752
|
+
* @module design-helpers
|
|
753
|
+
* Effectful helpers for the design pipeline gate: locate design-system.md and
|
|
754
|
+
* apply PostToolUse fuse-browser transitions to the design state. Split out of
|
|
755
|
+
* `design.ts` to keep that file within the SOLID size budget (SRP).
|
|
756
|
+
* @packageDocumentation
|
|
757
|
+
*/
|
|
758
|
+
const NAV$1 = "mcp__fuse-browser__browser_navigate";
|
|
759
|
+
const SHOT$1 = "mcp__fuse-browser__browser_screenshot";
|
|
760
|
+
const SCROLL = "mcp__fuse-browser__browser_scroll";
|
|
761
|
+
const GEMINI$1 = "mcp__gemini-design__create_frontend";
|
|
762
|
+
/** Read design-system.md walking up to 6 parents from `cwd` ("" if absent/unreadable). */
|
|
763
|
+
function findDesignSystem(cwd) {
|
|
764
|
+
let dir = cwd;
|
|
765
|
+
for (let i = 0; i < 6; i++) {
|
|
766
|
+
const p = join(dir, "design-system.md");
|
|
767
|
+
if (existsSync(p)) try {
|
|
768
|
+
return readFileSync(p, "utf8");
|
|
769
|
+
} catch {
|
|
770
|
+
return "";
|
|
771
|
+
}
|
|
772
|
+
const parent = dirname(dir);
|
|
773
|
+
if (parent === dir) break;
|
|
774
|
+
dir = parent;
|
|
775
|
+
}
|
|
776
|
+
return "";
|
|
777
|
+
}
|
|
778
|
+
/** Apply a PostToolUse fuse-browser transition to the design state. */
|
|
779
|
+
function recordPost(event, cacheDir, state) {
|
|
780
|
+
if (event.tool === SHOT$1) saveDesignState(cacheDir, recordScreenshot(state, MIN_SCREENSHOTS[state.mode]));
|
|
781
|
+
else if (event.tool === NAV$1) saveDesignState(cacheDir, recordNavigate(state));
|
|
782
|
+
else if (event.tool === SCROLL) saveDesignState(cacheDir, recordScroll(state));
|
|
783
|
+
else if (event.tool === GEMINI$1) saveDesignState(cacheDir, {
|
|
784
|
+
...state,
|
|
785
|
+
geminiCalls: state.geminiCalls + 1
|
|
786
|
+
});
|
|
787
|
+
else if (event.tool === "Read") saveDesignState(cacheDir, recordRead(state, event.filePath ?? ""));
|
|
788
|
+
else if ((event.tool === "Write" || event.tool === "Edit") && (event.filePath ?? "").endsWith("design-system.md")) saveDesignState(cacheDir, recordValidDesignSystem(state));
|
|
789
|
+
}
|
|
790
|
+
//#endregion
|
|
539
791
|
//#region src/policy/design/gates.ts
|
|
540
792
|
const ALLOWED_WRITE = /\.(html|css|md|json)$/;
|
|
541
793
|
const EXEMPT_DIRS = [
|
|
@@ -620,48 +872,28 @@ function geminiEnabled() {
|
|
|
620
872
|
//#region src/runtime/design.ts
|
|
621
873
|
const NAV = "mcp__fuse-browser__browser_navigate";
|
|
622
874
|
const SHOT = "mcp__fuse-browser__browser_screenshot";
|
|
623
|
-
const SCROLL = "mcp__fuse-browser__browser_scroll";
|
|
624
875
|
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
876
|
/**
|
|
654
877
|
* Design-pipeline gate (effectful: reads/writes the design state + design-system.md).
|
|
655
878
|
* Returns a Prompt to block, or null when this isn't a design-agent context / nothing fires.
|
|
656
879
|
*/
|
|
657
880
|
function designGate(payload, event, cacheDir, cwd) {
|
|
881
|
+
if (event.phase !== "post" && (event.tool === "Write" || event.tool === "Edit")) {
|
|
882
|
+
const skillBlock = uiDesignSkillGate(event.tool, event.filePath ?? "", event.content ?? "", collectDesignEvidence(event.sessionId, cwd));
|
|
883
|
+
if (skillBlock) return skillBlock;
|
|
884
|
+
}
|
|
658
885
|
const agentId = typeof payload.agent_id === "string" ? payload.agent_id : "";
|
|
659
886
|
const active = activeDesignAgent(cacheDir);
|
|
660
887
|
if (active && agentId && agentId !== active) return null;
|
|
661
888
|
const id = active || agentId;
|
|
662
889
|
if (!id) return null;
|
|
663
|
-
|
|
664
|
-
if (!state)
|
|
890
|
+
let state = loadDesignState(cacheDir, id);
|
|
891
|
+
if (!state) {
|
|
892
|
+
if (!active) return null;
|
|
893
|
+
const dsExists = findDesignSystem(cwd) !== "";
|
|
894
|
+
state = initDesignState(id, dsExists ? "page" : "full", dsExists);
|
|
895
|
+
saveDesignState(cacheDir, state);
|
|
896
|
+
}
|
|
665
897
|
if (event.phase === "post") {
|
|
666
898
|
recordPost(event, cacheDir, state);
|
|
667
899
|
if ((event.tool === "Write" || event.tool === "Edit") && /\.(tsx|jsx|css)$/.test(event.filePath ?? "")) {
|
|
@@ -1129,23 +1361,24 @@ function recordHistory(home, agentId, agentType, ts) {
|
|
|
1129
1361
|
function trackAgentMemory(data, home = homedir(), now = Date.now()) {
|
|
1130
1362
|
mkdirSync(sessionsDir(home), { recursive: true });
|
|
1131
1363
|
const agentType = String(data.agent_type ?? data.subagent_type ?? "unknown");
|
|
1132
|
-
const sessionId =
|
|
1364
|
+
const sessionId = sanitizeSessionId(data.session_id) ?? "unknown";
|
|
1133
1365
|
const ts = new Date(now).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1134
1366
|
recordHistory(home, String(data.agent_id ?? "unknown"), agentType, ts);
|
|
1135
1367
|
if (SKIP_AGENTS.test(agentType)) return JSON.stringify({ message: `Agent ${agentType} completed` });
|
|
1136
|
-
const
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1368
|
+
const state = loadSessionState(sessionId, home);
|
|
1369
|
+
const changes = state.changes;
|
|
1370
|
+
const count = changes?.cumulativeCodeFiles ?? 0;
|
|
1371
|
+
if (count > 0) {
|
|
1372
|
+
const files = (changes?.modifiedFiles ?? []).join(", ");
|
|
1373
|
+
saveSessionState(sessionId, {
|
|
1374
|
+
...state,
|
|
1375
|
+
changes: {
|
|
1376
|
+
...changes,
|
|
1144
1377
|
cumulativeCodeFiles: 0
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1378
|
+
}
|
|
1379
|
+
}, home);
|
|
1380
|
+
return contextResponse("SubagentStop", `SNIPER VALIDATION REQUIRED: Agent '${agentType}' modified ${count} code file(s): ${files}. Run sniper agent now.`);
|
|
1381
|
+
}
|
|
1149
1382
|
return JSON.stringify({ message: `Agent ${agentType} completed (no code changes)` });
|
|
1150
1383
|
}
|
|
1151
1384
|
//#endregion
|
|
@@ -1160,14 +1393,9 @@ function trackAgentMemory(data, home = homedir(), now = Date.now()) {
|
|
|
1160
1393
|
*/
|
|
1161
1394
|
function validateTeammateOutput(data, home = homedir()) {
|
|
1162
1395
|
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 {}
|
|
1396
|
+
const changes = loadSessionState(sanitizeSessionId(data.session_id) ?? "unknown", home).changes;
|
|
1397
|
+
const count = changes?.cumulativeCodeFiles ?? 0;
|
|
1398
|
+
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
1399
|
return "";
|
|
1172
1400
|
}
|
|
1173
1401
|
//#endregion
|
|
@@ -1260,7 +1488,7 @@ function validateRulesLoaded(data, home = homedir()) {
|
|
|
1260
1488
|
//#endregion
|
|
1261
1489
|
//#region src/runtime/lifecycle/track-changes.ts
|
|
1262
1490
|
/** 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)$/;
|
|
1491
|
+
const CODE_EXT$1 = /\.(ts|tsx|js|jsx|py|go|rs|java|php|cpp|c|rb|swift|kt|vue|svelte|astro)$/;
|
|
1264
1492
|
/**
|
|
1265
1493
|
* Handle PostToolUse Write/Edit: track the cumulative set of modified code
|
|
1266
1494
|
* files per session and emit the mandatory "SNIPER VALIDATION REQUIRED"
|
|
@@ -1272,7 +1500,7 @@ const CODE_EXT = /\.(ts|tsx|js|jsx|py|go|rs|java|php|cpp|c|rb|swift|kt|vue|svelt
|
|
|
1272
1500
|
* @returns The native hook stdout (possibly empty when not a code file).
|
|
1273
1501
|
*/
|
|
1274
1502
|
function trackSessionChanges(sessionIdRaw, filePath, home = homedir(), now = Date.now()) {
|
|
1275
|
-
if (!filePath || !CODE_EXT.test(filePath)) return "";
|
|
1503
|
+
if (!filePath || !CODE_EXT$1.test(filePath)) return "";
|
|
1276
1504
|
const sid = sanitizeSessionId(sessionIdRaw) ?? "unknown";
|
|
1277
1505
|
const state = loadSessionState(sid, home);
|
|
1278
1506
|
const prev = state.changes ?? {
|
|
@@ -1354,6 +1582,66 @@ function postEditTypescript(filePath) {
|
|
|
1354
1582
|
return contextResponse("PostToolUse", `Lint issues in ${basename(filePath)}: ${issues.join(" | ")}`);
|
|
1355
1583
|
}
|
|
1356
1584
|
//#endregion
|
|
1585
|
+
//#region src/runtime/lifecycle/task-completed.ts
|
|
1586
|
+
/** Code-file extensions audited on task completion (mirrors validate-task-solid.py). */
|
|
1587
|
+
const CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1588
|
+
".ts",
|
|
1589
|
+
".tsx",
|
|
1590
|
+
".js",
|
|
1591
|
+
".jsx",
|
|
1592
|
+
".py",
|
|
1593
|
+
".go",
|
|
1594
|
+
".rs",
|
|
1595
|
+
".java",
|
|
1596
|
+
".php",
|
|
1597
|
+
".cpp",
|
|
1598
|
+
".c",
|
|
1599
|
+
".rb",
|
|
1600
|
+
".swift",
|
|
1601
|
+
".kt",
|
|
1602
|
+
".dart",
|
|
1603
|
+
".vue",
|
|
1604
|
+
".svelte",
|
|
1605
|
+
".astro"
|
|
1606
|
+
]);
|
|
1607
|
+
/**
|
|
1608
|
+
* Re-count physical lines of every modified code file and collect SOLID
|
|
1609
|
+
* violations (`<basename>: <n> lines (max <max>)`) for those exceeding `max`.
|
|
1610
|
+
* @param files - Candidate modified file paths.
|
|
1611
|
+
* @param max - The SOLID line ceiling.
|
|
1612
|
+
* @returns The list of violation strings (empty when all files comply).
|
|
1613
|
+
*/
|
|
1614
|
+
function collectViolations(files, max) {
|
|
1615
|
+
const violations = [];
|
|
1616
|
+
for (const fp of files) {
|
|
1617
|
+
if (!CODE_EXTENSIONS.has(extname(fp)) || !existsSync(fp)) continue;
|
|
1618
|
+
try {
|
|
1619
|
+
const lines = countLines(readFileSync(fp, "utf-8"));
|
|
1620
|
+
if (lines > max) violations.push(`${basename(fp)}: ${lines} lines (max ${max})`);
|
|
1621
|
+
} catch {}
|
|
1622
|
+
}
|
|
1623
|
+
return violations;
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Handle TaskCompleted: re-measure the session's modified code files and emit a
|
|
1627
|
+
* `SOLID VIOLATION` additionalContext listing any file over the line ceiling.
|
|
1628
|
+
* Ports `task-completed/validate-task-solid.py`.
|
|
1629
|
+
* @param payload - The TaskCompleted hook payload (`task_id`, `task_subject`, `session_id`).
|
|
1630
|
+
* @param home - Home dir (defaults to `~`).
|
|
1631
|
+
* @returns The native hook stdout, or `""` when there are no violations.
|
|
1632
|
+
*/
|
|
1633
|
+
function validateTaskSolid(payload, home = homedir()) {
|
|
1634
|
+
const sid = sanitizeSessionId(payload.session_id ?? "unknown");
|
|
1635
|
+
if (!sid) return "";
|
|
1636
|
+
const files = loadSessionState(sid, home).changes?.modifiedFiles ?? [];
|
|
1637
|
+
if (files.length === 0) return "";
|
|
1638
|
+
const max = resolveMaxLines();
|
|
1639
|
+
const violations = collectViolations(files, max);
|
|
1640
|
+
if (violations.length === 0) return "";
|
|
1641
|
+
const taskId = String(payload.task_id ?? "");
|
|
1642
|
+
return contextResponse("TaskCompleted", `SOLID VIOLATION in task '${String(payload.task_subject ?? "")}' (${taskId}): ${violations.length} file(s) exceed ${max} lines: ` + violations.slice(0, 5).join("; "));
|
|
1643
|
+
}
|
|
1644
|
+
//#endregion
|
|
1357
1645
|
//#region src/runtime/lifecycle/cartographer/fs-util.ts
|
|
1358
1646
|
/**
|
|
1359
1647
|
* Filesystem helpers for the cartographer tree walk. Ports the fs parts of
|
|
@@ -1575,18 +1863,63 @@ function readPluginMeta(pluginPath) {
|
|
|
1575
1863
|
/**
|
|
1576
1864
|
* Auto-detect the marketplace `plugins` dir that contains `cartographer`,
|
|
1577
1865
|
* falling back to the first marketplace with a `plugins` dir, else `cwd`.
|
|
1578
|
-
* Ports `find_marketplace_plugins
|
|
1866
|
+
* Ports `find_marketplace_plugins`, but harness-agnostic: the config dir is
|
|
1867
|
+
* derived from the detected harness (`.claude`, `.codex`, `.cursor`, …) via the
|
|
1868
|
+
* shared `HOME_DIR` mapping instead of a hardcoded `.claude`.
|
|
1579
1869
|
* @param home - Home directory (defaults to `~`).
|
|
1870
|
+
* @param id - Detected harness id (defaults to runtime detection).
|
|
1580
1871
|
* @returns The resolved plugins directory.
|
|
1581
1872
|
*/
|
|
1582
|
-
function findMarketplacePlugins(home = homedir()) {
|
|
1583
|
-
const mp = join(home, ".claude", "plugins", "marketplaces");
|
|
1873
|
+
function findMarketplacePlugins(home = homedir(), id = detectHarness().id) {
|
|
1874
|
+
const mp = join(home, HOME_DIR[id] ?? ".claude", "plugins", "marketplaces");
|
|
1584
1875
|
const markets = sortedNames$1(mp);
|
|
1585
1876
|
for (const m of markets) if (existsSync(join(mp, m, "plugins", "cartographer"))) return join(mp, m, "plugins");
|
|
1586
1877
|
for (const m of markets) if (existsSync(join(mp, m, "plugins"))) return join(mp, m, "plugins");
|
|
1587
1878
|
return process.cwd();
|
|
1588
1879
|
}
|
|
1589
1880
|
//#endregion
|
|
1881
|
+
//#region src/runtime/lifecycle/cartographer/scan-hooks.ts
|
|
1882
|
+
/**
|
|
1883
|
+
* Hook scanning (fs). Ports `_scan_hooks` from `scan_plugins.py`: reads a
|
|
1884
|
+
* plugin's `hooks/hooks.json` and reduces it to a single `("hooks", "<events>", "")`
|
|
1885
|
+
* row. The `hooks` value (or the whole document) is either a MAP of event →
|
|
1886
|
+
* entries (keys are the events) or a LIST of entries each carrying an `event`.
|
|
1887
|
+
*/
|
|
1888
|
+
/** True for a non-null plain object (excludes arrays), mirroring Python `isinstance(x, dict)`. */
|
|
1889
|
+
function isPlainObject(v) {
|
|
1890
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1891
|
+
}
|
|
1892
|
+
/** Sort unique non-empty strings alpha (byte-order). */
|
|
1893
|
+
function sortedEvents(events) {
|
|
1894
|
+
return [...new Set([...events].filter((e) => e))].sort((a, b) => a.localeCompare(b, "en"));
|
|
1895
|
+
}
|
|
1896
|
+
/** Derive hook event names from the `hooks.json` data (map keys, or list `event` fields). */
|
|
1897
|
+
function hookEvents(hooksData) {
|
|
1898
|
+
if (isPlainObject(hooksData)) return sortedEvents(Object.keys(hooksData).filter((k) => !k.startsWith("_")));
|
|
1899
|
+
if (Array.isArray(hooksData)) return sortedEvents(hooksData.map((h) => isPlainObject(h) ? String(h.event ?? "") : ""));
|
|
1900
|
+
return [];
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Scan `hooks/hooks.json` into a single `("hooks", "<events>", "")` row.
|
|
1904
|
+
* @param root - Absolute plugin directory.
|
|
1905
|
+
* @returns The single hooks row, or `[]` when absent/empty/unreadable.
|
|
1906
|
+
*/
|
|
1907
|
+
function scanHooks(root) {
|
|
1908
|
+
const file = join(root, "hooks", "hooks.json");
|
|
1909
|
+
if (!existsSync(file)) return [];
|
|
1910
|
+
try {
|
|
1911
|
+
const raw = JSON.parse(readFileSync(file, "utf-8"));
|
|
1912
|
+
const events = hookEvents(isPlainObject(raw) ? raw.hooks ?? raw : {});
|
|
1913
|
+
return events.length ? [[
|
|
1914
|
+
"hooks",
|
|
1915
|
+
events.join(", "),
|
|
1916
|
+
""
|
|
1917
|
+
]] : [];
|
|
1918
|
+
} catch {
|
|
1919
|
+
return [];
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
//#endregion
|
|
1590
1923
|
//#region src/runtime/lifecycle/cartographer/scan.ts
|
|
1591
1924
|
/**
|
|
1592
1925
|
* Plugin scanning (fs). Ports `scan_plugins.py`: turns a plugin's
|
|
@@ -1655,24 +1988,6 @@ function scanCommands(root) {
|
|
|
1655
1988
|
fileField(join(dir, n), "description").slice(0, 50)
|
|
1656
1989
|
]);
|
|
1657
1990
|
}
|
|
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
1991
|
/**
|
|
1677
1992
|
* Scan a single plugin directory into ordered `[type, name, desc]` rows.
|
|
1678
1993
|
* @param pluginDir - Absolute plugin directory.
|
|
@@ -1853,6 +2168,15 @@ function generateEcosystemMap(now, pluginsDirOverride) {
|
|
|
1853
2168
|
* navigation context from the latter as additionalContext.
|
|
1854
2169
|
*/
|
|
1855
2170
|
/**
|
|
2171
|
+
* Resolve the marketplace plugins dir from `CLAUDE_PLUGIN_ROOT`, mirroring the
|
|
2172
|
+
* Python hook which passes `${CLAUDE_PLUGIN_ROOT}/..` to `generate_map.py`.
|
|
2173
|
+
* @returns The plugins dir (env `/..`), or `undefined` to fall back to auto-detect.
|
|
2174
|
+
*/
|
|
2175
|
+
function pluginsDirFromEnv() {
|
|
2176
|
+
const root = process.env.CLAUDE_PLUGIN_ROOT;
|
|
2177
|
+
return root ? resolve(root, "..") : void 0;
|
|
2178
|
+
}
|
|
2179
|
+
/**
|
|
1856
2180
|
* Regenerate the project map + plugin ecosystem map for `cwd` on SessionStart.
|
|
1857
2181
|
* Emits the ecosystem navigation context as additionalContext (or "").
|
|
1858
2182
|
* @param cwd - The working directory.
|
|
@@ -1861,7 +2185,7 @@ function generateEcosystemMap(now, pluginsDirOverride) {
|
|
|
1861
2185
|
*/
|
|
1862
2186
|
function cartoSessionStart(cwd, now = Date.now()) {
|
|
1863
2187
|
generateProjectMap(cwd);
|
|
1864
|
-
const ctx = generateEcosystemMap(now);
|
|
2188
|
+
const ctx = generateEcosystemMap(now, pluginsDirFromEnv());
|
|
1865
2189
|
return ctx ? contextResponse("SessionStart", ctx) : "";
|
|
1866
2190
|
}
|
|
1867
2191
|
//#endregion
|
|
@@ -2658,12 +2982,101 @@ async function cacheDocFromTranscript(transcript, cwd, home = homedir()) {
|
|
|
2658
2982
|
await writeJsonFile(indexFile, index, true);
|
|
2659
2983
|
}
|
|
2660
2984
|
//#endregion
|
|
2985
|
+
//#region src/runtime/lifecycle/aipilot/promote-global-lessons.ts
|
|
2986
|
+
/**
|
|
2987
|
+
* Promote high-frequency project lessons (occurrence count >= 3) to the global,
|
|
2988
|
+
* stack-scoped cache (`fusengine-cache/lessons/_global/<stack>.json`, cap 25).
|
|
2989
|
+
* Ports the ai-pilot `promote-global-lessons.ts` (a spawned subprocess) as an
|
|
2990
|
+
* in-process call from `cacheSniperLessons`. Pure side-effect.
|
|
2991
|
+
*/
|
|
2992
|
+
const MIN_COUNT = 3;
|
|
2993
|
+
const MAX_GLOBAL = 25;
|
|
2994
|
+
/** Group lessons by `error_type:pattern`. */
|
|
2995
|
+
function groupByKey(lessons) {
|
|
2996
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2997
|
+
for (const l of lessons) {
|
|
2998
|
+
const key = `${l.error_type}:${l.pattern}`;
|
|
2999
|
+
(groups.get(key) ?? groups.set(key, []).get(key)).push(l);
|
|
3000
|
+
}
|
|
3001
|
+
return [...groups.values()];
|
|
3002
|
+
}
|
|
3003
|
+
/** Read every project lesson file's `errors` into one flat (un-deduped) list. */
|
|
3004
|
+
async function aggregateErrors(cacheDir) {
|
|
3005
|
+
let files;
|
|
3006
|
+
try {
|
|
3007
|
+
files = readdirSync(cacheDir).filter((f) => f.endsWith(".json"));
|
|
3008
|
+
} catch {
|
|
3009
|
+
return [];
|
|
3010
|
+
}
|
|
3011
|
+
const all = [];
|
|
3012
|
+
for (const f of files) {
|
|
3013
|
+
const data = await readJsonFile(join(cacheDir, f));
|
|
3014
|
+
if (data?.errors) all.push(...data.errors);
|
|
3015
|
+
}
|
|
3016
|
+
return all;
|
|
3017
|
+
}
|
|
3018
|
+
/** Keep groups seen >= MIN_COUNT times, collapsed to a single lesson each. */
|
|
3019
|
+
function filterFrequent(lessons) {
|
|
3020
|
+
return groupByKey(lessons).filter((e) => e.length >= MIN_COUNT).map((entries) => {
|
|
3021
|
+
const first = entries[0];
|
|
3022
|
+
return {
|
|
3023
|
+
error_type: first.error_type,
|
|
3024
|
+
pattern: first.pattern,
|
|
3025
|
+
fix: first.fix,
|
|
3026
|
+
last_seen: first.last_seen,
|
|
3027
|
+
count: entries.length,
|
|
3028
|
+
files: [...new Set(entries.flatMap((e) => e.files))],
|
|
3029
|
+
code: { line: [...new Set(entries.flatMap((e) => e.code?.line ?? []))].slice(0, 5) }
|
|
3030
|
+
};
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
/** Merge tagged candidates into existing global lessons; dedup, sum, sort, cap. */
|
|
3034
|
+
function mergeGlobal(existing, candidates, projHash) {
|
|
3035
|
+
const tagged = candidates.map((c) => ({
|
|
3036
|
+
...c,
|
|
3037
|
+
source_projects: [projHash]
|
|
3038
|
+
}));
|
|
3039
|
+
return groupByKey([...existing, ...tagged]).map((entries) => {
|
|
3040
|
+
const first = entries[0];
|
|
3041
|
+
return {
|
|
3042
|
+
error_type: first.error_type,
|
|
3043
|
+
pattern: first.pattern,
|
|
3044
|
+
fix: first.fix,
|
|
3045
|
+
last_seen: first.last_seen,
|
|
3046
|
+
count: entries.reduce((sum, e) => sum + e.count, 0),
|
|
3047
|
+
files: [...new Set(entries.flatMap((e) => e.files))],
|
|
3048
|
+
code: { line: [...new Set(entries.flatMap((e) => e.code?.line ?? []))].slice(0, 5) },
|
|
3049
|
+
source_projects: [...new Set(entries.flatMap((e) => e.source_projects ?? []))]
|
|
3050
|
+
};
|
|
3051
|
+
}).sort((a, b) => b.count - a.count).slice(0, MAX_GLOBAL);
|
|
3052
|
+
}
|
|
3053
|
+
/**
|
|
3054
|
+
* Promote high-frequency lessons from `cacheDir` to the global stack file.
|
|
3055
|
+
* @param cacheDir - Per-project lessons cache dir to scan.
|
|
3056
|
+
* @param stack - Detected project stack (`nextjs`, `laravel`, …, `universal`).
|
|
3057
|
+
* @param projHash - Source project hash (tags promoted entries).
|
|
3058
|
+
* @param home - Home dir (defaults to `~`).
|
|
3059
|
+
*/
|
|
3060
|
+
async function promoteGlobalLessons(cacheDir, stack, projHash, home = homedir()) {
|
|
3061
|
+
const candidates = filterFrequent(await aggregateErrors(cacheDir));
|
|
3062
|
+
if (candidates.length === 0) return;
|
|
3063
|
+
const globalDir = join(cacheBaseDir(home), "lessons", "_global");
|
|
3064
|
+
mkdirSync(globalDir, { recursive: true });
|
|
3065
|
+
const globalFile = join(globalDir, `${stack}.json`);
|
|
3066
|
+
const merged = mergeGlobal(await readJsonFile(globalFile) ?? [], candidates, projHash);
|
|
3067
|
+
await writeJsonFile(globalFile, merged, true);
|
|
3068
|
+
logCacheEvent("lessons", "promoted", projHash, {
|
|
3069
|
+
count: merged.length,
|
|
3070
|
+
stack
|
|
3071
|
+
}, home);
|
|
3072
|
+
}
|
|
3073
|
+
//#endregion
|
|
2661
3074
|
//#region src/runtime/lifecycle/aipilot/cache-lessons.ts
|
|
2662
3075
|
/**
|
|
2663
3076
|
* SubagentStop (matcher "sniper") for the ai-pilot scope: capture error
|
|
2664
3077
|
* patterns + corrected code from the sniper transcript as lessons. Ports
|
|
2665
|
-
* `cache-sniper-lessons.ts
|
|
2666
|
-
*
|
|
3078
|
+
* `cache-sniper-lessons.ts`, including the high-frequency promotion to the
|
|
3079
|
+
* global stack cache (`promoteGlobalLessons`). Pure side-effect.
|
|
2667
3080
|
*/
|
|
2668
3081
|
/**
|
|
2669
3082
|
* SubagentStop sniper: extract lessons from the transcript and persist them.
|
|
@@ -2700,6 +3113,7 @@ async function cacheSniperLessons(transcript, cwd, home = homedir()) {
|
|
|
2700
3113
|
timestamp,
|
|
2701
3114
|
errors
|
|
2702
3115
|
}, true);
|
|
3116
|
+
await promoteGlobalLessons(cacheDir, detectStack(projectPath), pHash, home);
|
|
2703
3117
|
logCacheEvent("lessons", "hit", pHash, { count: edits.length }, home);
|
|
2704
3118
|
}
|
|
2705
3119
|
//#endregion
|
|
@@ -2924,12 +3338,12 @@ function contextTextOf(response) {
|
|
|
2924
3338
|
}
|
|
2925
3339
|
}
|
|
2926
3340
|
/**
|
|
2927
|
-
* Combine
|
|
2928
|
-
*
|
|
2929
|
-
* call must do that join itself). Returns "" when
|
|
3341
|
+
* Combine N SubagentStart responses into one (Claude concatenates every hook's
|
|
3342
|
+
* additionalContext; collapsing the matcher-"" + type-specific scripts into one
|
|
3343
|
+
* dispatch call must do that join itself). Returns "" when all are empty.
|
|
2930
3344
|
*/
|
|
2931
|
-
function combineContext(
|
|
2932
|
-
const parts =
|
|
3345
|
+
function combineContext(...responses) {
|
|
3346
|
+
const parts = responses.map(contextTextOf).filter(Boolean);
|
|
2933
3347
|
return parts.length ? contextResponse("SubagentStart", parts.join("\n\n")) : "";
|
|
2934
3348
|
}
|
|
2935
3349
|
/** The transcript path a SubagentStop payload reports (Stop-only field). */
|
|
@@ -2937,13 +3351,22 @@ function transcriptOf(payload) {
|
|
|
2937
3351
|
const t = payload.agent_transcript_path;
|
|
2938
3352
|
return typeof t === "string" ? t : void 0;
|
|
2939
3353
|
}
|
|
2940
|
-
/** SubagentStart
|
|
2941
|
-
async function
|
|
2942
|
-
const agent = agentTypeOf(payload);
|
|
3354
|
+
/** Type-specific SubagentStart cache for an agent, or "" when none applies. */
|
|
3355
|
+
async function typeSpecificCache(agent, cwd, now) {
|
|
2943
3356
|
if (agent.includes("explore-codebase")) return injectExploreCache(cwd, void 0, now);
|
|
2944
3357
|
if (agent.includes("research-expert")) return injectDocCache(cwd, void 0, now);
|
|
2945
3358
|
if (agent.includes("sniper")) return injectTestCache(cwd, void 0, now);
|
|
2946
|
-
return
|
|
3359
|
+
return "";
|
|
3360
|
+
}
|
|
3361
|
+
/**
|
|
3362
|
+
* SubagentStart routing. Parity with the Python ai-pilot hooks: the two
|
|
3363
|
+
* matcher-"" entries (APEX context + lessons "known issues") fire for EVERY
|
|
3364
|
+
* sub-agent, then the type-specific cache (explore/doc/test) is concatenated on
|
|
3365
|
+
* top. Sniper must still receive the lessons block — hence no early return.
|
|
3366
|
+
*/
|
|
3367
|
+
async function onSubagentStart(payload, cwd, now) {
|
|
3368
|
+
const agent = agentTypeOf(payload);
|
|
3369
|
+
return combineContext(await injectApexSubagentContext(cwd), await injectLessonsCache(cwd, void 0, now), await typeSpecificCache(agent, cwd, now));
|
|
2947
3370
|
}
|
|
2948
3371
|
/** SubagentStop routing: transcript-driven cache writers (side-effects). */
|
|
2949
3372
|
async function onSubagentStop(payload, cwd) {
|
|
@@ -3014,6 +3437,7 @@ function dispatchLifecycle(input) {
|
|
|
3014
3437
|
case "InstructionsLoaded":
|
|
3015
3438
|
validateRulesLoaded(input.payload);
|
|
3016
3439
|
return "";
|
|
3440
|
+
case "TaskCompleted": return validateTaskSolid(input.payload);
|
|
3017
3441
|
default: return null;
|
|
3018
3442
|
}
|
|
3019
3443
|
}
|
|
@@ -3465,11 +3889,11 @@ async function dispatchMemory(event, payload, cwd, now) {
|
|
|
3465
3889
|
* missing. Opt-in only — silent when no marker / non-HTML / file unreadable.
|
|
3466
3890
|
*/
|
|
3467
3891
|
/**
|
|
3468
|
-
* Validate the edited file's SEO completeness. Returns a
|
|
3469
|
-
* `
|
|
3892
|
+
* Validate the edited file's SEO completeness. Returns a block message (for a
|
|
3893
|
+
* top-level `decision: block` response) when HTML-like, opted-in, and missing
|
|
3470
3894
|
* elements; otherwise `null` (allow).
|
|
3471
3895
|
* @param payload - The raw PostToolUse payload.
|
|
3472
|
-
* @returns The
|
|
3896
|
+
* @returns The block reason string, or `null` to allow.
|
|
3473
3897
|
*/
|
|
3474
3898
|
function seoPostToolUse(payload) {
|
|
3475
3899
|
const path = payload.tool_input?.file_path;
|
|
@@ -3484,14 +3908,16 @@ function seoPostToolUse(payload) {
|
|
|
3484
3908
|
}
|
|
3485
3909
|
}
|
|
3486
3910
|
/**
|
|
3487
|
-
* SEO PostToolUse as a ready native response: a `
|
|
3911
|
+
* SEO PostToolUse as a ready native response: a top-level `decision: block`
|
|
3488
3912
|
* string when the edited file is missing SEO elements, else `null` (allow).
|
|
3913
|
+
* PostToolUse ignores `permissionDecision` (PreToolUse-only), so this must use
|
|
3914
|
+
* the `decision`/`reason` keys to actually feed the failure back to Claude.
|
|
3489
3915
|
* @param payload - The raw PostToolUse payload.
|
|
3490
|
-
* @returns The
|
|
3916
|
+
* @returns The block response string, or `null` to allow.
|
|
3491
3917
|
*/
|
|
3492
3918
|
function seoPostToolUseResponse(payload) {
|
|
3493
|
-
const
|
|
3494
|
-
return
|
|
3919
|
+
const block = seoPostToolUse(payload);
|
|
3920
|
+
return block ? blockResponse(block) : null;
|
|
3495
3921
|
}
|
|
3496
3922
|
//#endregion
|
|
3497
3923
|
//#region src/runtime/lifecycle-bridge.ts
|
|
@@ -3535,6 +3961,26 @@ function postEditContext(scope, event, now) {
|
|
|
3535
3961
|
}
|
|
3536
3962
|
//#endregion
|
|
3537
3963
|
//#region src/runtime/gate-helpers.ts
|
|
3964
|
+
/** Source extensions the APEX gates apply to (parity: require-apex-agents.py CODE_EXT). */
|
|
3965
|
+
const CODE_EXT = /\.(ts|tsx|js|jsx|py|go|rs|java|php|cpp|c|rb|swift|kt|dart|vue|svelte|astro)$/;
|
|
3966
|
+
/** Paths exempt from the APEX gates even when they are code (parity: require-apex-agents.py EXEMPT_PATTERNS). */
|
|
3967
|
+
const EXEMPT_PATTERNS = [
|
|
3968
|
+
/\.claude-plugin\//,
|
|
3969
|
+
/CHANGELOG\.md$/,
|
|
3970
|
+
/marketplace\.json$/,
|
|
3971
|
+
/\/\.claude\/(apex|memory|logs|fusengine-cache)\//
|
|
3972
|
+
];
|
|
3973
|
+
/**
|
|
3974
|
+
* True when `filePath` is a code file the APEX freshness/doc/SOLID gates should
|
|
3975
|
+
* police — i.e. it matches a tracked source extension AND no exempt pattern.
|
|
3976
|
+
* Mirrors require-apex-agents.py so non-code (and exempt) paths skip those gates.
|
|
3977
|
+
* @param filePath - Absolute path of the edit target (or undefined).
|
|
3978
|
+
* @returns Whether the APEX gates apply to this path.
|
|
3979
|
+
*/
|
|
3980
|
+
function isApexScoped(filePath) {
|
|
3981
|
+
if (!filePath || !CODE_EXT.test(filePath)) return false;
|
|
3982
|
+
return !EXEMPT_PATTERNS.some((p) => p.test(filePath));
|
|
3983
|
+
}
|
|
3538
3984
|
/**
|
|
3539
3985
|
* Code-only line count of the existing on-disk file (undefined if
|
|
3540
3986
|
* absent/unreadable). Uses {@link countLines} (skips blank/comment lines) so a
|
|
@@ -3556,7 +4002,7 @@ function existingLineCount(path) {
|
|
|
3556
4002
|
//#region src/freshness/agent-evidence.ts
|
|
3557
4003
|
/**
|
|
3558
4004
|
* Platform-authored transcript evidence for APEX agent freshness.
|
|
3559
|
-
* Parses the Claude Code session JSONL transcript to find genuine Task
|
|
4005
|
+
* Parses the Claude Code session JSONL transcript to find genuine `Task`/`Agent`
|
|
3560
4006
|
* tool_use entries — forging this requires writing into the transcript file
|
|
3561
4007
|
* which the Claude Code platform controls, unlike the self-recorded track.
|
|
3562
4008
|
*/
|
|
@@ -3569,9 +4015,9 @@ function parseTs(raw) {
|
|
|
3569
4015
|
}
|
|
3570
4016
|
/**
|
|
3571
4017
|
* 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`.
|
|
4018
|
+
* transcript at `transcriptPath` contains a genuine `tool_use` of the `Task` or
|
|
4019
|
+
* `Agent` tool whose `subagent_type` (or `name`) input field matches — after
|
|
4020
|
+
* stripping any plugin prefix — with the entry timestamp within `windowMs` of `now`.
|
|
3575
4021
|
*
|
|
3576
4022
|
* **Timestamp note:** when a transcript entry carries no `timestamp` field it
|
|
3577
4023
|
* is counted as within-window (we cannot prove staleness). This is
|
|
@@ -3609,9 +4055,10 @@ function agentsRanFromTranscript(transcriptPath, names, windowMs, now) {
|
|
|
3609
4055
|
const content = entry.message?.content;
|
|
3610
4056
|
if (!Array.isArray(content)) continue;
|
|
3611
4057
|
for (const block of content) {
|
|
3612
|
-
if (block?.type !== "tool_use" || block.name !== "Task") continue;
|
|
3613
|
-
const
|
|
3614
|
-
|
|
4058
|
+
if (block?.type !== "tool_use" || block.name !== "Task" && block.name !== "Agent") continue;
|
|
4059
|
+
const raw = block.input?.subagent_type ?? block.input?.name;
|
|
4060
|
+
const agent = typeof raw === "string" ? raw.split(":").pop() ?? raw : void 0;
|
|
4061
|
+
if (agent !== void 0 && names.includes(agent)) found.add(agent);
|
|
3615
4062
|
}
|
|
3616
4063
|
if (found.size === names.length) return true;
|
|
3617
4064
|
}
|
|
@@ -3958,6 +4405,8 @@ const TRIVIAL_BUDGET = 4;
|
|
|
3958
4405
|
* track. Returns the first blocking prompt, or null to allow.
|
|
3959
4406
|
*/
|
|
3960
4407
|
async function gate(input) {
|
|
4408
|
+
const precommit = preCommitGate(input.tool, input.command, input.cwd);
|
|
4409
|
+
if (precommit) return precommit;
|
|
3961
4410
|
const existingLines = existingLineCount(input.filePath);
|
|
3962
4411
|
let quick;
|
|
3963
4412
|
try {
|
|
@@ -3973,8 +4422,6 @@ async function gate(input) {
|
|
|
3973
4422
|
return FAIL_CLOSED;
|
|
3974
4423
|
}
|
|
3975
4424
|
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
4425
|
const modular = modularGate(input.tool, input.filePath, input.content, input.cwd);
|
|
3979
4426
|
if (modular) return modular;
|
|
3980
4427
|
if (!input.filePath) return null;
|
|
@@ -3982,29 +4429,31 @@ async function gate(input) {
|
|
|
3982
4429
|
const track = await loadTrack(input.trackFile);
|
|
3983
4430
|
const solidOrSkill = frameworkSkillGate(input, track.refsRead, existingLines);
|
|
3984
4431
|
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
|
-
|
|
4432
|
+
if (isApexScoped(input.filePath)) {
|
|
4433
|
+
const lineCount = input.content === void 0 ? Number.POSITIVE_INFINITY : input.content.split("\n").length;
|
|
4434
|
+
if (!input.isReplaceAll && lineCount < 5 && trivialCount(track, window, input.now) < 4) {
|
|
4435
|
+
await saveTrack(input.trackFile, recordTrivialEdit(track, input.now, window, input.now));
|
|
4436
|
+
return null;
|
|
4437
|
+
}
|
|
4438
|
+
const freshnessFor = (names, windowMs = window) => input.transcriptPath ? agentsRanFromTranscript(input.transcriptPath, names, windowMs, input.now) : agentsFresh(track, names, windowMs, input.now);
|
|
4439
|
+
const ctx = {
|
|
4440
|
+
sessionId: input.sessionId,
|
|
4441
|
+
framework: input.framework,
|
|
4442
|
+
filePath: input.filePath,
|
|
4443
|
+
content: input.content ?? "",
|
|
4444
|
+
authorizations: track.authorizations,
|
|
4445
|
+
refs: input.refs,
|
|
4446
|
+
refsRead: track.refsRead,
|
|
4447
|
+
agentsFresh: freshnessFor([...REQUIRED_AGENTS]),
|
|
4448
|
+
brainstormRequired: track.brainstormRequired,
|
|
4449
|
+
brainstormFresh: freshnessFor(["brainstorming"], Number.MAX_SAFE_INTEGER)
|
|
4450
|
+
};
|
|
4451
|
+
try {
|
|
4452
|
+
const apex = evaluateApex(ctx);
|
|
4453
|
+
if (apex) return apex;
|
|
4454
|
+
} catch {
|
|
4455
|
+
return FAIL_CLOSED;
|
|
4456
|
+
}
|
|
4008
4457
|
}
|
|
4009
4458
|
return dryGate(input.tool, input.filePath, input.content, input.cwd);
|
|
4010
4459
|
}
|
|
@@ -4137,7 +4586,7 @@ async function handleHook(id, payload, opts) {
|
|
|
4137
4586
|
const response = payload.tool_response ?? payload.tool_output;
|
|
4138
4587
|
mcpPostStore(event.tool, event.input, response, mcpDir);
|
|
4139
4588
|
const designWarn = designGate(payload, event, mcpDir, opts.cwd);
|
|
4140
|
-
const
|
|
4589
|
+
const activities = activityFor({
|
|
4141
4590
|
tool: event.tool,
|
|
4142
4591
|
input: event.input,
|
|
4143
4592
|
sessionId: event.sessionId,
|
|
@@ -4145,7 +4594,7 @@ async function handleHook(id, payload, opts) {
|
|
|
4145
4594
|
now: opts.now,
|
|
4146
4595
|
responseLength: extractText(response).length
|
|
4147
4596
|
});
|
|
4148
|
-
|
|
4597
|
+
for (const activity of activities) await recordActivity(file, activity);
|
|
4149
4598
|
postTrackingSideEffects(opts.scope ?? "core", event, event.input, opts.now, payload, opts.cwd);
|
|
4150
4599
|
const seoDeny = opts.scope === "seo" ? seoPostToolUseResponse(payload) : null;
|
|
4151
4600
|
if (seoDeny) return {
|
|
@@ -4176,4 +4625,4 @@ async function handleHook(id, payload, opts) {
|
|
|
4176
4625
|
});
|
|
4177
4626
|
}
|
|
4178
4627
|
//#endregion
|
|
4179
|
-
export { sessionStartCore as $, writePluginMap as A,
|
|
4628
|
+
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 };
|