@fusengine/harness 0.1.67 → 0.1.69

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 CHANGED
@@ -125,7 +125,7 @@ also works elsewhere.
125
125
  | Harness | PreToolUse coverage | Lifecycle (Session/Subagent/Stop/Compact/…) | Known limit |
126
126
  |---|---|---|---|
127
127
  | **claude-code** | Full: `evaluate` + APEX gates via `handleHook` (`src/adapters/claude/index.ts`) | 14 event types implemented (`dispatch.ts`) — fires once wired into `.claude/settings.json` beyond the `init` default | None found; richer lifecycle needs manual/marketplace wiring (see Quickstart) |
128
- | **codex** | Bash gated reliably; **`apply_patch` edits are now gated too** — the patch text is parsed per file (`adapters/codex/apply-patch.ts`), each hunk runs the file gates and ONE violating hunk denies the whole patch (`runtime/apply-patch-gate.ts`, sim scenario 22 incl. the multi-file smuggling case). `ask` prompts are **downgraded to explicit deny** (`respond.ts`, sim scenario 23) because Codex fails open on unsupported shapes. | Not wired by `harness init codex` (PreToolUse `Bash\|apply_patch` + PostToolUse only, `src/init/templates.ts:29-38`) | Upstream caveat: Codex itself does not always enforce a correct `apply_patch` deny (openai/codex#27833) — we emit the right verdict; enforcement is theirs. No interactive `ask`. |
128
+ | **codex** | Bash gated reliably; **`apply_patch` edits are now gated too** — the patch text is parsed per file (`adapters/codex/apply-patch.ts`), each hunk runs the file gates and ONE violating hunk denies the whole patch (`runtime/apply-patch-gate.ts`, sim scenario 22 incl. the multi-file smuggling case). `ask` prompts are **downgraded to explicit deny** (`respond.ts`, sim scenario 23) because Codex fails open on unsupported shapes. On the PostToolUse side, an allowed patch is fanned into one synthetic per-file event so the SOLID/tracking/post-edit handlers see every touched file (`runtime/post-fanout.ts`), and a non-blocking security advisory fires on the first qualifying file (`securityAdvisoryForPatch`). | Not wired by `harness init codex` (PreToolUse `Bash\|apply_patch` + PostToolUse only, `src/init/templates.ts:29-38`) — `Stop` (session cleanup + the SOLID/receipt completion check, since Codex never emits `SessionEnd`/`TaskCompleted`, `runtime/lifecycle/stop-core.ts`) and `SessionStart` (plugin agents/commands cache resync, `runtime/lifecycle/codex-resync/`) ARE implemented but, same caveat as claude-code's extra lifecycle above, only fire once the codex-plugins marketplace's own `hooks.json` wires them. | Upstream caveat: Codex itself does not always enforce a correct `apply_patch` deny (openai/codex#27833) — we emit the right verdict; enforcement is theirs. No interactive `ask`. Codex has no native `PostToolUseFailure`; a failure is inferred from the ordinary `PostToolUse` result shape (non-zero exit or an explicit error field only — never a guess) and journaled into the one-shot failure tally (`src/tracking/codex-post-failure.ts`). |
129
129
  | **cursor** | `beforeShellExecution` can deny/ask (shell only, `cursor/index.ts:16-21`) | none | File edits are **advisory only**: `afterFileEdit` always returns `allow` + a `user_message` correction on violation — a `deny` there has no proven effect (hook was "informational only" at launch, and Cursor's deny-enforcement for file ops is confirmed broken upstream, forum.cursor.com/t/154377). Human sees the message; the model is never re-informed. Platform ceiling, documented in `cursor/index.ts`. |
130
130
  | **gemini-cli** | `BeforeTool` denies via `{decision:"deny",reason}` (`gemini/index.ts:22-36`) | none | Thin stateless adapter — no session track, no APEX gates wired through it. |
131
131
  | **cline** | `PreToolUse` only; block → `{cancel:true}`, non-block → `contextModification` (`cline/index.ts:24-36`) | none | Same as gemini-cli: stateless guard only. |
@@ -198,6 +198,72 @@ Features shipped since 0.1.44, each with its own test:
198
198
  - **Hook simulator** — 18 end-to-end scenarios (payload in, expected verdict out)
199
199
  replayed against the real CLI in both `src` and built `dist` modes in CI
200
200
  (`test/sim/README.md`, `test/sim/scenarios/`).
201
+ - **Codex `multi_agent_v2` custom-agent evidence** — a `spawn_agent` call
202
+ carrying `tool_input.agent_type` is recorded as `subagent-<agent_type>` in the
203
+ same SessionTrack as Claude Task evidence, so a Codex explore-codebase/sniper
204
+ satisfies the APEX freshness gates exactly like a Claude subagent
205
+ (`src/tracking/session-state.ts`, sim scenario 32b).
206
+ - **File-size gate judges the real Edit outcome, not the on-disk count** — an
207
+ Edit on a file already over `FUSE_SOLID_MAX_LINES` used to be denied
208
+ unconditionally (the on-disk count always won); the gate now computes the
209
+ actual post-edit line count and allows a shrinking Edit while still denying a
210
+ computable growth past the limit (`src/policy/file-size.ts`, sim scenario 33).
211
+ - **Codex `apply_patch` fan-out to post-handlers** — each file in a patch
212
+ envelope becomes a synthetic per-file PostToolUse event (`add`→`Write`,
213
+ `update`→`Edit`, `delete` no-ops on the Write/Edit-only handlers) instead of
214
+ the whole-patch shape whose own `filePath`/`content` are always undefined
215
+ (`src/runtime/post-fanout.ts`, `test/apply-patch-post.test.ts`).
216
+ - **Non-blocking multi-file security advisory** — the same PreToolUse "read the
217
+ security skill" nudge a single Write/Edit gets is now evaluated per file
218
+ across a Codex `apply_patch` envelope, firing on the first qualifying
219
+ add/update code file (delete ignored outright) — `additionalContext` only,
220
+ never a deny (`src/runtime/lifecycle/security/check-skill.ts::securityAdvisoryForPatch`,
221
+ `test/security-advisory-patch.test.ts`).
222
+ - **Codex SessionStart agent/command resync** — the plugin agents/commands
223
+ cache under `~/.codex/{agents,prompts}` is re-materialized only when its
224
+ sha256 fingerprint changed (or a command symlink dangles) since the last
225
+ apply, otherwise a cheap no-op; agents are copied (Codex won't load
226
+ symlinked TOMLs), commands are symlinked; a best-effort inter-process lock
227
+ avoids a torn cache under concurrent session starts (`src/runtime/lifecycle/codex-resync/`,
228
+ `test/resync-agents.test.ts`).
229
+ - **Codex failure tracking inferred from PostToolUse** — Codex reports every
230
+ outcome, success or failure, on the same `PostToolUse` event; a failure is
231
+ classified from an explicit signal only (non-zero exit, an error field —
232
+ never "can't tell") and journaled into the existing one-shot failure tally
233
+ (`src/tracking/codex-post-failure.ts`, `test/codex-post-failure.test.ts`).
234
+ - **Shell-credited ref reads** — a `.md` SOLID/skill reference read through a
235
+ read-only shell command (`cat`/`head`/`tail`/`sed`/`rg`/`less`/`more`/`bat`,
236
+ including inside a `sh -c` wrapper) now credits the same ref-read evidence a
237
+ native `Read` would, so a Codex teammate's shell-first consultation still
238
+ satisfies `solidReadGate`; `sed -i`/`--in-place` and any non-whitelisted
239
+ command are never credited (`src/policy/shell-read-refs.ts`,
240
+ `test/shell-read-refs.test.ts`).
241
+
242
+ ### Notification sounds
243
+
244
+ A native OS sound plays on two lifecycle events: the core-scope `Stop` under
245
+ Codex (`stopCore`, since Claude Code already voices its own turn-end via a
246
+ native `afplay` hook — this harness deliberately does not double it) and
247
+ `TeammateIdle` under Claude Code when there's an actionable notice (a missing
248
+ deliverable or a sniper reminder). **On by default**; opt out entirely with
249
+ `FUSE_HARNESS_SOUND=0`.
250
+
251
+ Resolution per event kind (`stop`/`permission`/`human`) cascades: (1) a
252
+ per-kind env override path (`FUSE_HARNESS_SOUND_STOP` / `_PERMISSION` /
253
+ `_HUMAN`) if it exists on disk; (2) the package's own bundled
254
+ `assets/song/{finish,permission-need,need-human}.mp3` (located by walking up
255
+ to the running `package.json`, so it survives the flat/hashed `dist/` build);
256
+ (3) `$CLAUDE_PLUGIN_ROOT/song/<file>.mp3` as a last-resort fallback. No file
257
+ resolves → silent no-op. The `permission` kind and its `.mp3`/env var exist in
258
+ the module for symmetry but have no current call site — permission prompts
259
+ stay native-only on every harness today.
260
+
261
+ Playback is native per platform (`afplay` on darwin, `paplay` on linux,
262
+ PowerShell's `SoundPlayer` on win32) and **absolutely fail-open**: an
263
+ unsupported platform, a missing player binary, an undecodable file, or a
264
+ non-zero exit is swallowed — a broken or absent player can never break a hook
265
+ (`src/runtime/notifications.ts`, `src/runtime/notification-sound.ts`,
266
+ `test/notifications.test.ts`).
201
267
 
202
268
  ### Environment
203
269
 
@@ -214,6 +280,8 @@ Features shipped since 0.1.44, each with its own test:
214
280
  | `FUSE_WEBFETCH_TTL_SEC` | WebFetch cache freshness, seconds (default 24h — pages stale faster than docs). |
215
281
  | `RALPH_MODE` | **Opt-in (default off).** Exempts safe git commands (`add`/`commit`/`checkout -b`/`status`/`diff`/`log`) from the confirmation ask and auto-approves project installs. Destructive git (force-push, `reset --hard`) and system installs still gate. |
216
282
  | `CLAUDE_PROJECT_DIR` | Overrides the project root used to hash the out-of-tree state dir (`src/runtime/paths.ts:20`). |
283
+ | `FUSE_HARNESS_SOUND` | **On by default.** Set to `0` to disable every lifecycle notification sound. |
284
+ | `FUSE_HARNESS_SOUND_STOP` / `_PERMISSION` / `_HUMAN` | Per-kind override: an absolute path to a sound file to play instead of the bundled `assets/song/*.mp3` for that kind (see [Notification sounds](#notification-sounds)). |
217
285
 
218
286
  ## Library usage
219
287
 
@@ -286,9 +354,10 @@ Run `bun run docs:api` for the generated typedoc API reference.
286
354
 
287
355
  ## Known limitations
288
356
 
289
- - **Codex file edits are not gated.** The SOLID/file-size gate keys off
290
- `tool_input.file_path`, which Codex's `apply_patch` call never supplies only
291
- Bash is reliably gated on Codex today (see [Compatibility](#compatibility)).
357
+ - **Codex `apply_patch` enforcement depends on Codex itself.** The harness
358
+ parses the patch and emits the right verdict per file, but Codex does not
359
+ always enforce a correct deny on its end (openai/codex#27833, see
360
+ [Compatibility](#compatibility)) — enforcement beyond the verdict is theirs.
292
361
  - **Cursor file edits are advisory-only.** `afterFileEdit` fires after the edit
293
362
  already happened — a platform limit, not something this harness can work around.
294
363
  - **Hook fan-out is mitigated, not eliminated.** The ~11-sibling-plugin burst is
@@ -306,7 +375,7 @@ Run `bun run docs:api` for the generated typedoc API reference.
306
375
  ## Develop
307
376
 
308
377
  ```sh
309
- bun test # 484 tests (94 files)
378
+ bun test # 662 tests (120 files)
310
379
  bunx tsc --noEmit # typecheck (isolatedDeclarations)
311
380
  bun run build # dist + .d.mts via tsdown (for Node/bundler consumers)
312
381
  bun run docs:api # generate the typedoc API reference
Binary file
Binary file
Binary file
@@ -1,2 +1,2 @@
1
- import { a as fileSizeGuard, c as readClaudeInput, i as denyResponse, l as systemMessage, n as blockResponse, o as guard, r as contextResponse, s as informResponse, t as attachSystemMessage, u as toClaudeResponse } from "../../claude-Cu6rPxUZ.mjs";
1
+ import { a as fileSizeGuard, c as readClaudeInput, i as denyResponse, l as systemMessage, n as blockResponse, o as guard, r as contextResponse, s as informResponse, t as attachSystemMessage, u as toClaudeResponse } from "../../claude-CHpb1U0A.mjs";
2
2
  export { attachSystemMessage, blockResponse, contextResponse, denyResponse, fileSizeGuard, guard, informResponse, readClaudeInput, systemMessage, toClaudeResponse };
@@ -1,4 +1,4 @@
1
- import { t as evaluate } from "../../evaluate-CxgYAODk.mjs";
1
+ import { t as evaluate } from "../../evaluate-Dx6q1kmQ.mjs";
2
2
  import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
3
3
  //#region src/adapters/cline/index.ts
4
4
  /**
@@ -1,8 +1,8 @@
1
1
  import { f as countLines } from "../../home-state-D0RLWP8J.mjs";
2
- import { t as evaluate } from "../../evaluate-CxgYAODk.mjs";
2
+ import { t as evaluate } from "../../evaluate-Dx6q1kmQ.mjs";
3
3
  import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
4
4
  import { n as parseApplyPatch, t as isBypassPermissions } from "../../permission-mode-BN3MNgbm.mjs";
5
- import { c as readClaudeInput, i as denyResponse, r as contextResponse, s as informResponse, v as commandToString } from "../../claude-Cu6rPxUZ.mjs";
5
+ import { c as readClaudeInput, i as denyResponse, r as contextResponse, s as informResponse, v as commandToString } from "../../claude-CHpb1U0A.mjs";
6
6
  //#region src/adapters/codex/index.ts
7
7
  /**
8
8
  * OpenAI Codex CLI adapter (hook-mode). Codex's `PreToolUse` hook (since 2026)
@@ -1,4 +1,4 @@
1
- import { t as evaluate } from "../../evaluate-CxgYAODk.mjs";
1
+ import { t as evaluate } from "../../evaluate-Dx6q1kmQ.mjs";
2
2
  import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
3
3
  //#region src/adapters/cursor/index.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { t as evaluate } from "../../evaluate-CxgYAODk.mjs";
1
+ import { t as evaluate } from "../../evaluate-Dx6q1kmQ.mjs";
2
2
  import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
3
3
  //#region src/adapters/gemini/index.ts
4
4
  /**
@@ -1,2 +1,2 @@
1
- import { n as readHermesInput, r as toHermesResponse, t as guard } from "../../hermes-BpD0nlyR.mjs";
1
+ import { n as readHermesInput, r as toHermesResponse, t as guard } from "../../hermes-CddsFoPN.mjs";
2
2
  export { guard, readHermesInput, toHermesResponse };
@@ -1,4 +1,4 @@
1
- import { t as evaluate } from "./evaluate-CxgYAODk.mjs";
1
+ import { t as evaluate } from "./evaluate-Dx6q1kmQ.mjs";
2
2
  import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
package/dist/cli/bin.mjs CHANGED
@@ -2,10 +2,10 @@
2
2
  import { r as loadDotenv, s as resolveTtlSec } from "../dotenv-Jj8aL1FL.mjs";
3
3
  import { t as detectHarness } from "../harness-Cb9xR8dC.mjs";
4
4
  import { t as claudeHome } from "../home-state-D0RLWP8J.mjs";
5
- import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-BfJytXnW.mjs";
5
+ import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-B9Br7SEr.mjs";
6
6
  import { n as writeInitFile, t as initFor } from "../run-Do2JltgU.mjs";
7
- import { J as runningVersion, Lt as todayUtc, Y as versionBanner, q as runDoctor, t as handleHook } from "../handle-CU5UdAei.mjs";
8
- import { p as readStdin$1 } from "../claude-Cu6rPxUZ.mjs";
7
+ import { $ as versionBanner, Q as runningVersion, Rt as todayUtc, Z as runDoctor, t as handleHook } from "../handle-CVk0NlTg.mjs";
8
+ import { p as readStdin$1 } from "../claude-CHpb1U0A.mjs";
9
9
  import { delimiter, join } from "node:path";
10
10
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
11
11
  import { homedir } from "node:os";
@@ -1,2 +1,2 @@
1
- import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-BfJytXnW.mjs";
1
+ import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-B9Br7SEr.mjs";
2
2
  export { checkStaged, stagedContent, stagedFiles };
@@ -1,3 +1,4 @@
1
+ import { r as resolveMaxLines } from "./limits-CHn8AIL1.mjs";
1
2
  import { f as countLines, p as evaluateFileSize, r as fusengineCache, t as claudeHome } from "./home-state-D0RLWP8J.mjs";
2
3
  import { dirname, join, normalize } from "node:path";
3
4
  import { existsSync } from "node:fs";
@@ -146,6 +147,53 @@ function resolveSolidRefFramework(filePath) {
146
147
  return "generic";
147
148
  }
148
149
  //#endregion
150
+ //#region src/policy/edit-outcome.ts
151
+ /**
152
+ * Count non-overlapping literal occurrences of `needle` in `haystack`. An
153
+ * empty `needle` returns 0 — treated as "not found" by callers, since Edit's
154
+ * `old_string` is never legitimately empty and an empty needle has no
155
+ * well-defined occurrence count.
156
+ * @param haystack - The on-disk file content to search.
157
+ * @param needle - The literal substring to count.
158
+ */
159
+ function countOccurrences(haystack, needle) {
160
+ if (needle === "") return 0;
161
+ let count = 0;
162
+ let from = 0;
163
+ for (;;) {
164
+ const at = haystack.indexOf(needle, from);
165
+ if (at === -1) break;
166
+ count++;
167
+ from = at + needle.length;
168
+ }
169
+ return count;
170
+ }
171
+ /**
172
+ * Compute the line count an Edit would PRODUCE, before it lands on disk —
173
+ * `existingLines − countLines(old_string) + countLines(new_string)`, scaled
174
+ * by how many `old_string` occurrences are actually replaced (1 normally, or
175
+ * every occurrence under `replace_all`). Pure arithmetic on the literal
176
+ * strings — no regex/fuzzy matching, matching the real Edit tool's own
177
+ * exact-substring semantics.
178
+ *
179
+ * Returns `null` (fail-closed — caller must fall back to judging the stale
180
+ * on-disk count alone) when `old_string` is missing/empty, or does not occur
181
+ * in `existingContent` at all: an Edit whose `old_string` doesn't match the
182
+ * real file is stale or adversarial, never a case to special-case an allow for.
183
+ * @param existingContent - Full on-disk content of the file being edited.
184
+ * @param oldString - The Edit's `old_string` (tool_input, unmodified).
185
+ * @param newString - The Edit's `new_string` (tool_input, unmodified).
186
+ * @param replaceAll - The Edit's `replace_all` flag.
187
+ */
188
+ function computeEditResultLines(existingContent, oldString, newString, replaceAll) {
189
+ if (!oldString) return null;
190
+ const occurrences = countOccurrences(existingContent, oldString);
191
+ if (occurrences === 0) return null;
192
+ const times = replaceAll ? occurrences : 1;
193
+ const delta = countLines(newString) - countLines(oldString);
194
+ return countLines(existingContent) + delta * times;
195
+ }
196
+ //#endregion
149
197
  //#region src/policy/guards/security.ts
150
198
  /** Critical patterns that must always be blocked — parity `security_rules.py`'s cumulated violation names. */
151
199
  const CRITICAL_PATTERNS = [
@@ -822,6 +870,43 @@ function buildNeverApprovalPrompt(cmd) {
822
870
  };
823
871
  }
824
872
  //#endregion
873
+ //#region src/policy/git-gates.ts
874
+ /**
875
+ * Evaluate the two git confirmation gates — destructive block ({@link
876
+ * GIT_BLOCKED}) and confirmation ask ({@link GIT_ASK}) — against `command`.
877
+ * Skipped entirely when `ralphSafe` is true: Ralph mode already silent-
878
+ * allowed the command upstream (parity with evaluate.ts's pre-existing
879
+ * `!ralphSafe &&` guards). Returns the matching gate's {@link PolicyResult},
880
+ * or `null` when neither gate matches.
881
+ */
882
+ function evaluateGitGates(command, ralphSafe) {
883
+ if (ralphSafe || !command) return null;
884
+ if (matchPatterns(command, GIT_BLOCKED)) {
885
+ const reason = `Destructive git command: ${command}`;
886
+ return {
887
+ decision: "deny",
888
+ message: reason,
889
+ prompt: {
890
+ kind: "block",
891
+ title: "Destructive git command",
892
+ reason,
893
+ actions: ["Use a non-destructive alternative (e.g. --force-with-lease; avoid --hard / -D)"]
894
+ }
895
+ };
896
+ }
897
+ if (matchPatterns(command, GIT_ASK)) return {
898
+ decision: "deny",
899
+ message: `Git operation requires confirmation: ${command}`,
900
+ prompt: {
901
+ kind: "ask",
902
+ title: "Confirm git operation",
903
+ reason: `Authorize: ${command.trim()}`,
904
+ actions: ["Approve if this git operation is intended"]
905
+ }
906
+ };
907
+ return null;
908
+ }
909
+ //#endregion
825
910
  //#region src/policy/evaluate.ts
826
911
  /**
827
912
  * Evaluate a single tool-use against the bundled policies, returning a pure
@@ -845,35 +930,21 @@ function evaluate(ctx) {
845
930
  prompt
846
931
  };
847
932
  }
848
- if (!ralphSafe && ctx.command && matchPatterns(ctx.command, GIT_BLOCKED)) {
849
- const reason = `Destructive git command: ${ctx.command}`;
850
- return {
851
- decision: "deny",
852
- message: reason,
853
- prompt: {
854
- kind: "block",
855
- title: "Destructive git command",
856
- reason,
857
- actions: ["Use a non-destructive alternative (e.g. --force-with-lease; avoid --hard / -D)"]
858
- }
859
- };
860
- }
861
- if (!ralphSafe && ctx.command && matchPatterns(ctx.command, GIT_ASK)) return {
862
- decision: "deny",
863
- message: `Git operation requires confirmation: ${ctx.command}`,
864
- prompt: {
865
- kind: "ask",
866
- title: "Confirm git operation",
867
- reason: `Authorize: ${ctx.command.trim()}`,
868
- actions: ["Approve if this git operation is intended"]
869
- }
870
- };
933
+ const gitGate = evaluateGitGates(ctx.command, ralphSafe);
934
+ if (gitGate) return gitGate;
871
935
  if (ctx.filePath && isFileSizeScoped(ctx.filePath) && ctx.agentType !== "Explore" && ctx.agentType !== "Plan") {
872
936
  const incoming = ctx.content !== void 0 ? countLines(ctx.content) : 0;
873
- const lines = ctx.tool === "Edit" ? Math.max(incoming, ctx.existingLines ?? 0) : incoming || (ctx.existingLines ?? 0);
937
+ const existing = ctx.existingLines ?? 0;
938
+ const max = ctx.maxLines ?? resolveMaxLines();
939
+ const editResult = ctx.tool === "Edit" && ctx.existingContent !== void 0 && ctx.content !== void 0 ? computeEditResultLines(ctx.existingContent, ctx.oldString, ctx.content, ctx.isReplaceAll === true) : null;
940
+ if (editResult !== null && (editResult <= max || editResult < existing)) return {
941
+ decision: "allow",
942
+ message: null
943
+ };
944
+ const lines = ctx.tool === "Edit" ? editResult ?? Math.max(incoming, existing) : incoming || existing;
874
945
  const framework = resolveSolidRefFramework(ctx.filePath);
875
946
  const displayLines = ctx.tool === "Write" ? ctx.existingLines ?? lines : lines;
876
- const verdict = evaluateFileSize(lines, ctx.maxLines, ctx.filePath, framework, displayLines);
947
+ const verdict = evaluateFileSize(lines, max, ctx.filePath, framework, displayLines);
877
948
  if (lines > 0 && !verdict.ok) return {
878
949
  decision: "deny",
879
950
  message: verdict.message,