@fusengine/harness 0.1.57 → 0.1.59

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,8 +125,8 @@ 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** | Reuses the Claude reader/response shapeCodex's hook wire format matches Claude's (`src/adapters/codex/index.ts:1-8`). Bash is gated reliably. | Not wired by `harness init codex` (PreToolUse `Bash\|apply_patch` + PostToolUse only, `src/init/templates.ts:29-38`) | The SOLID/file-size gate covers `apply_patch` edits at **0%** it keys off `tool_input.file_path` (Write/Edit shape), which Codex's diff-carrying `apply_patch` call never supplies (`codex/index.ts:6-8`). Codex also **parses but does not honor** `permissionDecision:"ask"` — deny-only (`codex/index.ts:6`). |
129
- | **cursor** | `beforeShellExecution` can deny/ask (shell only, `cursor/index.ts:16-21`) | none | `afterFileEdit` is **observe-only** — "Cursor cannot block here" (`cursor/index.ts:23-24`); a file-edit violation is logged, never prevented (platform limit, not a bug here). |
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`. |
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. |
132
132
  | **hermes** | `pre_tool_call` proven: reuses the Claude stdin reader, blocks via `{decision:"block",reason}` (`hermes/index.ts:12-36`) | untested — no lifecycle dispatch wired for Hermes in this repo | `ask`/`inform` degrade to non-blocking `{context}` — Hermes "has no interactive ask state" (`hermes/index.ts:27-28`). |
@@ -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-BxC9semG.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-BLuab-tR.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-CNAzgxnN.mjs";
1
+ import { t as evaluate } from "../../evaluate-BgFLlqrs.mjs";
2
2
  import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
3
3
  //#region src/adapters/cline/index.ts
4
4
  /**
@@ -1,2 +1,14 @@
1
- import { a as denyResponse, i as contextResponse, l as readClaudeInput, s as guard, t as ClaudeHookInput } from "../../index-DZCLmSoO.mjs";
2
- export { type ClaudeHookInput as CodexHookInput, contextResponse, denyResponse, guard, readClaudeInput as readCodexInput };
1
+ import { t as Prompt } from "../../types-DVbIl9md.mjs";
2
+ import { a as denyResponse, i as contextResponse, l as readClaudeInput, t as ClaudeHookInput } from "../../index-DZCLmSoO.mjs";
3
+
4
+ //#region src/adapters/codex/index.d.ts
5
+ /** Render a portable {@link Prompt} as a Codex hook response, `ask` → explicit deny. */
6
+ declare function toCodexResponse(prompt: Prompt): string;
7
+ /**
8
+ * Run the bundled policy over a Codex payload and return the native response
9
+ * string (deny/additionalContext), or null to allow. `apply_patch` is fanned
10
+ * into per-file checks; every other tool routes through the portable policy.
11
+ */
12
+ declare function guard(input: ClaudeHookInput): string | null;
13
+ //#endregion
14
+ export { type ClaudeHookInput as CodexHookInput, contextResponse, denyResponse, guard, readClaudeInput as readCodexInput, toCodexResponse };
@@ -1,2 +1,66 @@
1
- import { c as readClaudeInput, i as denyResponse, o as guard, r as contextResponse } from "../../claude-BxC9semG.mjs";
2
- export { contextResponse, denyResponse, guard, readClaudeInput as readCodexInput };
1
+ import { f as countLines } from "../../home-state-D0RLWP8J.mjs";
2
+ import { t as evaluate } from "../../evaluate-BgFLlqrs.mjs";
3
+ import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
4
+ import { t as parseApplyPatch } from "../../apply-patch-CIS2EZ_q.mjs";
5
+ import { c as readClaudeInput, i as denyResponse, r as contextResponse } from "../../claude-BLuab-tR.mjs";
6
+ //#region src/adapters/codex/index.ts
7
+ /**
8
+ * OpenAI Codex CLI adapter (hook-mode). Codex's `PreToolUse` hook (since 2026)
9
+ * uses the SAME envelope as Claude Code — `tool_name`/`tool_input` in,
10
+ * `hookSpecificOutput.permissionDecision` out — so it shares Claude's readers.
11
+ * Config lives at `.codex/hooks.json`.
12
+ *
13
+ * Two Codex-specific quirks this adapter closes (audit 2026-07-06):
14
+ * 1. `apply_patch` is Codex's PRIMARY edit primitive; its payload is a freeform
15
+ * patch in `tool_input.command`, with NO `file_path`/`content`. Claude's guard
16
+ * keyed off those fields, so the SOLID/DRY gates saw NOTHING (enforcement 0%).
17
+ * Here the patch is parsed and each file is judged (one violating hunk denies).
18
+ * 2. Codex parses but NEVER honors `permissionDecision: "ask"` (deny-only) — an
19
+ * `ask` silently fails open (verified: `pre_tool_use.rs` test
20
+ * `unsupported_permission_decision_fails_open`). Every `ask` is downgraded to
21
+ * an explicit deny.
22
+ */
23
+ const ASK_PREFIX = "[downgraded from ask — Codex has no interactive approval]";
24
+ /** Render a portable {@link Prompt} as a Codex hook response, `ask` → explicit deny. */
25
+ function toCodexResponse(prompt) {
26
+ const message = formatPrompt(prompt);
27
+ if (prompt.kind === "inform") return contextResponse("PreToolUse", message);
28
+ if (prompt.kind === "ask") return denyResponse("PreToolUse", `${ASK_PREFIX}\n${message}`);
29
+ return denyResponse("PreToolUse", message);
30
+ }
31
+ /** OR the per-file SOLID verdict of an `apply_patch` payload — first block wins. */
32
+ function applyPatchPrompt(command) {
33
+ for (const f of parseApplyPatch(command)) {
34
+ if (f.op === "delete") continue;
35
+ const r = evaluate({
36
+ tool: f.op === "add" ? "Write" : "Edit",
37
+ filePath: f.path,
38
+ content: f.content,
39
+ existingLines: countLines(f.content)
40
+ });
41
+ if (r.decision !== "allow" && r.prompt) return r.prompt;
42
+ }
43
+ return null;
44
+ }
45
+ /** Portable single-tool verdict for non-`apply_patch` Codex tools. */
46
+ function resolvePrompt(input) {
47
+ const i = input.tool_input;
48
+ const r = evaluate({
49
+ tool: input.tool_name ?? "Write",
50
+ filePath: i?.file_path,
51
+ content: i?.content ?? i?.new_string,
52
+ command: i?.command
53
+ });
54
+ return r.decision === "allow" || !r.prompt ? null : r.prompt;
55
+ }
56
+ /**
57
+ * Run the bundled policy over a Codex payload and return the native response
58
+ * string (deny/additionalContext), or null to allow. `apply_patch` is fanned
59
+ * into per-file checks; every other tool routes through the portable policy.
60
+ */
61
+ function guard(input) {
62
+ const prompt = input.tool_name === "apply_patch" ? applyPatchPrompt(input.tool_input?.command ?? "") : resolvePrompt(input);
63
+ return prompt ? toCodexResponse(prompt) : null;
64
+ }
65
+ //#endregion
66
+ export { contextResponse, denyResponse, guard, readClaudeInput as readCodexInput, toCodexResponse };
@@ -15,6 +15,19 @@ interface CursorEditPayload {
15
15
  }[];
16
16
  }
17
17
  /**
18
+ * `afterFileEdit` stdout response. Its schema (cursor.com/docs/hooks#afterFileEdit)
19
+ * is DELIBERATELY narrower than the "before" hooks: `permission` + `user_message`
20
+ * only — there is NO `agent_message` and NO `updated_input`. Since the edit is
21
+ * already on disk when this "after" hook fires, `deny` cannot revert it and the
22
+ * correction reaches only the HUMAN (`user_message`), never the model — so this
23
+ * path is strictly ADVISORY, not an enforceable gate.
24
+ */
25
+ interface CursorEditResponse {
26
+ permission: "allow" | "deny";
27
+ /** User-visible correction — snake_case (#141516); the only channel afterFileEdit exposes. */
28
+ user_message?: string;
29
+ }
30
+ /**
18
31
  * `beforeShellExecution` stdout response. Message keys are snake_case:
19
32
  * Cursor silently ignores camelCase `userMessage`/`agentMessage` (#141516,
20
33
  * regression persists through v2.0.77+ — forum #142589), matching the
@@ -32,9 +45,24 @@ interface CursorResponse {
32
45
  //#region src/adapters/cursor/index.d.ts
33
46
  /** Guard a shell command (git/install policies). */
34
47
  declare function beforeShellExecution(payload: CursorShellPayload): CursorResponse;
35
- /** Observe a file edit (Cursor cannot block here). Returns the verdict for logging. */
36
- declare function afterFileEdit(payload: CursorEditPayload): {
37
- violation: string | null;
38
- };
48
+ /**
49
+ * Advise on a file edit AFTER Cursor has written it — a HUMAN-VISIBLE audit note,
50
+ * never a gate. This is an "after" hook: the edit is already on disk. On a
51
+ * SOLID/DRY violation we surface the correction through `user_message` (the only
52
+ * channel afterFileEdit exposes — no `agent_message`, so the model is never
53
+ * re-informed) while ALWAYS returning `permission: "allow"`.
54
+ *
55
+ * We deliberately never emit `permission: "deny"` here, for two distinct reasons:
56
+ * (1) structural — afterFileEdit was "informational only" at launch (Chacon,
57
+ * Cursor hooks beta 1.7, 2025-09: no channel to stop the agent), and a post-write
58
+ * deny has no documented rollback; (2) empirical — Cursor staff confirm the
59
+ * deny-enforcement path is broken for file operations (forum.cursor.com/t/154377,
60
+ * v2.6.18, 2026-03, open) — proven for file READS, plausibly the same for writes.
61
+ * So a `deny` would be a false blocking signal; `allow` + `user_message` is the
62
+ * only proven-safe shape.
63
+ * @param payload - The `afterFileEdit` stdin payload.
64
+ * @returns Always an allow; carries the user-visible correction on a violation.
65
+ */
66
+ declare function afterFileEdit(payload: CursorEditPayload): CursorEditResponse;
39
67
  //#endregion
40
- export { type CursorEditPayload, type CursorResponse, type CursorShellPayload, afterFileEdit, beforeShellExecution };
68
+ export { type CursorEditPayload, type CursorEditResponse, type CursorResponse, type CursorShellPayload, afterFileEdit, beforeShellExecution };
@@ -1,4 +1,4 @@
1
- import { t as evaluate } from "../../evaluate-CNAzgxnN.mjs";
1
+ import { t as evaluate } from "../../evaluate-BgFLlqrs.mjs";
2
2
  import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
3
3
  //#region src/adapters/cursor/index.ts
4
4
  /**
@@ -23,7 +23,24 @@ function beforeShellExecution(payload) {
23
23
  agent_message: msg
24
24
  };
25
25
  }
26
- /** Observe a file edit (Cursor cannot block here). Returns the verdict for logging. */
26
+ /**
27
+ * Advise on a file edit AFTER Cursor has written it — a HUMAN-VISIBLE audit note,
28
+ * never a gate. This is an "after" hook: the edit is already on disk. On a
29
+ * SOLID/DRY violation we surface the correction through `user_message` (the only
30
+ * channel afterFileEdit exposes — no `agent_message`, so the model is never
31
+ * re-informed) while ALWAYS returning `permission: "allow"`.
32
+ *
33
+ * We deliberately never emit `permission: "deny"` here, for two distinct reasons:
34
+ * (1) structural — afterFileEdit was "informational only" at launch (Chacon,
35
+ * Cursor hooks beta 1.7, 2025-09: no channel to stop the agent), and a post-write
36
+ * deny has no documented rollback; (2) empirical — Cursor staff confirm the
37
+ * deny-enforcement path is broken for file operations (forum.cursor.com/t/154377,
38
+ * v2.6.18, 2026-03, open) — proven for file READS, plausibly the same for writes.
39
+ * So a `deny` would be a false blocking signal; `allow` + `user_message` is the
40
+ * only proven-safe shape.
41
+ * @param payload - The `afterFileEdit` stdin payload.
42
+ * @returns Always an allow; carries the user-visible correction on a violation.
43
+ */
27
44
  function afterFileEdit(payload) {
28
45
  const content = payload.edits?.map((e) => e.new_string).join("\n") ?? "";
29
46
  const r = evaluate({
@@ -31,7 +48,11 @@ function afterFileEdit(payload) {
31
48
  filePath: payload.file_path,
32
49
  content
33
50
  });
34
- return { violation: r.decision === "deny" ? r.message : null };
51
+ if (r.decision !== "deny" || !r.prompt) return { permission: "allow" };
52
+ return {
53
+ permission: "allow",
54
+ user_message: formatPrompt(r.prompt)
55
+ };
35
56
  }
36
57
  //#endregion
37
58
  export { afterFileEdit, beforeShellExecution };
@@ -1,4 +1,4 @@
1
- import { t as evaluate } from "../../evaluate-CNAzgxnN.mjs";
1
+ import { t as evaluate } from "../../evaluate-BgFLlqrs.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-DWXCRFZU.mjs";
1
+ import { n as readHermesInput, r as toHermesResponse, t as guard } from "../../hermes-CyhCYqD7.mjs";
2
2
  export { guard, readHermesInput, toHermesResponse };
@@ -0,0 +1,81 @@
1
+ //#region src/adapters/codex/apply-patch.ts
2
+ const BEGIN = "*** Begin Patch";
3
+ const END = "*** End Patch";
4
+ const ADD = "*** Add File: ";
5
+ const DEL = "*** Delete File: ";
6
+ const UPD = "*** Update File: ";
7
+ const MOVE = "*** Move to: ";
8
+ const EOF = "*** End of File";
9
+ const ENV = "*** Environment ID: ";
10
+ /**
11
+ * Parse a Codex freeform patch into its per-file changes. Lenient on whitespace
12
+ * around structural markers (as Codex's own parser is); returns `[]` when no
13
+ * recognizable hunk is present (malformed input fails open, not closed).
14
+ * @param text - Raw patch body from `tool_input.command`.
15
+ * @returns One {@link PatchedFile} per Add/Update/Delete hunk, in order.
16
+ */
17
+ function parseApplyPatch(text) {
18
+ const files = [];
19
+ let cur = null;
20
+ let buf = [];
21
+ const flush = () => {
22
+ if (cur) {
23
+ cur.content = buf.join("\n");
24
+ files.push(cur);
25
+ }
26
+ cur = null;
27
+ buf = [];
28
+ };
29
+ for (const line of text.split("\n")) {
30
+ const marker = line.trimStart();
31
+ if (marker === BEGIN || marker.startsWith(ENV)) continue;
32
+ if (marker === END) {
33
+ flush();
34
+ continue;
35
+ }
36
+ if (marker.startsWith(ADD)) {
37
+ flush();
38
+ cur = {
39
+ path: marker.slice(14).trim(),
40
+ content: "",
41
+ op: "add"
42
+ };
43
+ continue;
44
+ }
45
+ if (marker.startsWith(DEL)) {
46
+ flush();
47
+ files.push({
48
+ path: marker.slice(17).trim(),
49
+ content: "",
50
+ op: "delete"
51
+ });
52
+ continue;
53
+ }
54
+ if (marker.startsWith(UPD)) {
55
+ flush();
56
+ cur = {
57
+ path: marker.slice(17).trim(),
58
+ content: "",
59
+ op: "update"
60
+ };
61
+ continue;
62
+ }
63
+ if (marker.startsWith(MOVE)) {
64
+ if (cur) cur.path = marker.slice(13).trim();
65
+ continue;
66
+ }
67
+ if (marker === EOF || !cur) continue;
68
+ if (cur.op === "add") {
69
+ if (line.startsWith("+")) buf.push(line.slice(1));
70
+ continue;
71
+ }
72
+ if (line.startsWith("@@")) continue;
73
+ if (line.startsWith("+")) buf.push(line.slice(1));
74
+ else if (line.startsWith("-")) continue;
75
+ else buf.push(line.startsWith(" ") ? line.slice(1) : line);
76
+ }
77
+ flush();
78
+ return files;
79
+ }
80
+ //#endregion
81
+ export { parseApplyPatch as t };
@@ -1,4 +1,4 @@
1
- import { t as evaluate } from "./evaluate-CNAzgxnN.mjs";
1
+ import { t as evaluate } from "./evaluate-BgFLlqrs.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,9 +2,9 @@
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-DZvP_9xB.mjs";
5
+ import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-B4b2_7Qm.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-DY79I4a8.mjs";
7
+ import { J as runningVersion, Lt as todayUtc, Y as versionBanner, q as runDoctor, t as handleHook } from "../handle-B6yL03we.mjs";
8
8
  import { delimiter, join } from "node:path";
9
9
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
10
10
  import { homedir } from "node:os";
@@ -1,2 +1,2 @@
1
- import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-DZvP_9xB.mjs";
1
+ import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-B4b2_7Qm.mjs";
2
2
  export { checkStaged, stagedContent, stagedFiles };
@@ -436,7 +436,7 @@ const CODE_MUTATORS = [
436
436
  desc: "awk in-place edit"
437
437
  },
438
438
  {
439
- re: /\bpatch\b/,
439
+ re: /(?:^|[\n;&|(])\s*patch(?=\s|<|[;&|)>]|$)/,
440
440
  desc: "patch file modification"
441
441
  },
442
442
  {
@@ -4,7 +4,7 @@ import { r as projectLayout } from "./layout-C0jaaCQC.mjs";
4
4
  import { i as walkUpFor, n as projectRoot, r as projectRootOrNull, t as isCodeFile } from "./project-root-3kk7gCOp.mjs";
5
5
  import { A as detectCreationIntent, E as isExcludedSwiftPath, F as docConsultedGate, H as detectProjectType$1, I as evaluateApex, M as POST_AUTH_GATES, N as PRE_AUTH_GATES, S as usesTailwindUtilities, T as isExcludedJsPath, V as detectModularArchitecture, W as requiredArchSkill, _ as scanPlugin, c as EXCLUDE_DIRS$1, d as buildApexTaskInjection, h as buildClaudeMdContext, k as capVerbosity, l as PROJECT_INDICATORS, n as missingSeoElements, o as parseEnrichment, r as descFromText, s as parseEntry, t as isHtmlLike, w as frameworkSolidGate, x as skillTriggerGate, y as parseField, z as detectFramework } from "./validate-DCQ8dkdL.mjs";
6
6
  import { a as sanitizeSessionId, c as sessionsDir, d as countFrameworkCodeLines, f as countLines, i as loadSessionState, l as PLUGINS_DIR, n as fuseHarnessHome, o as saveSessionState, r as fusengineCache, t as claudeHome } from "./home-state-D0RLWP8J.mjs";
7
- import { n as FAIL_CLOSED, t as evaluate } from "./evaluate-CNAzgxnN.mjs";
7
+ import { n as FAIL_CLOSED, t as evaluate } from "./evaluate-BgFLlqrs.mjs";
8
8
  import { a as writeJsonFile, i as readJsonFile, r as hashText, t as atomicWrite } from "./json-io-DisYd2fb.mjs";
9
9
  import { r as isDocConsulted } from "./doc-helpers-CWZegVdR.mjs";
10
10
  import { n as findMarketplacePlugins, r as readPluginMeta, t as resolveSkillPath } from "./skill-path-DhItkBzk.mjs";
@@ -13,8 +13,9 @@ import { a as nowStamp, l as throttleMs, n as readRoots, o as readState, s as se
13
13
  import { d as loadIndex, i as cacheLookupMeta, n as webfetchCacheWrite, o as cacheLookupSubstringMeta, t as mcpCacheWrite, u as extractText } from "./mcp-store-BkBDmuxN.mjs";
14
14
  import { t as loadRefs } from "./loader-AGz4nK7d.mjs";
15
15
  import { a as writeLastNonce, c as recordAgent, d as recordRefRead, f as recordTarget, h as apexAuthorizationGate, i as verifyTrack, l as recordBrainstormRequired, m as trivialCount, n as saveTrack, o as agentsFresh, p as recordTrivialEdit, r as signTrack, s as emptyTrack, t as loadTrack, u as recordDoc } from "./store-CQ4roWrU.mjs";
16
- import { d as collectFiles, f as pathExists, g as writeText, h as spawnCapture, i as denyResponse, l as systemMessage, m as sleep, n as blockResponse, p as readText, r as contextResponse, s as informResponse, t as attachSystemMessage } from "./claude-BxC9semG.mjs";
17
- import { r as toHermesResponse } from "./hermes-DWXCRFZU.mjs";
16
+ import { t as parseApplyPatch } from "./apply-patch-CIS2EZ_q.mjs";
17
+ import { d as collectFiles, f as pathExists, g as writeText, h as spawnCapture, i as denyResponse, l as systemMessage, m as sleep, n as blockResponse, p as readText, r as contextResponse, s as informResponse, t as attachSystemMessage } from "./claude-BLuab-tR.mjs";
18
+ import { r as toHermesResponse } from "./hermes-CyhCYqD7.mjs";
18
19
  import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
19
20
  import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
20
21
  import { homedir } from "node:os";
@@ -86,15 +87,31 @@ function normalizeEvent(id, payload) {
86
87
  }
87
88
  const event = str(payload.hook_event_name) ?? "";
88
89
  const input = payload.tool_input ?? payload;
89
- return {
90
+ const tool = str(payload.tool_name) ?? "";
91
+ const base = {
90
92
  phase: /post|after/i.test(event) ? "post" : "pre",
91
- tool: str(payload.tool_name) ?? "",
93
+ tool,
92
94
  input,
93
95
  sessionId: str(payload.session_id) ?? str(payload.conversation_id) ?? "",
96
+ agentType: str(payload.agent_type) ?? str(input.subagent_type)
97
+ };
98
+ if (tool === "apply_patch") {
99
+ const files = parseApplyPatch(str(input.command) ?? str(payload.command) ?? "").map((f) => ({
100
+ filePath: f.path,
101
+ content: f.content,
102
+ op: f.op
103
+ }));
104
+ return {
105
+ ...base,
106
+ phase: "pre",
107
+ files: files.length > 0 ? files : void 0
108
+ };
109
+ }
110
+ return {
111
+ ...base,
94
112
  filePath: str(input.file_path) ?? str(input.path) ?? str(payload.file_path),
95
113
  content: str(input.content) ?? str(input.new_string),
96
- command: str(input.command) ?? str(payload.command),
97
- agentType: str(payload.agent_type) ?? str(input.subagent_type)
114
+ command: str(input.command) ?? str(payload.command)
98
115
  };
99
116
  }
100
117
  //#endregion
@@ -329,6 +346,95 @@ function designLifecycle(payload, cacheDir, cwd, stamp, now) {
329
346
  }
330
347
  return false;
331
348
  }
349
+ //#endregion
350
+ //#region src/runtime/inject-dedup-exclusive.ts
351
+ /**
352
+ * @module inject-dedup-exclusive
353
+ * Cross-process-exact-once cooldown gate, via EXCLUSIVE file creation.
354
+ *
355
+ * {@link module:inject-dedup.oncePerWindow}'s shared-JSON read-modify-write is
356
+ * best-effort under true concurrency: the ~11-process plugin hook fan-out for
357
+ * one Claude event can lose an update and let 2-3 siblings all observe "not
358
+ * seen yet" (lesson 2026-07-05 16:00 — the `saveTrack` lost-update race, same
359
+ * shape). `writeFileSync(path, data, { flag: "wx" })` sidesteps this: the OS
360
+ * guarantees exclusive creation is atomic, so of N concurrent siblings calling
361
+ * this for the SAME key, exactly one observes success and the rest get
362
+ * `EEXIST` — never a double-win, with no lock file or retry loop needed.
363
+ *
364
+ * One marker file per key (not a shared map) is the tradeoff for that
365
+ * guarantee. A bounded sweep on every call deletes markers older than
366
+ * `windowMs` so the directory never grows unbounded under many distinct keys.
367
+ *
368
+ * Reserve this for HIGH-CONCURRENCY callers on a short burst window (same
369
+ * tool-use/lifecycle event fanned out to every installed plugin) — e.g. the
370
+ * sniper reminder ({@link module:lifecycle/track-changes}) and compliance
371
+ * notices ({@link module:notices}). Low-frequency, long-window callers (e.g.
372
+ * the 30-min lessons Stop-reminder cooldown) are fine on the JSON mode: a
373
+ * single real Stop event per session is not concurrent with itself the way
374
+ * one PostToolUse's ~11 sibling hooks are within the same 2s burst.
375
+ * @packageDocumentation
376
+ */
377
+ /** Subdirectory (under the state dir) holding one marker file per dedup key. */
378
+ const EXCLUSIVE_SUBDIR = "inject-dedup-locks";
379
+ /**
380
+ * Filesystem-safe, collision-resistant basename for `key`. Full MD5 hex
381
+ * (unlike {@link module:util/json-io.hashText}'s 8-char truncation) — a
382
+ * collision here would silently merge two unrelated keys' exclusivity.
383
+ */
384
+ function lockFileName(key) {
385
+ return `${createHash("md5").update(key).digest("hex")}.lock`;
386
+ }
387
+ /**
388
+ * Delete marker files older than `windowMs` in `dir` — O(n) per call, bounds
389
+ * directory growth. Compares against the creation timestamp STORED IN the
390
+ * marker's content (written by {@link onceExclusive} below), not the file's
391
+ * fs `mtime`: callers may pass a fake logical `now` (tests), which would never
392
+ * agree with the real OS clock backing `mtime`.
393
+ */
394
+ function sweepExclusiveDir(dir, now, windowMs) {
395
+ let entries;
396
+ try {
397
+ entries = readdirSync(dir);
398
+ } catch {
399
+ return;
400
+ }
401
+ for (const entry of entries) {
402
+ const path = join(dir, entry);
403
+ try {
404
+ const createdAt = Number(readFileSync(path, "utf8"));
405
+ if (!Number.isFinite(createdAt) || now - createdAt >= windowMs) unlinkSync(path);
406
+ } catch {}
407
+ }
408
+ }
409
+ /**
410
+ * Cooldown gate via exclusive marker-file creation. Returns `true` exactly
411
+ * once per `key` within `windowMs` across ALL concurrent processes sharing
412
+ * `opts.dir` (the caller MAY emit), `false` for every other concurrent or
413
+ * subsequent call inside the same window (the caller SHOULD suppress).
414
+ *
415
+ * Fails open on any unwritable state dir or unexpected fs error: the emission
416
+ * is allowed rather than silently dropped.
417
+ * @param key - Stable identity of the block (same semantics as {@link module:inject-dedup.oncePerWindow}).
418
+ * @param windowMs - Suppression window in ms (also the sweep threshold).
419
+ * @param opts - Optional clock + state-dir overrides (for tests).
420
+ * @returns `true` to proceed/emit, `false` to suppress.
421
+ */
422
+ function onceExclusive(key, windowMs, opts = {}) {
423
+ const now = opts.now ?? Date.now();
424
+ const dir = join(opts.dir ?? defaultStateDir(), EXCLUSIVE_SUBDIR);
425
+ try {
426
+ mkdirSync(dir, { recursive: true });
427
+ } catch {
428
+ return true;
429
+ }
430
+ sweepExclusiveDir(dir, now, windowMs);
431
+ try {
432
+ writeFileSync(join(dir, lockFileName(key)), String(now), { flag: "wx" });
433
+ return true;
434
+ } catch (err) {
435
+ return err.code !== "EEXIST";
436
+ }
437
+ }
332
438
  /** Sidecar basename under the per-project state dir. */
333
439
  const SIDECAR$2 = "inject-dedup.json";
334
440
  /** Load the `{ key -> epochMs }` map, or `{}` when missing/corrupt. */
@@ -373,6 +479,24 @@ function oncePerWindow(key, windowMs, opts = {}) {
373
479
  return true;
374
480
  }
375
481
  //#endregion
482
+ //#region src/runtime/fragment-registry.ts
483
+ let registry = [];
484
+ /** Clear the registry. Call once before dispatching a hook event. */
485
+ function resetFragmentRegistry() {
486
+ registry = [];
487
+ }
488
+ /** Record one fragment's post-cap size. Called by {@link module:inject-budget.capFragment}. */
489
+ function recordFragment(label, chars) {
490
+ registry.push({
491
+ label,
492
+ chars
493
+ });
494
+ }
495
+ /** Snapshot of every fragment recorded since the last {@link resetFragmentRegistry}. */
496
+ function fragmentRegistry() {
497
+ return [...registry];
498
+ }
499
+ //#endregion
376
500
  //#region src/runtime/inject-budget.ts
377
501
  /**
378
502
  * @module inject-budget
@@ -411,7 +535,10 @@ const FRAGMENT_CHAR_CAP = 8e3;
411
535
  * @returns `text` unchanged, or a truncated copy ending in the cut notice — always ≤ the cap.
412
536
  */
413
537
  function capFragment(label, text) {
414
- if (text.length <= 8e3) return text;
538
+ if (text.length <= 8e3) {
539
+ recordFragment(label, text.length);
540
+ return text;
541
+ }
415
542
  const totalLen = text.length;
416
543
  const safeLabel = label.length <= 80 ? label : `${label.slice(0, 77)}...`;
417
544
  const suffixFor = (keptLen) => `\n[truncated ${safeLabel}: kept ${keptLen} of ${totalLen} chars — source file unchanged]`;
@@ -420,7 +547,9 @@ function capFragment(label, text) {
420
547
  const slice = text.slice(0, budget);
421
548
  const lastNl = slice.lastIndexOf("\n");
422
549
  const kept = (lastNl > 0 ? slice.slice(0, lastNl) : slice).trimEnd();
423
- return kept + suffixFor(kept.length);
550
+ const result = kept + suffixFor(kept.length);
551
+ recordFragment(label, result.length);
552
+ return result;
424
553
  }
425
554
  /**
426
555
  * One-line numeric recap of what a batch of fragments actually injected —
@@ -1244,9 +1373,13 @@ function trackAgentMemory(data, home = homedir(), now = Date.now()) {
1244
1373
  cumulativeCodeFiles: 0
1245
1374
  }
1246
1375
  }, home);
1247
- const windowMs = resolveTtlSec(process.env) * 1e3 * 5;
1248
- const note = freshReceiptFromFile(trackFile(sessionId, defaultStateDir(process.cwd())), windowMs, now) === null ? " NO VERIFICATION RECEIPT — run tsc + tests before reporting done." : "";
1249
- return contextResponse("SubagentStop", `SNIPER VALIDATION REQUIRED: Agent '${agentType}' modified ${owned.length} code file(s): ${owned.join(", ")}. Run sniper agent now.${note}`);
1376
+ const hookCwd = typeof data.cwd === "string" ? data.cwd : process.cwd();
1377
+ const present = owned.filter((f) => existsSync(resolve(hookCwd, f)));
1378
+ if (present.length > 0) {
1379
+ const windowMs = resolveTtlSec(process.env) * 1e3 * 5;
1380
+ const note = freshReceiptFromFile(trackFile(sessionId, defaultStateDir(process.cwd())), windowMs, now) === null ? " NO VERIFICATION RECEIPT — run tsc + tests before reporting done." : "";
1381
+ return contextResponse("SubagentStop", `SNIPER VALIDATION REQUIRED: Agent '${agentType}' modified ${present.length} code file(s): ${present.join(", ")}. Run sniper agent now.${note}`);
1382
+ }
1250
1383
  }
1251
1384
  }
1252
1385
  return JSON.stringify({ message: `Agent ${agentType} completed (no code changes)` });
@@ -1389,13 +1522,24 @@ const BURST_DEDUP_MS = 2e3;
1389
1522
  * nothing surfaced skill credits, freshness, or the sniper reminder to the human.
1390
1523
  *
1391
1524
  * The text builders are pure. `refCreditNoticeFor` is the one exception — like
1392
- * `pre-allow.ts`/`track-changes.ts` already do, it calls the existing
1393
- * {@link oncePerWindow} file-backed cooldown gate directly, so a caller can drop
1394
- * it straight into a PostToolUse loop without re-deriving the dedup. Rendering
1395
- * onto a harness's native stdout always goes through the existing adapter
1396
- * helpers (`respond`/`attachSystemMessage`); a harness with no `systemMessage`
1397
- * channel (e.g. cline) silently drops the notice there (documented no-op, never
1398
- * a crash) nothing in this module renders directly.
1525
+ * `pre-allow.ts` uses the JSON `oncePerWindow`, this one uses the exclusive
1526
+ * {@link onceExclusive} mode instead: it fires on the same short burst window
1527
+ * as `track-changes.ts`'s sniper reminder, fanned out across every installed
1528
+ * plugin's PostToolUse hook for ONE real edit, so the same lost-update race
1529
+ * applies (lesson 2026-07-05 16:00). Rendering onto a harness's native stdout
1530
+ * always goes through the existing adapter helpers (`respond`/
1531
+ * `attachSystemMessage`); a harness with no `systemMessage` channel (e.g.
1532
+ * cline) silently drops the notice there (documented no-op, never a crash) —
1533
+ * nothing in this module renders directly.
1534
+ *
1535
+ * CHANNEL CONTRACT — human-only, exactly once: the `userMessage`/`systemMessage`
1536
+ * these notices ride is a HUMAN channel by platform contract. It never reaches
1537
+ * the agent — `formatPrompt` (src/prompt/types.ts) excludes it — so the emitting
1538
+ * sub-agent does NOT see its own notice; only the human owner does, and exactly
1539
+ * ONCE even under the multi-plugin fan-out, because the emitter is gated by
1540
+ * {@link onceExclusive}. Verified live 2026-07-06: under the real ×11 fan-out a
1541
+ * single skill-ref Read surfaced one `✓ SOLID refs read` line in the owner's
1542
+ * terminal (not duplicated, and correctly invisible to the agent).
1399
1543
  */
1400
1544
  /** One compliance line: `✓ <gate> — <detail>` (detail omitted when empty). */
1401
1545
  function complianceNotice(gate, detail) {
@@ -1439,7 +1583,7 @@ function refCreditNoticeFor(activities, sessionId, now, dir) {
1439
1583
  if (a.kind !== "ref" || !a.path) continue;
1440
1584
  const notice = refCreditedNotice(a.path);
1441
1585
  if (!notice) continue;
1442
- if (!oncePerWindow(`ref-credited:${sessionId}:${a.path}`, 2e3, {
1586
+ if (!onceExclusive(`ref-credited:${sessionId}:${a.path}`, 2e3, {
1443
1587
  now,
1444
1588
  dir
1445
1589
  })) continue;
@@ -1482,7 +1626,7 @@ function trackSessionChanges(sessionIdRaw, filePath, home = homedir(), now = Dat
1482
1626
  lastCheck: new Date(now).toISOString().replace(/\.\d{3}Z$/, "Z")
1483
1627
  };
1484
1628
  saveSessionState(sid, state, home);
1485
- if (!oncePerWindow(`sniper:${sid}:${filePath}`, 2e3, {
1629
+ if (!onceExclusive(`sniper:${sid}:${filePath}`, 2e3, {
1486
1630
  now,
1487
1631
  dir: sessionsDir(home)
1488
1632
  })) return "";
@@ -6125,6 +6269,93 @@ function reconcileRefReadsFromTranscript(track, transcriptPath, now) {
6125
6269
  return next;
6126
6270
  }
6127
6271
  //#endregion
6272
+ //#region src/freshness/ref-journal.ts
6273
+ /**
6274
+ * Append-only journal of `.md` reference reads — the FRESH, race-immune companion
6275
+ * to {@link reconcileRefReadsFromTranscript}.
6276
+ *
6277
+ * WHY a third source (track + transcript were not enough for teammates): the live
6278
+ * session track write is a non-atomic load→mutate→save, so under the multi-plugin
6279
+ * hook fan-out (one process per plugin, ×N) a lone `refsRead` write is clobbered
6280
+ * (lost update). The transcript reconcile recovers the LEAD's lost reads because,
6281
+ * by the time the lead edits, its Read has flushed to the platform transcript. But
6282
+ * the platform flushes that JSONL to disk with a MULTI-MINUTE lag (measured ~230s,
6283
+ * well past the 120s freshness TTL): a background TEAMMATE reads the exact listed
6284
+ * ref then edits within seconds — far faster than the flush — so the transcript on
6285
+ * disk does NOT yet contain the teammate's read, reconcile misses it, and (its live
6286
+ * track write having been lost to the fan-out) solidReadGate blocks despite a
6287
+ * genuine read. That is the "teammate solidRead" gap.
6288
+ *
6289
+ * This journal closes it: every credited `.md` read is appended (one JSON line) the
6290
+ * instant PostToolUse fires — O_APPEND is per-write atomic, so concurrent fan-out
6291
+ * processes each add their own line and none is lost, and there is no flush lag. The
6292
+ * gate folds it back BEFORE any refsRead consumer, alongside the transcript.
6293
+ *
6294
+ * ANTI-FORGERY: it lives in the out-of-tree state dir the protected-path guard
6295
+ * denies agents from writing (SAME boundary as the signed track); the only writer is
6296
+ * our PostToolUse on a real `.md` Read. COST: the gate reads it once per edit, bounded
6297
+ * by {@link appendRefRead}'s trim cap — cheaper than the multi-MB transcript parse.
6298
+ */
6299
+ /** Journal filename inside the per-session state dir. */
6300
+ const JOURNAL = "refs-read.log";
6301
+ /**
6302
+ * Append a credited `.md` read to the state-dir journal (O_APPEND, atomic under
6303
+ * the fan-out). No-op for non-`.md` paths (parity with the reconcile filter).
6304
+ * Fully fail-open: a mkdir/append error is swallowed so recording never blocks the
6305
+ * PostToolUse path.
6306
+ * @param dir - Per-session state dir (`dirname(trackFile)`).
6307
+ * @param path - The read file's absolute path.
6308
+ * @param ts - The tool event's epoch-ms timestamp.
6309
+ */
6310
+ function appendRefRead(dir, path, ts) {
6311
+ if (!path.endsWith(".md")) return;
6312
+ try {
6313
+ mkdirSync(dir, { recursive: true });
6314
+ const file = join(dir, JOURNAL);
6315
+ appendFileSync(file, JSON.stringify({
6316
+ p: path,
6317
+ t: ts
6318
+ }) + "\n", "utf-8");
6319
+ trimLogFile(file, 128 * 1024, 1e3);
6320
+ } catch {}
6321
+ }
6322
+ /**
6323
+ * Fold every `.md` read in the state-dir journal into `track` (immutably), each
6324
+ * stamped with its journalled timestamp and never rolling back a MORE-recent
6325
+ * existing stamp — identical merge semantics to
6326
+ * {@link reconcileRefReadsFromTranscript}, but from the fresh append-only journal
6327
+ * instead of the lagged transcript. Fail-open: an absent/unreadable journal returns
6328
+ * `track` unchanged (same reference).
6329
+ * @param track - The current (possibly race-damaged) session track.
6330
+ * @param dir - Per-session state dir (`dirname(trackFile)`).
6331
+ * @param now - Fallback epoch-ms for entries with an invalid timestamp.
6332
+ * @returns The track with journalled `.md` reads merged into `refsRead`/`refsReadAt`.
6333
+ */
6334
+ function reconcileRefReadsFromJournal(track, dir, now) {
6335
+ let text;
6336
+ try {
6337
+ text = readText(join(dir, JOURNAL));
6338
+ } catch {
6339
+ return track;
6340
+ }
6341
+ let next = track;
6342
+ for (const line of text.split("\n")) {
6343
+ if (!line.trim()) continue;
6344
+ let entry;
6345
+ try {
6346
+ entry = JSON.parse(line);
6347
+ } catch {
6348
+ continue;
6349
+ }
6350
+ const path = typeof entry.p === "string" ? entry.p : "";
6351
+ if (!path.endsWith(".md")) continue;
6352
+ const ts = typeof entry.t === "number" && Number.isFinite(entry.t) ? entry.t : now;
6353
+ const prev = next.refsReadAt?.[path];
6354
+ if (prev === void 0 || prev < ts) next = recordRefRead(next, path, ts);
6355
+ }
6356
+ return next;
6357
+ }
6358
+ //#endregion
6128
6359
  //#region src/policy/shadcn-skill-gate.ts
6129
6360
  /** File extensions the shadcn gate polices (source: `\.(tsx|jsx|css|scss|json)$`). */
6130
6361
  const SHADCN_FILE_RE = /\.(tsx|jsx|css|scss|json)$/;
@@ -6586,7 +6817,7 @@ async function runGates(input) {
6586
6817
  if (modular) return modular;
6587
6818
  if (!input.filePath) return null;
6588
6819
  const filePath = input.filePath;
6589
- const track = reconcileRefReadsFromTranscript(await loadTrack(input.trackFile), input.transcriptPath, input.now);
6820
+ const track = reconcileRefReadsFromJournal(reconcileRefReadsFromTranscript(await loadTrack(input.trackFile), input.transcriptPath, input.now), dirname(input.trackFile), input.now);
6590
6821
  const solidOrSkill = frameworkSkillGate(input, track.refsRead, existingCodeLines);
6591
6822
  if (solidOrSkill) return solidOrSkill;
6592
6823
  if (isShadcnWrite(input.tool, filePath)) {
@@ -6729,6 +6960,7 @@ function mcpPostStore(tool, input, response, dir, now = Date.now()) {
6729
6960
  async function recordActivity(file, activity) {
6730
6961
  const track = await loadTrack(file);
6731
6962
  await saveTrack(file, activity.kind === "agent" ? recordAgent(track, activity.name, activity.ts, activity.quality) : activity.kind === "doc" ? recordDoc(track, activity.framework, activity.sessionId, activity.source, activity.ts) : recordRefRead(track, activity.path, activity.ts));
6963
+ if (activity.kind === "ref") appendRefRead(dirname(file), activity.path, activity.ts ?? Date.now());
6732
6964
  }
6733
6965
  //#endregion
6734
6966
  //#region src/runtime/respond.ts
@@ -6769,6 +7001,7 @@ function respond(id, prompt) {
6769
7001
  case "codex":
6770
7002
  if (kind === "block") return denyResponse("PreToolUse", message);
6771
7003
  if (kind === "inform") return userMessage ? informResponse("PreToolUse", userMessage, reason ? message : "") : contextResponse("PreToolUse", message);
7004
+ if (id === "codex") return denyResponse("PreToolUse", `[downgraded from ask — Codex has no interactive approval]\n${message}`);
6772
7005
  return JSON.stringify({ hookSpecificOutput: {
6773
7006
  hookEventName: "PreToolUse",
6774
7007
  permissionDecision: "ask",
@@ -7314,6 +7547,47 @@ async function allowOutcome(id, event, payload, mcpDir, cwd, evidence) {
7314
7547
  };
7315
7548
  }
7316
7549
  //#endregion
7550
+ //#region src/runtime/apply-patch-gate.ts
7551
+ /**
7552
+ * OR the static per-file verdict for a Codex `apply_patch` envelope: run the
7553
+ * file-level gates (protected-path, SOLID file-size, DRY) that key off
7554
+ * `filePath`/`content` over EACH touched file and return the first blocking
7555
+ * {@link Prompt}. One violating hunk blocks the whole patch — the parity the
7556
+ * single-file `Write`/`Edit` path already has, extended to the multi-file
7557
+ * primitive.
7558
+ *
7559
+ * Only the read-only/pure gates run here (no session-state writes), so the
7560
+ * `~11×` hook fan-out stays idempotent — the stateful APEX freshness/skill
7561
+ * gates are tool-level and never policed `apply_patch` (its `filePath` was
7562
+ * always undefined), so they are intentionally out of scope.
7563
+ *
7564
+ * File-size tool mapping mirrors Claude: an `add` is judged like a `Write`
7565
+ * (full new content), an `update` like an `Edit` (partial content, compared
7566
+ * against the on-disk count so an already-oversized file still blocks).
7567
+ * @param files - Per-file changes from {@link NormalizedFile}.
7568
+ * @param cwd - Project root for the DRY codebase grep.
7569
+ * @returns The first blocking prompt, or null when every file passes.
7570
+ */
7571
+ function applyPatchGate(files, cwd) {
7572
+ for (const f of files) {
7573
+ const tool = f.op === "add" ? "Write" : "Edit";
7574
+ const protectedDeny = protectedPathGate(tool, f.filePath);
7575
+ if (protectedDeny) return protectedDeny;
7576
+ if (f.op === "delete") continue;
7577
+ const { raw: existingLines } = existingLineCounts(f.filePath);
7578
+ const quick = evaluate({
7579
+ tool,
7580
+ filePath: f.filePath,
7581
+ content: f.content,
7582
+ existingLines
7583
+ });
7584
+ if (quick.decision !== "allow" && quick.prompt) return quick.prompt;
7585
+ const dry = dryGate(tool, f.filePath, f.content, cwd);
7586
+ if (dry) return dry;
7587
+ }
7588
+ return null;
7589
+ }
7590
+ //#endregion
7317
7591
  //#region src/runtime/handle-pre.ts
7318
7592
  /**
7319
7593
  * Run the PreToolUse pipeline: MCP/WebFetch cache intercept, design gate, APEX
@@ -7357,6 +7631,13 @@ async function handlePre(ctx) {
7357
7631
  exit: 0
7358
7632
  };
7359
7633
  }
7634
+ if (event.files && event.files.length > 0) {
7635
+ const patchPrompt = applyPatchGate(event.files, opts.cwd);
7636
+ if (patchPrompt) return {
7637
+ stdout: respond(id, patchPrompt),
7638
+ exit: 0
7639
+ };
7640
+ }
7360
7641
  const prompt = await gate({
7361
7642
  sessionId: event.sessionId,
7362
7643
  framework,
@@ -7670,6 +7951,51 @@ async function asyncScopeStdout(scope, event, payload, cwd, now) {
7670
7951
  return null;
7671
7952
  }
7672
7953
  //#endregion
7954
+ //#region src/runtime/inject-budget-recap.ts
7955
+ /**
7956
+ * @module inject-budget-recap
7957
+ * ONE event-level recap of every {@link module:inject-budget.capFragment}
7958
+ * -tracked fragment injected for a SessionStart/SubagentStart event — the
7959
+ * aggregated view the per-fragment caps never gave: each injection point
7960
+ * (dev-context, snapshot sections, lessons, apex-subagent, ...) caps and
7961
+ * reports itself in ISOLATION, with no total across the whole event.
7962
+ *
7963
+ * Rides the user-visible `systemMessage` channel, Claude-Code-only (gate at
7964
+ * the call site — mirrors the existing `id === "claude-code"` pattern for
7965
+ * `designLifecycle` in handle.ts): other adapters do not all re-export
7966
+ * `attachSystemMessage`, and stamping a Claude-shaped envelope onto another
7967
+ * harness's stdout shape would be silently wrong there.
7968
+ *
7969
+ * Deduped via {@link module:inject-dedup.onceExclusive}, not the JSON
7970
+ * `oncePerWindow`: SessionStart/SubagentStart fan out across every installed
7971
+ * plugin exactly like PostToolUse does (see burst-window.ts) — a shared-JSON
7972
+ * read-modify-write here would risk the same lost-update race already fixed
7973
+ * for the sniper reminder (lesson 2026-07-05 16:00).
7974
+ * @packageDocumentation
7975
+ */
7976
+ /**
7977
+ * Attach the aggregated recap onto `stdout` when 2+ fragments were recorded
7978
+ * for `rawEvent`. A lone fragment already carries its own visibility (its
7979
+ * producer's own per-fragment report, when it has one) — no recap is added
7980
+ * for that case, so the common single-fragment event stays noise-free.
7981
+ * @param stdout - The already-rendered hook stdout for this event.
7982
+ * @param rawEvent - The raw hook event name (only SessionStart/SubagentStart qualify; others pass through unchanged).
7983
+ * @param sessionId - Current session id (dedup scope).
7984
+ * @param cwd - Project root (state-dir scope for the dedup marker).
7985
+ * @param now - Clock.
7986
+ * @returns `stdout` with a `systemMessage` recap attached, or `stdout` unchanged.
7987
+ */
7988
+ function attachBudgetRecap(stdout, rawEvent, sessionId, cwd, now) {
7989
+ if (rawEvent !== "SessionStart" && rawEvent !== "SubagentStart") return stdout;
7990
+ const fragments = fragmentRegistry();
7991
+ if (fragments.length <= 1) return stdout;
7992
+ if (!onceExclusive(`budget:${sessionId}:${rawEvent}`, 2e3, {
7993
+ now,
7994
+ dir: defaultStateDir(cwd)
7995
+ })) return stdout;
7996
+ return attachSystemMessage(stdout, budgetReport(fragments));
7997
+ }
7998
+ //#endregion
7673
7999
  //#region src/runtime/handle.ts
7674
8000
  /** Raw Claude hook event name from a payload (empty when absent). */
7675
8001
  function rawEventName(payload) {
@@ -7683,6 +8009,7 @@ function rawEventName(payload) {
7683
8009
  */
7684
8010
  async function handleHook(id, payload, opts) {
7685
8011
  const event = normalizeEvent(id, payload);
8012
+ resetFragmentRegistry();
7686
8013
  const layout = projectLayout(opts.cwd);
7687
8014
  const file = trackFile(event.sessionId, defaultStateDir(opts.cwd));
7688
8015
  const mcpDir = layout.cacheDir;
@@ -7698,7 +8025,7 @@ async function handleHook(id, payload, opts) {
7698
8025
  };
7699
8026
  const life = lifecycleStdout(payload, opts.cwd, opts.scope ?? "core", opts.now);
7700
8027
  if (life !== null) return {
7701
- stdout: life,
8028
+ stdout: id === "claude-code" ? attachBudgetRecap(life, rawEventName(payload), event.sessionId, opts.cwd, opts.now) : life,
7702
8029
  exit: 0
7703
8030
  };
7704
8031
  const userPrompt = typeof payload.prompt === "string" ? payload.prompt : void 0;
@@ -1,6 +1,6 @@
1
- import { t as evaluate } from "./evaluate-CNAzgxnN.mjs";
1
+ import { t as evaluate } from "./evaluate-BgFLlqrs.mjs";
2
2
  import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
3
- import { c as readClaudeInput } from "./claude-BxC9semG.mjs";
3
+ import { c as readClaudeInput } from "./claude-BLuab-tR.mjs";
4
4
  //#region src/adapters/hermes/index.ts
5
5
  /**
6
6
  * Hermes Agent adapter (hook-mode). Nous Research's hermes-agent (2026) pipes a
package/dist/index.mjs CHANGED
@@ -6,7 +6,7 @@ import { i as walkUpFor, n as projectRoot, r as projectRootOrNull, t as isCodeFi
6
6
  import { n as detectMode, r as modeFor, t as detectHarness } from "./harness-Cb9xR8dC.mjs";
7
7
  import { A as detectCreationIntent, B as DEV_KEYWORDS, C as SKILL_TRIGGERS, D as MAX_EXA_RESULTS, F as docConsultedGate, H as detectProjectType, I as evaluateApex, L as freshnessGate, M as POST_AUTH_GATES, N as PRE_AUTH_GATES, O as MAX_TOKENS, P as brainstormGate, R as solidReadGate, S as usesTailwindUtilities, U as isApexCommand, V as detectModularArchitecture, W as requiredArchSkill, a as firstHeading, b as detectRequiredSkills, c as EXCLUDE_DIRS, d as buildApexTaskInjection, f as loadApexTaskState, g as detectClaudeMdProjectType, h as buildClaudeMdContext, i as firstComment, j as APEX_GATES, k as capVerbosity, l as PROJECT_INDICATORS, m as buildApexInstruction, n as missingSeoElements, o as parseEnrichment, p as DEV_VERBS, r as descFromText, s as parseEntry, t as isHtmlLike, u as buildApexTaskContext, v as parseBodyDesc, w as frameworkSolidGate, x as skillTriggerGate, y as parseField, z as detectFramework } from "./validate-DCQ8dkdL.mjs";
8
8
  import { d as countFrameworkCodeLines, f as countLines, l as PLUGINS_DIR, p as evaluateFileSize, u as SOLID_REF } from "./home-state-D0RLWP8J.mjs";
9
- import { A as PROJECT_INSTALL, C as PROTECTED_GIT_RE, D as securityGuard, E as CRITICAL_PATTERNS, M as SYSTEM_INSTALL, N as isRalphMode, O as GIT_ASK, P as matchPatterns, S as PROTECTED_FRAGMENTS, T as ASK_PATTERNS, _ as CODE_MUTATORS, a as registerGuard, b as SAFE_PREFIXES, c as GO_DECL_RE, d as PY_MODEL_RE, f as SWIFT_PROTO_RE, g as ASK_WRITERS, h as bashWriteGuard, i as clearUserGuards, j as RALPH_SAFE, k as GIT_BLOCKED, l as JAVA_DECL_RE, m as interfaceSeparationGuard, n as FAIL_CLOSED, o as runGuards, p as TS_DECL_RE, r as GUARDS, s as installGuard, t as evaluate, u as PHP_DECL_RE, v as CODE_REDIRECT, w as protectedPathGuard, x as SESSION_STATE_FRAGMENT, y as FILE_REDIRECT } from "./evaluate-CNAzgxnN.mjs";
9
+ import { A as PROJECT_INSTALL, C as PROTECTED_GIT_RE, D as securityGuard, E as CRITICAL_PATTERNS, M as SYSTEM_INSTALL, N as isRalphMode, O as GIT_ASK, P as matchPatterns, S as PROTECTED_FRAGMENTS, T as ASK_PATTERNS, _ as CODE_MUTATORS, a as registerGuard, b as SAFE_PREFIXES, c as GO_DECL_RE, d as PY_MODEL_RE, f as SWIFT_PROTO_RE, g as ASK_WRITERS, h as bashWriteGuard, i as clearUserGuards, j as RALPH_SAFE, k as GIT_BLOCKED, l as JAVA_DECL_RE, m as interfaceSeparationGuard, n as FAIL_CLOSED, o as runGuards, p as TS_DECL_RE, r as GUARDS, s as installGuard, t as evaluate, u as PHP_DECL_RE, v as CODE_REDIRECT, w as protectedPathGuard, x as SESSION_STATE_FRAGMENT, y as FILE_REDIRECT } from "./evaluate-BgFLlqrs.mjs";
10
10
  import { i as resolveSessions, n as formatDocSatisfactionStatus, r as isDocConsulted, t as formatDocDeny } from "./doc-helpers-CWZegVdR.mjs";
11
11
  import { i as parseFrontmatter, n as scoreReferences, r as globToRe, t as routeReferences } from "./router-PKVNBHge.mjs";
12
12
  import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
@@ -1,5 +1,5 @@
1
1
  import { A as detectCreationIntent, B as DEV_KEYWORDS, C as SKILL_TRIGGERS, D as MAX_EXA_RESULTS, F as docConsultedGate, H as detectProjectType, I as evaluateApex, L as freshnessGate, M as POST_AUTH_GATES, N as PRE_AUTH_GATES, O as MAX_TOKENS, P as brainstormGate, R as solidReadGate, S as usesTailwindUtilities, U as isApexCommand, V as detectModularArchitecture, W as requiredArchSkill, a as firstHeading, b as detectRequiredSkills, c as EXCLUDE_DIRS, d as buildApexTaskInjection, f as loadApexTaskState, g as detectClaudeMdProjectType, h as buildClaudeMdContext, i as firstComment, j as APEX_GATES, k as capVerbosity, l as PROJECT_INDICATORS, m as buildApexInstruction, n as missingSeoElements, o as parseEnrichment, p as DEV_VERBS, r as descFromText, s as parseEntry, t as isHtmlLike, u as buildApexTaskContext, v as parseBodyDesc, w as frameworkSolidGate, x as skillTriggerGate, y as parseField, z as detectFramework } from "../validate-DCQ8dkdL.mjs";
2
2
  import { d as countFrameworkCodeLines, f as countLines, l as PLUGINS_DIR, p as evaluateFileSize, u as SOLID_REF } from "../home-state-D0RLWP8J.mjs";
3
- import { A as PROJECT_INSTALL, C as PROTECTED_GIT_RE, D as securityGuard, E as CRITICAL_PATTERNS, M as SYSTEM_INSTALL, N as isRalphMode, O as GIT_ASK, P as matchPatterns, S as PROTECTED_FRAGMENTS, T as ASK_PATTERNS, _ as CODE_MUTATORS, a as registerGuard, b as SAFE_PREFIXES, c as GO_DECL_RE, d as PY_MODEL_RE, f as SWIFT_PROTO_RE, g as ASK_WRITERS, h as bashWriteGuard, i as clearUserGuards, j as RALPH_SAFE, k as GIT_BLOCKED, l as JAVA_DECL_RE, m as interfaceSeparationGuard, n as FAIL_CLOSED, o as runGuards, p as TS_DECL_RE, r as GUARDS, s as installGuard, t as evaluate, u as PHP_DECL_RE, v as CODE_REDIRECT, w as protectedPathGuard, x as SESSION_STATE_FRAGMENT, y as FILE_REDIRECT } from "../evaluate-CNAzgxnN.mjs";
3
+ import { A as PROJECT_INSTALL, C as PROTECTED_GIT_RE, D as securityGuard, E as CRITICAL_PATTERNS, M as SYSTEM_INSTALL, N as isRalphMode, O as GIT_ASK, P as matchPatterns, S as PROTECTED_FRAGMENTS, T as ASK_PATTERNS, _ as CODE_MUTATORS, a as registerGuard, b as SAFE_PREFIXES, c as GO_DECL_RE, d as PY_MODEL_RE, f as SWIFT_PROTO_RE, g as ASK_WRITERS, h as bashWriteGuard, i as clearUserGuards, j as RALPH_SAFE, k as GIT_BLOCKED, l as JAVA_DECL_RE, m as interfaceSeparationGuard, n as FAIL_CLOSED, o as runGuards, p as TS_DECL_RE, r as GUARDS, s as installGuard, t as evaluate, u as PHP_DECL_RE, v as CODE_REDIRECT, w as protectedPathGuard, x as SESSION_STATE_FRAGMENT, y as FILE_REDIRECT } from "../evaluate-BgFLlqrs.mjs";
4
4
  import "../policy-la_KkjCS.mjs";
5
5
  export { APEX_GATES, ASK_PATTERNS, ASK_WRITERS, CODE_MUTATORS, CODE_REDIRECT, CRITICAL_PATTERNS, DEV_KEYWORDS, DEV_VERBS, EXCLUDE_DIRS, FAIL_CLOSED, FILE_REDIRECT, GIT_ASK, GIT_BLOCKED, GO_DECL_RE, GUARDS, JAVA_DECL_RE, MAX_EXA_RESULTS, MAX_TOKENS, PHP_DECL_RE, PLUGINS_DIR, POST_AUTH_GATES, PRE_AUTH_GATES, PROJECT_INDICATORS, PROJECT_INSTALL, PROTECTED_FRAGMENTS, PROTECTED_GIT_RE, PY_MODEL_RE, RALPH_SAFE, SAFE_PREFIXES, SESSION_STATE_FRAGMENT, SKILL_TRIGGERS, SOLID_REF, SWIFT_PROTO_RE, SYSTEM_INSTALL, TS_DECL_RE, bashWriteGuard, brainstormGate, buildApexInstruction, buildApexTaskContext, buildApexTaskInjection, buildClaudeMdContext, capVerbosity, clearUserGuards, countFrameworkCodeLines, countLines, descFromText, detectClaudeMdProjectType, detectCreationIntent, detectFramework, detectModularArchitecture, detectProjectType, detectRequiredSkills, docConsultedGate, evaluate, evaluateApex, evaluateFileSize, firstComment, firstHeading, frameworkSolidGate, freshnessGate, installGuard, interfaceSeparationGuard, isApexCommand, isHtmlLike, isRalphMode, loadApexTaskState, matchPatterns, missingSeoElements, parseBodyDesc, parseEnrichment, parseEntry, parseField, protectedPathGuard, registerGuard, requiredArchSkill, runGuards, securityGuard, skillTriggerGate, solidReadGate, usesTailwindUtilities };
@@ -1,5 +1,5 @@
1
1
  import { t as isCodeFile } from "./project-root-3kk7gCOp.mjs";
2
- import { t as evaluate } from "./evaluate-CNAzgxnN.mjs";
2
+ import { t as evaluate } from "./evaluate-BgFLlqrs.mjs";
3
3
  import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
4
4
  import { execSync } from "node:child_process";
5
5
  //#region src/cli/run.ts
@@ -150,6 +150,12 @@ declare function dryGate(tool: string, filePath: string, content: string | undef
150
150
  declare function preCommitGate(tool: string, command: string | undefined, cwd: string | undefined): Prompt | null;
151
151
  //#endregion
152
152
  //#region src/runtime/normalize.d.ts
153
+ /** One file fanned out of a multi-file edit primitive (Codex `apply_patch`). */
154
+ interface NormalizedFile {
155
+ filePath: string;
156
+ content: string;
157
+ op: "add" | "update" | "delete";
158
+ }
153
159
  /** A hook event normalized across harnesses. */
154
160
  interface NormalizedEvent {
155
161
  phase: "pre" | "post";
@@ -161,6 +167,13 @@ interface NormalizedEvent {
161
167
  command?: string;
162
168
  /** Subagent type, if the tool-use came from one (Explore/Plan are file-size-exempt). */
163
169
  agentType?: string;
170
+ /**
171
+ * Per-file changes when the tool is a multi-file edit primitive (Codex
172
+ * `apply_patch`). Present ONLY for `apply_patch`; the file gates OR each
173
+ * entry's verdict so one violating hunk blocks the whole envelope. Left
174
+ * undefined for every other tool/harness (single-file `filePath`/`content`).
175
+ */
176
+ files?: NormalizedFile[];
164
177
  }
165
178
  /**
166
179
  * Normalize a harness hook payload into a uniform event. Handles Cline's nested
@@ -830,4 +843,4 @@ interface PreContext {
830
843
  */
831
844
  declare function handlePre(ctx: PreContext): Promise<HandleOutcome>;
832
845
  //#endregion
833
- export { Activity, DEFAULT_WINDOW_MS, DuplicationVerdict, type GateInput, HandleOptions, HandleOutcome, LifecycleInput, MCP_TTL_MS, McpIntercept, NormalizedEvent, PluginScope, PreContext, REQUIRED_AGENTS, SolidProfile, TRIVIAL_BUDGET, ToolEvent, WEBFETCH_TTL_MS, activityFor, aipilotPostToolUse, cacheQueryOf, cartoSessionStart, checkFileSize, claudeHome, claudeMdKey, cleanupSession, countFiles, countLoc, defaultStateDir, detectDuplication, detectSolidProfile, devContext, dispatchAipilot, dispatchLessons, dispatchLifecycle, dispatchMemory, dryGate, extractSymbols, fuseHarnessHome, fusengineCache, gate, generateEcosystemMap, generateProjectMap, getFileDesc, gitContext, handleHook, handlePre, harnessStateDir, injectRules, isMcpTool, isProject, isoUtc, lessonsArchiveFileFor, lessonsFileFor, lessonsStateFileFor, lifecycleStdout, listChildren, loadEnriched, loadSecurityState, loadSessionState, logToolFailure, mcpPostStore, mcpPreIntercept, mergeLines, normalizeEvent, postEditContext, postEditTypescript, postTrackingSideEffects, preCommitGate, projectContext, projectHash, promptSubmitContext, pruneEmptyDirs, purgeTtlTree, queryOf, readRules, recordActivity, removeOldFiles, respond, runSessionStartCleanups, sanitizeSessionId, saveApexState, saveSecurityState, saveSessionState, securityAdvisory, securityStateDir, securityStatePath, seoPostToolUse, seoPostToolUseResponse, sessionStartCore, sessionStatePath, sessionsDir, solidDetectStart, subagentCacheContext, taskContext, todayUtc, trackAgentMemory, trackEnrichment, trackFile, trackMcpResearch, trackSessionChanges, trackSkillRead, trackWatchResearch, trimLogFile, validateRulesLoaded, validateSolidGate, validateTailwind, validateTeammateOutput, writePluginMap, writeTree };
846
+ export { Activity, DEFAULT_WINDOW_MS, DuplicationVerdict, type GateInput, HandleOptions, HandleOutcome, LifecycleInput, MCP_TTL_MS, McpIntercept, NormalizedEvent, NormalizedFile, PluginScope, PreContext, REQUIRED_AGENTS, SolidProfile, TRIVIAL_BUDGET, ToolEvent, WEBFETCH_TTL_MS, activityFor, aipilotPostToolUse, cacheQueryOf, cartoSessionStart, checkFileSize, claudeHome, claudeMdKey, cleanupSession, countFiles, countLoc, defaultStateDir, detectDuplication, detectSolidProfile, devContext, dispatchAipilot, dispatchLessons, dispatchLifecycle, dispatchMemory, dryGate, extractSymbols, fuseHarnessHome, fusengineCache, gate, generateEcosystemMap, generateProjectMap, getFileDesc, gitContext, handleHook, handlePre, harnessStateDir, injectRules, isMcpTool, isProject, isoUtc, lessonsArchiveFileFor, lessonsFileFor, lessonsStateFileFor, lifecycleStdout, listChildren, loadEnriched, loadSecurityState, loadSessionState, logToolFailure, mcpPostStore, mcpPreIntercept, mergeLines, normalizeEvent, postEditContext, postEditTypescript, postTrackingSideEffects, preCommitGate, projectContext, projectHash, promptSubmitContext, pruneEmptyDirs, purgeTtlTree, queryOf, readRules, recordActivity, removeOldFiles, respond, runSessionStartCleanups, sanitizeSessionId, saveApexState, saveSecurityState, saveSessionState, securityAdvisory, securityStateDir, securityStatePath, seoPostToolUse, seoPostToolUseResponse, sessionStartCore, sessionStatePath, sessionsDir, solidDetectStart, subagentCacheContext, taskContext, todayUtc, trackAgentMemory, trackEnrichment, trackFile, trackMcpResearch, trackSessionChanges, trackSkillRead, trackWatchResearch, trimLogFile, validateRulesLoaded, validateSolidGate, validateTailwind, validateTeammateOutput, writePluginMap, writeTree };
@@ -1,6 +1,6 @@
1
1
  import { r as projectLayout } from "../layout-C0jaaCQC.mjs";
2
2
  import { a as sanitizeSessionId, c as sessionsDir, i as loadSessionState, n as fuseHarnessHome, o as saveSessionState, r as fusengineCache, s as sessionStatePath, t as claudeHome } from "../home-state-D0RLWP8J.mjs";
3
- import { $ as postEditTypescript, A as trackSkillRead, At as trackFile, B as isProject, C as seoPostToolUse, Ct as gitContext, D as postTrackingSideEffects, Dt as taskContext, E as securityAdvisory, Et as promptSubmitContext, F as dispatchLessons, Ft as securityStateDir, G as getFileDesc, H as loadEnriched, I as cartoSessionStart, It as securityStatePath, K as listChildren, L as generateEcosystemMap, Lt as todayUtc, M as dispatchLifecycle, Mt as isoUtc, N as aipilotPostToolUse, Nt as loadSecurityState, O as trackWatchResearch, Ot as defaultStateDir, P as dispatchAipilot, Pt as saveSecurityState, Q as lessonsStateFileFor, R as writePluginMap, S as postEditContext, St as devContext, T as dispatchMemory, Tt as claudeMdKey, U as mergeLines, V as writeTree, W as countFiles, X as lessonsArchiveFileFor, Z as lessonsFileFor, _ as preCommitGate, _t as sessionStartCore, a as recordActivity, at as validateTeammateOutput, b as extractSymbols, bt as removeOldFiles, c as MCP_TTL_MS, ct as validateTailwind, d as isMcpTool, dt as countLoc, et as trackSessionChanges, f as queryOf, ft as detectSolidProfile, g as gate, gt as runSessionStartCleanups, h as TRIVIAL_BUDGET, ht as readRules, i as respond, it as logToolFailure, j as trackEnrichment, jt as normalizeEvent, k as trackMcpResearch, kt as projectHash, l as WEBFETCH_TTL_MS, lt as validateSolidGate, m as REQUIRED_AGENTS, mt as injectRules, n as activityFor, nt as cleanupSession, o as mcpPostStore, ot as trackAgentMemory, p as DEFAULT_WINDOW_MS, pt as solidDetectStart, r as handlePre, rt as saveApexState, s as mcpPreIntercept, st as subagentCacheContext, t as handleHook, tt as validateRulesLoaded, u as cacheQueryOf, ut as checkFileSize, v as detectDuplication, vt as pruneEmptyDirs, w as seoPostToolUseResponse, wt as projectContext, x as lifecycleStdout, xt as trimLogFile, y as dryGate, yt as purgeTtlTree, z as generateProjectMap } from "../handle-DY79I4a8.mjs";
3
+ import { $ as postEditTypescript, A as trackSkillRead, At as trackFile, B as isProject, C as seoPostToolUse, Ct as gitContext, D as postTrackingSideEffects, Dt as taskContext, E as securityAdvisory, Et as promptSubmitContext, F as dispatchLessons, Ft as securityStateDir, G as getFileDesc, H as loadEnriched, I as cartoSessionStart, It as securityStatePath, K as listChildren, L as generateEcosystemMap, Lt as todayUtc, M as dispatchLifecycle, Mt as isoUtc, N as aipilotPostToolUse, Nt as loadSecurityState, O as trackWatchResearch, Ot as defaultStateDir, P as dispatchAipilot, Pt as saveSecurityState, Q as lessonsStateFileFor, R as writePluginMap, S as postEditContext, St as devContext, T as dispatchMemory, Tt as claudeMdKey, U as mergeLines, V as writeTree, W as countFiles, X as lessonsArchiveFileFor, Z as lessonsFileFor, _ as preCommitGate, _t as sessionStartCore, a as recordActivity, at as validateTeammateOutput, b as extractSymbols, bt as removeOldFiles, c as MCP_TTL_MS, ct as validateTailwind, d as isMcpTool, dt as countLoc, et as trackSessionChanges, f as queryOf, ft as detectSolidProfile, g as gate, gt as runSessionStartCleanups, h as TRIVIAL_BUDGET, ht as readRules, i as respond, it as logToolFailure, j as trackEnrichment, jt as normalizeEvent, k as trackMcpResearch, kt as projectHash, l as WEBFETCH_TTL_MS, lt as validateSolidGate, m as REQUIRED_AGENTS, mt as injectRules, n as activityFor, nt as cleanupSession, o as mcpPostStore, ot as trackAgentMemory, p as DEFAULT_WINDOW_MS, pt as solidDetectStart, r as handlePre, rt as saveApexState, s as mcpPreIntercept, st as subagentCacheContext, t as handleHook, tt as validateRulesLoaded, u as cacheQueryOf, ut as checkFileSize, v as detectDuplication, vt as pruneEmptyDirs, w as seoPostToolUseResponse, wt as projectContext, x as lifecycleStdout, xt as trimLogFile, y as dryGate, yt as purgeTtlTree, z as generateProjectMap } from "../handle-B6yL03we.mjs";
4
4
  //#region src/runtime/storage.ts
5
5
  /**
6
6
  * The project's single state dir (`<root>/.harness`) — neutral + harness-agnostic,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusengine/harness",
3
- "version": "0.1.57",
3
+ "version": "0.1.59",
4
4
  "description": "Harness-agnostic toolkit for AI coding agents: runtime harness detection (Claude Code, Codex, Cursor, Cline, Gemini, Aider...), pure policy core (env config, project/framework detection, SOLID/file-size limits, APEX freshness, guard patterns, portable prompts), cache, project memory, ref routing, state/locks, statusline, per-harness adapters (Claude/Cursor/Cline/Gemini) and a cli-mode harness-check binary. Bun-native, with a built dist for Node + bundlers.",
5
5
  "type": "module",
6
6
  "module": "src/index.ts",