@fusengine/harness 0.1.56 → 0.1.58
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 +150 -50
- 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-CoyvRORV.mjs → handle-BdOh1jPG.mjs} +2229 -1528
- 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 +16 -1
- package/dist/runtime/index.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
# @fusengine/harness
|
|
2
2
|
|
|
3
|
-
A **
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
and **thin per-harness adapters** that map a hook payload to the policy and back
|
|
10
|
-
to that harness's native response.
|
|
3
|
+
A **governance layer for AI coding agents**: gates backed by cited evidence
|
|
4
|
+
(not vibes), a decision-time lesson memory, verification receipts, and a
|
|
5
|
+
one-shot gate metric — ported from a Claude Code plugin into one **Bun-native**
|
|
6
|
+
npm package. The policy core is harness-agnostic; how much of it actually
|
|
7
|
+
*enforces* depends on the harness's own hook system — see the
|
|
8
|
+
[compatibility matrix](#compatibility) below before assuming parity across harnesses.
|
|
11
9
|
|
|
12
10
|
```
|
|
13
|
-
detect → init (
|
|
11
|
+
detect → init (writes that harness's hook wiring) → `harness hook` → guards + APEX gates → native deny/ask/context
|
|
14
12
|
```
|
|
15
13
|
|
|
16
14
|
## Install
|
|
@@ -25,18 +23,37 @@ bun add @fusengine/harness # Bun reads the TS source directly — no build
|
|
|
25
23
|
|
|
26
24
|
```sh
|
|
27
25
|
cd your-project
|
|
28
|
-
harness init # detects the harness, writes its pre+post
|
|
29
|
-
# The SOLID-read gate auto-activates from discovered skills (default marketplace: fusengine-plugins).
|
|
26
|
+
harness init # detects the harness, writes its pre+post hook wiring
|
|
30
27
|
export FUSE_HARNESS_MARKETPLACES=fusengine-plugins # (optional) which marketplaces to auto-scan
|
|
31
28
|
export FUSE_HARNESS_REFS=.claude/skills # (optional) explicit refs dir, overrides auto-discovery
|
|
32
29
|
```
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
`.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
`init` writes the wiring file for the detected harness (`.claude/settings.json`,
|
|
32
|
+
`.codex/hooks.json`, `.cursor/hooks.json`, `.gemini/settings.json`, or
|
|
33
|
+
`.clinerules/hooks/PreToolUse`+`PostToolUse`) — for Claude Code, Codex, Cursor,
|
|
34
|
+
Gemini CLI and Cline this covers **PreToolUse + PostToolUse only**
|
|
35
|
+
(`src/init/templates.ts`); Hermes has no `init` runner because its config lives
|
|
36
|
+
outside the project at `~/.hermes/config.yaml` (`src/adapters/hermes/index.ts:3-4`).
|
|
37
|
+
The richer lifecycle (SessionStart, SubagentStart/Stop, Stop, PreCompact/PostCompact,
|
|
38
|
+
TaskCompleted, TeammateIdle, PostToolUseFailure, InstructionsLoaded — 14 distinct
|
|
39
|
+
Claude Code hook events in total, `src/runtime/lifecycle/dispatch.ts` +
|
|
40
|
+
`src/runtime/handle.ts:56-80`) is **implemented** in the runtime but only fires
|
|
41
|
+
when something wires those extra hook events into `.claude/settings.json` — either
|
|
42
|
+
by hand, or via the fusengine-plugins marketplace this repo is itself developed
|
|
43
|
+
under (its ~11 sibling plugins each add their own event, `src/runtime/burst-window.ts:5-11`).
|
|
44
|
+
|
|
45
|
+
**Where state lives** (two separate roots — this changed across 0.1.34→0.1.51,
|
|
46
|
+
don't assume either the old or the "everything in one place" story):
|
|
47
|
+
|
|
48
|
+
- Session track, deny-loop/one-shot sidecars: **out-of-tree**, under
|
|
49
|
+
`~/.fuse-harness/state/<8-char-md5-of-project-path>/` (`src/runtime/paths.ts:5-6,41-43`) —
|
|
50
|
+
deliberately outside the repo so the protected-path guard never has to bless
|
|
51
|
+
writes to its own enforcement state.
|
|
52
|
+
- MCP/WebFetch response cache and the curated `MEMORY/LESSON.md`-equivalent lessons
|
|
53
|
+
file: still **in-tree**, under `<project>/.harness/{cache,memory}`
|
|
54
|
+
(`src/config/layout.ts:15-26`) — `.harness/` is gitignored except
|
|
55
|
+
`memory/LESSON.md`, which is meant to be committed (`STATE_GITIGNORE`,
|
|
56
|
+
`src/config/layout.ts:10`).
|
|
40
57
|
|
|
41
58
|
### CLI
|
|
42
59
|
|
|
@@ -66,7 +83,7 @@ Any consumer `hooks.json` / `settings.json` that runs the harness via `bunx`
|
|
|
66
83
|
|
|
67
84
|
```jsonc
|
|
68
85
|
// .claude/settings.json — correct: exact pin
|
|
69
|
-
"command": "bunx @fusengine/harness@0.1.
|
|
86
|
+
"command": "bunx @fusengine/harness@0.1.56 hook claude"
|
|
70
87
|
// WRONG — may silently keep running a stale global install:
|
|
71
88
|
"command": "bunx @fusengine/harness hook claude"
|
|
72
89
|
```
|
|
@@ -75,9 +92,8 @@ Any consumer `hooks.json` / `settings.json` that runs the harness via `bunx`
|
|
|
75
92
|
prefer an already-installed **global** copy over npm-latest, and publishing a new
|
|
76
93
|
version updates neither that global nor the bunx cache (`bun pm cache rm` does
|
|
77
94
|
**not** remove the global). A hook wired without an exact pin can therefore keep
|
|
78
|
-
executing an old harness indefinitely after you publish a fix
|
|
79
|
-
|
|
80
|
-
`bunx`-resolved binary. Pinning `@X.Y.Z` forces the exact version to resolve.
|
|
95
|
+
executing an old harness indefinitely after you publish a fix. Pinning `@X.Y.Z`
|
|
96
|
+
forces the exact version to resolve.
|
|
81
97
|
|
|
82
98
|
Run `harness doctor` to see which version is actually executing and whether npm
|
|
83
99
|
has a newer one:
|
|
@@ -85,11 +101,11 @@ has a newer one:
|
|
|
85
101
|
```sh
|
|
86
102
|
harness doctor
|
|
87
103
|
# @fusengine/harness doctor
|
|
88
|
-
# running: 0.1.
|
|
104
|
+
# running: 0.1.56
|
|
89
105
|
# package: /Users/you/.bun/install/global/node_modules/@fusengine/harness
|
|
90
106
|
# runtime: /Users/you/.bun/bin/bun
|
|
91
|
-
# npm latest: 0.1.
|
|
92
|
-
# ! stale — npm serves 0.1.
|
|
107
|
+
# npm latest: 0.1.57
|
|
108
|
+
# ! stale — npm serves 0.1.57. Pin "@fusengine/harness@0.1.57" in hooks.json.
|
|
93
109
|
```
|
|
94
110
|
|
|
95
111
|
If `doctor` reports a stale global, clear it and re-pin:
|
|
@@ -100,29 +116,88 @@ bun remove -g @fusengine/harness && bun pm cache rm
|
|
|
100
116
|
|
|
101
117
|
[oven-sh/bun#5791]: https://github.com/oven-sh/bun/issues/5791
|
|
102
118
|
|
|
119
|
+
## Compatibility
|
|
120
|
+
|
|
121
|
+
**There is no "runs the same on any harness."** Each row is a real ceiling, not
|
|
122
|
+
a formatting nuance — read it before assuming a gate that works on Claude Code
|
|
123
|
+
also works elsewhere.
|
|
124
|
+
|
|
125
|
+
| Harness | PreToolUse coverage | Lifecycle (Session/Subagent/Stop/Compact/…) | Known limit |
|
|
126
|
+
|---|---|---|---|
|
|
127
|
+
| **claude-code** | Full: `evaluate` + APEX gates via `handleHook` (`src/adapters/claude/index.ts`) | 14 event types implemented (`dispatch.ts`) — fires once wired into `.claude/settings.json` beyond the `init` default | None found; richer lifecycle needs manual/marketplace wiring (see Quickstart) |
|
|
128
|
+
| **codex** | Bash gated reliably; **`apply_patch` edits are now gated too** — the patch text is parsed per file (`adapters/codex/apply-patch.ts`), each hunk runs the file gates and ONE violating hunk denies the whole patch (`runtime/apply-patch-gate.ts`, sim scenario 22 incl. the multi-file smuggling case). `ask` prompts are **downgraded to explicit deny** (`respond.ts`, sim scenario 23) because Codex fails open on unsupported shapes. | Not wired by `harness init codex` (PreToolUse `Bash\|apply_patch` + PostToolUse only, `src/init/templates.ts:29-38`) | Upstream caveat: Codex itself does not always enforce a correct `apply_patch` deny (openai/codex#27833) — we emit the right verdict; enforcement is theirs. No interactive `ask`. |
|
|
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
|
+
| **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
|
+
| **cline** | `PreToolUse` only; block → `{cancel:true}`, non-block → `contextModification` (`cline/index.ts:24-36`) | none | Same as gemini-cli: stateless guard only. |
|
|
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`). |
|
|
133
|
+
|
|
103
134
|
## What it enforces
|
|
104
135
|
|
|
105
|
-
|
|
136
|
+
Guard/gate chain evaluated before a tool runs (`src/policy/guards/index.ts`,
|
|
137
|
+
`src/policy/apex-gates.ts`, `src/policy/evaluate.ts`):
|
|
106
138
|
|
|
107
139
|
| Guard / gate | Fires on |
|
|
108
140
|
|---|---|
|
|
109
|
-
| file-size (SOLID) | a code file over `FUSE_SOLID_MAX_LINES` (default 100) |
|
|
110
|
-
| git | destructive git (`push --force`, `reset --hard`, …) |
|
|
111
|
-
| bash-write | `python3 -c` / `sed -i` / redirects to code files |
|
|
112
|
-
| install | `npm/pip/brew/...` installs (asks) |
|
|
113
141
|
| security | `rm -rf /`, fork bombs, `curl \| sh`; `sudo` (asks) |
|
|
142
|
+
| protected-path | edits to `.claude/plugins\|logs\|cache`, `.git/`, the harness's own state dirs |
|
|
143
|
+
| bash-write | `python3 -c` / `sed -i` / redirects to code files |
|
|
114
144
|
| interface-separation | top-level interface/type/protocol in a component/controller |
|
|
115
|
-
|
|
|
145
|
+
| install | `npm/pip/brew/...` installs (asks) |
|
|
146
|
+
| git | destructive git (`push --force`, `reset --hard`, …) — block; routine git — ask |
|
|
147
|
+
| file-size (SOLID) | a code file over `FUSE_SOLID_MAX_LINES` (default 100) |
|
|
148
|
+
| APEX brainstorm | creating a new file without brainstorming (when flagged) |
|
|
116
149
|
| APEX freshness | `explore-codebase` + `research-expert` not run within the window |
|
|
117
|
-
| APEX doc-consulted |
|
|
118
|
-
| APEX solid-read | required SOLID refs (auto-discovered, or `FUSE_HARNESS_REFS`) not read |
|
|
119
|
-
| framework sub-skill | framework / shadcn / Tailwind code whose required skill
|
|
150
|
+
| APEX doc-consulted | Context7 **and** Exa not consulted this session (a web-only fallback also passes) |
|
|
151
|
+
| APEX solid-read | required SOLID refs (auto-discovered, or `FUSE_HARNESS_REFS`) not read within the TTL |
|
|
152
|
+
| framework sub-skill | framework / shadcn / Tailwind code whose required skill wasn't read this session |
|
|
120
153
|
| Gemini MCP (opt-in) | hand-written Tailwind UI without a `mcp__gemini-design__*` call — only when `FUSE_ENFORCE_GEMINI_MCP` is set |
|
|
121
|
-
| brainstorm | creating a new file without brainstorming (when flagged) |
|
|
122
154
|
| MCP verbosity / cache | caps exa `numResults`; serves a fresh cached MCP/WebFetch result |
|
|
123
155
|
|
|
124
|
-
A trivial-edit fast path lets a few tiny (< 5-line, non-`replace_all`)
|
|
125
|
-
through per window without the full APEX gates.
|
|
156
|
+
A trivial-edit fast path lets a few tiny (< 5-line, non-`replace_all`) `Edit`s
|
|
157
|
+
through per window without the full APEX gates (`Write` is never trivial).
|
|
158
|
+
|
|
159
|
+
## Beyond gating: memory, receipts, one-shot metric
|
|
160
|
+
|
|
161
|
+
Features shipped since 0.1.44, each with its own test:
|
|
162
|
+
|
|
163
|
+
- **Deny-loop breaker** — an identical retried call that was already denied gets
|
|
164
|
+
a rewritten `[REPEAT] … STOP` message forcing a different approach, instead of
|
|
165
|
+
looping silently (`src/policy/deny-loop.ts`, `test/deny-loop.test.ts`).
|
|
166
|
+
- **Burst-window dedup** — every deployed plugin registers its own hook, so one
|
|
167
|
+
real tool call can fan out to ~11 sibling processes; a same-op record within
|
|
168
|
+
2s is folded into the first instead of re-counted (`src/runtime/burst-window.ts`,
|
|
169
|
+
`test/burst-dedup.test.ts`).
|
|
170
|
+
- **One-shot gate metric** — every gate outcome (deny or its later fix) lands in
|
|
171
|
+
a 7-day sidecar keyed by a content-free op hash, so a deny→allow transition is
|
|
172
|
+
visible (`src/tracking/one-shot.ts`, `test/one-shot.test.ts`).
|
|
173
|
+
- **Verification receipts** — a `tsc`/test run is captured from PostToolUse Bash
|
|
174
|
+
output; `TaskCompleted` **refuses** a "done" over modified code files without a
|
|
175
|
+
fresh passing receipt (`src/tracking/receipts.ts`, `test/receipts.test.ts`).
|
|
176
|
+
- **Decision-time lessons** — a `MEMORY/LESSON.md` bullet tagged with
|
|
177
|
+
`[TRIGGERS tool:… path:… error:… keyword:…]` is injected as `additionalContext`
|
|
178
|
+
the moment a matching call is about to repeat a known mistake, cooldown-guarded
|
|
179
|
+
(`src/policy/lessons/lesson-gate.ts`).
|
|
180
|
+
- **Failure lessons** *(Claude-Code-only — no `PostToolUseFailure` hook on Codex/Hermes,
|
|
181
|
+
`src/runtime/lifecycle/failure-lesson.ts:8-9`)* — a tool failure's error message is
|
|
182
|
+
matched against `error:`-triggered lessons and injected on the spot
|
|
183
|
+
(`test/failure-lesson.test.ts`, `test/sim/scenarios/17-failure-lesson.json`).
|
|
184
|
+
- **TeammateIdle anti-false-done** *(Claude-Code-only, `teammate-idle-check.ts:9-10`)* —
|
|
185
|
+
files a teammate announced as changed are checked against disk; a missing file
|
|
186
|
+
warns the lead before it's treated as done (`test/teammate-idle-check.test.ts`).
|
|
187
|
+
- **PostCompact re-injection** — after context compaction, the reconciliation
|
|
188
|
+
snapshot is re-sent with a "reread files before editing" reminder, deduped per
|
|
189
|
+
session (`src/runtime/lifecycle/post-compact.ts`, `test/post-compact.test.ts`).
|
|
190
|
+
- **Reconciliation snapshot at SessionStart** — git state, running harness
|
|
191
|
+
version + drift vs. npm, `.claude/BOARD.md`, and the one-shot summary, each
|
|
192
|
+
collector isolated so one failure can't blank the rest
|
|
193
|
+
(`src/runtime/lifecycle/snapshot/index.ts`, `test/snapshot.test.ts`).
|
|
194
|
+
- **Injection budget cap** — harness-produced context fragments (lessons,
|
|
195
|
+
snapshot, APEX task context) are capped at ~8000 chars each; owner-authored
|
|
196
|
+
content (CLAUDE.md/rules) is never capped
|
|
197
|
+
(`src/runtime/inject-budget.ts`, `test/inject-budget.test.ts`).
|
|
198
|
+
- **Hook simulator** — 18 end-to-end scenarios (payload in, expected verdict out)
|
|
199
|
+
replayed against the real CLI in both `src` and built `dist` modes in CI
|
|
200
|
+
(`test/sim/README.md`, `test/sim/scenarios/`).
|
|
126
201
|
|
|
127
202
|
### Environment
|
|
128
203
|
|
|
@@ -130,11 +205,15 @@ through per window without the full APEX gates.
|
|
|
130
205
|
|---|---|
|
|
131
206
|
| `FUSE_SOLID_MAX_LINES` | SOLID file-size limit (default `100`). |
|
|
132
207
|
| `FUSE_HARNESS_REFS` | Explicit `path.delimiter`-list of `.md` SOLID-reference dirs → activates `solidReadGate`. Overrides auto-discovery. |
|
|
133
|
-
| `FUSE_HARNESS_MARKETPLACES` | Comma-list of marketplace names whose `solid-*` skill refs are auto-discovered when `FUSE_HARNESS_REFS` is unset (default `fusengine-plugins
|
|
134
|
-
| `FUSE_ENFORCE_TTL_SEC` | APEX freshness window in seconds. |
|
|
135
|
-
| `FUSE_LESSONS_THROTTLE_MIN` | Lessons-injection throttle (
|
|
136
|
-
| `FUSE_ENFORCE_GEMINI_MCP` | **Opt-in (default off).**
|
|
137
|
-
| `
|
|
208
|
+
| `FUSE_HARNESS_MARKETPLACES` | Comma-list of marketplace names whose `solid-*` skill refs are auto-discovered when `FUSE_HARNESS_REFS` is unset (default `fusengine-plugins`). |
|
|
209
|
+
| `FUSE_ENFORCE_TTL_SEC` | APEX freshness window in seconds (default `120`). |
|
|
210
|
+
| `FUSE_LESSONS_THROTTLE_MIN` | Lessons-injection throttle, minutes (default `5`). |
|
|
211
|
+
| `FUSE_ENFORCE_GEMINI_MCP` | **Opt-in (default off).** Blocks hand-written Tailwind UI (`.tsx/.jsx/.vue/.svelte`) until a `mcp__gemini-design__*` call is made this session. Read fresh per call (`src/policy/gemini-mcp-gate.ts`). |
|
|
212
|
+
| `FUSE_DESIGN_GEMINI` | **Opt-in (default off), a *different* gate from the one above.** Enables the design-pipeline's own Gemini gates (`create_frontend` validation + "generate before hand-writing HTML/CSS") — inert unless a design agent is active (`src/policy/design/gates.ts:58-60`, see [docs/design.md](docs/design.md)). |
|
|
213
|
+
| `FUSE_MCP_TTL_SEC` | MCP (Context7/Exa) cache freshness, seconds (default 48h, `src/runtime/mcp-key.ts`). |
|
|
214
|
+
| `FUSE_WEBFETCH_TTL_SEC` | WebFetch cache freshness, seconds (default 24h — pages stale faster than docs). |
|
|
215
|
+
| `RALPH_MODE` | **Opt-in (default off).** Exempts safe git commands (`add`/`commit`/`checkout -b`/`status`/`diff`/`log`) from the confirmation ask and auto-approves project installs. Destructive git (force-push, `reset --hard`) and system installs still gate. |
|
|
216
|
+
| `CLAUDE_PROJECT_DIR` | Overrides the project root used to hash the out-of-tree state dir (`src/runtime/paths.ts:20`). |
|
|
138
217
|
|
|
139
218
|
## Library usage
|
|
140
219
|
|
|
@@ -155,7 +234,8 @@ const prompt = await gate({ sessionId, framework: "react", tool: "Write",
|
|
|
155
234
|
```
|
|
156
235
|
|
|
157
236
|
The `Prompt` it returns (`{ kind: "block" | "ask" | "inform", title, reason, actions? }`)
|
|
158
|
-
is portable; each adapter maps it to the harness's native shape
|
|
237
|
+
is portable; each adapter maps it to the harness's native shape — but, per the
|
|
238
|
+
compatibility matrix above, not every harness can act on every `kind`.
|
|
159
239
|
|
|
160
240
|
### Extend it
|
|
161
241
|
|
|
@@ -177,15 +257,15 @@ registerGuard(({ tool, command }) =>
|
|
|
177
257
|
| Subpath | What |
|
|
178
258
|
|---------|------|
|
|
179
259
|
| `./detect` | `detectHarness()` / `detectMode()` — 13 harnesses, `hook` vs `cli`. |
|
|
180
|
-
| `./policy` | `evaluate(ctx)`, the
|
|
260
|
+
| `./policy` | `evaluate(ctx)`, the guard chain, `evaluateApex`, framework detection. |
|
|
181
261
|
| `./runtime` | `handleHook`, `gate`, `recordActivity`, `activityFor`, per-harness storage + MCP intercept. |
|
|
182
|
-
| `./tracking` | Session track: `recordAgent/Doc/RefRead`, `agentsFresh`,
|
|
262
|
+
| `./tracking` | Session track: `recordAgent/Doc/RefRead`, `agentsFresh`, receipts, one-shot metric. |
|
|
183
263
|
| `./refs` | Frontmatter parse, `loadRefs(dir)`, SOLID ref scoring/routing. |
|
|
184
264
|
| `./prompt` | The portable `Prompt` type + `formatPrompt`. |
|
|
185
265
|
| `./cache` | MCP/WebFetch cache: key, lookup/store, compaction, response extraction. |
|
|
186
266
|
| `./memory` | Per-project "never reproduce" lessons. |
|
|
187
267
|
| `./config` `./util` `./state` `./statusline` `./freshness` `./init` `./cli` | env config, project-root, locks, statusline, doc-freshness, wiring templates, staged checks. |
|
|
188
|
-
| `./adapters/{claude,codex,cursor,cline,gemini}` | Thin per-harness adapters. |
|
|
268
|
+
| `./adapters/{claude,codex,cursor,cline,gemini,hermes}` | Thin per-harness adapters — see [Compatibility](#compatibility) for what each can actually enforce. |
|
|
189
269
|
|
|
190
270
|
## Documentation
|
|
191
271
|
|
|
@@ -193,20 +273,40 @@ registerGuard(({ tool, command }) =>
|
|
|
193
273
|
|-------|------|
|
|
194
274
|
| [docs/index.md](https://github.com/fusengine/harness/blob/main/docs/index.md) | architecture overview + map |
|
|
195
275
|
| [docs/detect.md](https://github.com/fusengine/harness/blob/main/docs/detect.md) | harness detection (`hook` vs `cli`) |
|
|
196
|
-
| [docs/policy.md](https://github.com/fusengine/harness/blob/main/docs/policy.md) | `evaluate`,
|
|
276
|
+
| [docs/policy.md](https://github.com/fusengine/harness/blob/main/docs/policy.md) | `evaluate`, the guard chain, framework, APEX gates |
|
|
197
277
|
| [docs/guards.md](https://github.com/fusengine/harness/blob/main/docs/guards.md) | the guard chain, `registerGuard`, fail-closed |
|
|
198
|
-
| [docs/runtime.md](https://github.com/fusengine/harness/blob/main/docs/runtime.md) | `handleHook`, `gate`, tracking, MCP intercept |
|
|
199
|
-
| [docs/config.md](https://github.com/fusengine/harness/blob/main/docs/config.md) | env config (TTL, max-lines, refs dir) |
|
|
278
|
+
| [docs/runtime.md](https://github.com/fusengine/harness/blob/main/docs/runtime.md) | `handleHook`, `gate`, tracking, MCP intercept, state paths |
|
|
279
|
+
| [docs/config.md](https://github.com/fusengine/harness/blob/main/docs/config.md) | env config (TTL, max-lines, refs dir, Gemini opt-ins) |
|
|
200
280
|
| [docs/modules.md](https://github.com/fusengine/harness/blob/main/docs/modules.md) | cache · refs · state · memory · statusline · util |
|
|
201
|
-
| [docs/adapters.md](https://github.com/fusengine/harness/blob/main/docs/adapters.md) | adapters
|
|
281
|
+
| [docs/adapters.md](https://github.com/fusengine/harness/blob/main/docs/adapters.md) | adapters, compatibility, `harness init`/`hook` wiring |
|
|
282
|
+
| [docs/design.md](https://github.com/fusengine/harness/blob/main/docs/design.md) | design-agent pipeline — state machine, gates, opt-in Gemini |
|
|
202
283
|
| [CHANGELOG.md](https://github.com/fusengine/harness/blob/main/CHANGELOG.md) | release history |
|
|
203
284
|
|
|
204
285
|
Run `bun run docs:api` for the generated typedoc API reference.
|
|
205
286
|
|
|
287
|
+
## Known limitations
|
|
288
|
+
|
|
289
|
+
- **Codex file edits are not gated.** The SOLID/file-size gate keys off
|
|
290
|
+
`tool_input.file_path`, which Codex's `apply_patch` call never supplies — only
|
|
291
|
+
Bash is reliably gated on Codex today (see [Compatibility](#compatibility)).
|
|
292
|
+
- **Cursor file edits are advisory-only.** `afterFileEdit` fires after the edit
|
|
293
|
+
already happened — a platform limit, not something this harness can work around.
|
|
294
|
+
- **Hook fan-out is mitigated, not eliminated.** The ~11-sibling-plugin burst is
|
|
295
|
+
deduped within a 2s window (`BURST_DEDUP_MS`), but that window is a heuristic,
|
|
296
|
+
not a protocol guarantee — an unusually slow fan-out could in theory land
|
|
297
|
+
outside it.
|
|
298
|
+
- **Sidechain hook reliability is a platform issue, worked around, not fixed.**
|
|
299
|
+
Sub-agent `PostToolUse` hooks don't always fire on Claude Code (documented
|
|
300
|
+
platform issues #43612/#27655/#34692); `SubagentStop` transcript harvesting
|
|
301
|
+
(`src/freshness/evidence-harvest-io.ts`) compensates, but only at that
|
|
302
|
+
checkpoint, not continuously.
|
|
303
|
+
- **Hermes coverage beyond `pre_tool_call` is unverified** — no lifecycle events
|
|
304
|
+
have been proven against a live Hermes install in this repo.
|
|
305
|
+
|
|
206
306
|
## Develop
|
|
207
307
|
|
|
208
308
|
```sh
|
|
209
|
-
bun test #
|
|
309
|
+
bun test # 484 tests (94 files)
|
|
210
310
|
bunx tsc --noEmit # typecheck (isolatedDeclarations)
|
|
211
311
|
bun run build # dist + .d.mts via tsdown (for Node/bundler consumers)
|
|
212
312
|
bun run docs:api # generate the typedoc API reference
|
|
@@ -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 {
|
|
7
|
+
import { J as runningVersion, Lt as todayUtc, Y as versionBanner, q as runDoctor, t as handleHook } from "../handle-BdOh1jPG.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 };
|