@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 +2 -2
- package/dist/adapters/claude/index.mjs +1 -1
- package/dist/adapters/cline/index.mjs +1 -1
- package/dist/adapters/codex/index.d.mts +14 -2
- package/dist/adapters/codex/index.mjs +66 -2
- package/dist/adapters/cursor/index.d.mts +33 -5
- package/dist/adapters/cursor/index.mjs +24 -3
- package/dist/adapters/gemini/index.mjs +1 -1
- package/dist/adapters/hermes/index.mjs +1 -1
- package/dist/apply-patch-CIS2EZ_q.mjs +81 -0
- package/dist/{claude-BxC9semG.mjs → claude-BLuab-tR.mjs} +1 -1
- package/dist/cli/bin.mjs +2 -2
- package/dist/cli/index.mjs +1 -1
- package/dist/{evaluate-CNAzgxnN.mjs → evaluate-BgFLlqrs.mjs} +1 -1
- package/dist/{handle-DY79I4a8.mjs → handle-B6yL03we.mjs} +350 -23
- package/dist/{hermes-DWXCRFZU.mjs → hermes-CyhCYqD7.mjs} +2 -2
- package/dist/index.mjs +1 -1
- package/dist/policy/index.mjs +1 -1
- package/dist/{run-DZvP_9xB.mjs → run-B4b2_7Qm.mjs} +1 -1
- package/dist/runtime/index.d.mts +14 -1
- package/dist/runtime/index.mjs +1 -1
- package/package.json +1 -1
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** |
|
|
129
|
-
| **cursor** | `beforeShellExecution` can deny/ask (shell only, `cursor/index.ts:16-21`) | none | `afterFileEdit`
|
|
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-
|
|
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,2 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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 {
|
|
2
|
-
|
|
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
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
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-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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,2 +1,2 @@
|
|
|
1
|
-
import { n as readHermesInput, r as toHermesResponse, t as guard } from "../../hermes-
|
|
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-
|
|
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-
|
|
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-
|
|
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";
|
package/dist/cli/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-
|
|
1
|
+
import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-B4b2_7Qm.mjs";
|
|
2
2
|
export { checkStaged, stagedContent, stagedFiles };
|
|
@@ -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-
|
|
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 {
|
|
17
|
-
import { r as
|
|
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
|
-
|
|
90
|
+
const tool = str(payload.tool_name) ?? "";
|
|
91
|
+
const base = {
|
|
90
92
|
phase: /post|after/i.test(event) ? "post" : "pre",
|
|
91
|
-
tool
|
|
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)
|
|
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
|
-
|
|
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
|
|
1248
|
-
const
|
|
1249
|
-
|
|
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
|
|
1393
|
-
* {@link
|
|
1394
|
-
*
|
|
1395
|
-
*
|
|
1396
|
-
*
|
|
1397
|
-
*
|
|
1398
|
-
*
|
|
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 (!
|
|
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 (!
|
|
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-
|
|
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-
|
|
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-
|
|
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";
|
package/dist/policy/index.mjs
CHANGED
|
@@ -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-
|
|
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-
|
|
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
|
package/dist/runtime/index.d.mts
CHANGED
|
@@ -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 };
|
package/dist/runtime/index.mjs
CHANGED
|
@@ -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-
|
|
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.
|
|
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",
|