chisle 3.6.0 → 3.7.0

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.
@@ -0,0 +1,49 @@
1
+ ---
2
+ trigger: always_on
3
+ description: Chisle, zero-fluff prose + YAGNI-first code
4
+ ---
5
+
6
+ # Chisle
7
+
8
+ Chisle: maximum-efficiency dev mode. Two compressions, always active together.
9
+
10
+ **Prose:** Default to fragments. Drop articles, filler (just/really/basically/
11
+ actually), pleasantries (sure/certainly/happy to), hedging, linking verbs where
12
+ meaning survives. Causality as arrows (X → Y). Technical terms, code, API names,
13
+ errors: exact, verbatim. Terse ≠ incomplete: keep every decisive fact (the fix,
14
+ the gotcha, the why); cut the words around them, never the facts. Structure is
15
+ tokens, so answer at the question's altitude; no manufactured headings, bullet
16
+ lists, numbered steps, or sections the question didn't ask for. Two tight
17
+ paragraphs beat five headed sections. "Summarize/compare X vs Y" is
18
+ the trap: skip headed pro/con bullet walls, name the decisive tradeoffs in
19
+ prose, give the verdict, stop.
20
+
21
+ **Code: the efficiency ladder.** Stop at the first rung that holds:
22
+ 1. Does this need to exist at all? (YAGNI)
23
+ 2. Already in this codebase? Reuse it.
24
+ 3. Stdlib does it? Use it.
25
+ 4. Native platform feature covers it?
26
+ 5. Already-installed dependency solves it?
27
+ 6. Can it be one line?
28
+ 7. Only then: the minimum code that works.
29
+
30
+ No unrequested abstractions. Deletion over addition. Shortest diff wins, after
31
+ you understand the problem and never instead of it.
32
+
33
+ **Thinking is billed too.** Reasoning tokens cost the same as written ones. The
34
+ ladder is a stopping rule, not a checklist to walk aloud: stop at the first rung
35
+ that holds, don't re-derive the rungs above it or weigh alternatives already
36
+ excluded. Obvious fix → give it; a one-line change gets no design review. Never
37
+ think less about *understanding* the problem: root-cause bugs, read what you
38
+ edit. Depth where the problem is actually hard, nowhere else.
39
+
40
+ **Context diet:** tool output you pull in is billed on every later turn. Grep
41
+ for the symbol first; read only the matching region, not the whole file. Narrow
42
+ at the source (`ls dir` not `ls -R`, pipe long output through `tail`/`grep`).
43
+ Never re-read what's already in context unless it changed. Never skim what
44
+ you're about to edit: diet trims transport, not understanding.
45
+
46
+ **Never minimal about:** input validation at trust boundaries, error handling
47
+ that prevents data loss, security, accessibility, anything explicitly requested.
48
+
49
+ Always on once installed. Deactivate: "stop chisle" / "normal mode".
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chisle",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "description": "Maximum-efficiency dev mode. Zero-fluff prose combined with YAGNI/ladder-first code decisions. One persona: the senior dev who deletes code for fun and bills by the syllable.",
5
5
  "hooks": {
6
6
  "SessionStart": [
@@ -22,7 +22,7 @@
22
22
  // Tunables (shared with the Claude/Pi/Copilot compressor, see chisle-config):
23
23
  // CHISLE_COMPRESS=0 kill switch
24
24
  // CHISLE_DEFAULT_MODE=off disable by default (env or ~/.config/chisle)
25
- // CHISLE_COMPRESS_MAX_CHARS outputs at/under this size are not elided
25
+ // CHISLE_COMPRESS_MAX_CHARS elision threshold and final OpenCode bound
26
26
  // CHISLE_COMPRESS_TOOLS=bash,… override the tool allowlist
27
27
 
28
28
  import { createRequire } from 'module';
@@ -52,12 +52,16 @@ const config = loadFrom([
52
52
  ]) || { getDefaultMode: () => 'on' };
53
53
 
54
54
  const compressForOpencode = core && core.compressForOpencode;
55
+ const recordSavings = core && core.recordSavings;
55
56
  const getDefaultMode = config.getDefaultMode || (() => 'on');
57
+ const getOpencodeDir = config.getOpencodeDir || (() => path.dirname(__dirname));
56
58
 
57
59
  // Already compressed: our elision marker. Skip such text so a second pass
58
60
  // (messages.transform after tool.execute.after already ran) never nests a
59
61
  // marker or eats an earlier salvage block. Keeps compression idempotent.
60
62
  const CHISLE_MARK = '[chisle:';
63
+ const getOpencodeMaxChars = () =>
64
+ (core && core.limitsFor && core.limitsFor().maxChars) || 8000;
61
65
 
62
66
  export default async () => {
63
67
  return {
@@ -75,10 +79,19 @@ export default async () => {
75
79
  // MCP tools can hand back a content[] array rather than a string;
76
80
  // built-ins give a string. Only the string case is ours to touch.
77
81
  if (!output || typeof output.output !== 'string') return;
78
- if (output.output.includes(CHISLE_MARK)) return;
79
- const updated = compressForOpencode(input && input.tool, output.output, { mode: getDefaultMode() });
80
- if (updated != null && updated.length < output.output.length) {
82
+ if (output.output.includes(CHISLE_MARK) && output.output.length <= getOpencodeMaxChars()) return;
83
+ const original = output.output;
84
+ const updated = compressForOpencode(input && input.tool, original, {
85
+ mode: getDefaultMode(),
86
+ sessionId: input && input.sessionID,
87
+ callId: input && input.callID,
88
+ });
89
+ if (updated != null && updated.length < original.length) {
81
90
  output.output = updated;
91
+ if (recordSavings) {
92
+ recordSavings(original.length - updated.length,
93
+ process.env.CHISLE_STATE_DIR || getOpencodeDir());
94
+ }
82
95
  }
83
96
  } catch (e) { /* keep the original output */ }
84
97
  },
@@ -102,7 +115,7 @@ export default async () => {
102
115
  if (!part || part.type !== 'tool') continue;
103
116
  const st = part.state;
104
117
  if (!st || st.status !== 'completed' || typeof st.output !== 'string') continue;
105
- if (st.output.includes(CHISLE_MARK)) continue;
118
+ if (st.output.includes(CHISLE_MARK) && st.output.length <= getOpencodeMaxChars()) continue;
106
119
  const updated = compressForOpencode(part.tool, st.output, { mode });
107
120
  if (updated != null && updated.length < st.output.length) {
108
121
  st.output = updated;
package/AGENTS.md CHANGED
@@ -4,7 +4,7 @@ Maximum-efficiency dev mode. Zero-fluff prose + YAGNI-first code, always active
4
4
 
5
5
  This file is the agent-agnostic instruction set (the `AGENTS.md` convention used
6
6
  by Codex, Amp, and others). The same content is mirrored per-agent under
7
- `.cursor/`, `.windsurf/`, `.clinerules/`, `.kiro/`, and `.github/copilot-instructions.md`.
7
+ `.cursor/`, `.windsurf/`, `.clinerules/`, `.kiro/`, `.agents/rules/` (Antigravity), and `.github/copilot-instructions.md`.
8
8
  Source of truth: [`skills/chisle/SKILL.md`](./skills/chisle/SKILL.md).
9
9
 
10
10
  ## Prose: zero fluff
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  <p align="center">
16
16
  <a href="https://www.npmjs.com/package/chisle"><img src="https://img.shields.io/npm/v/chisle?style=flat-square&color=d78a3c" alt="npm version"></a>
17
- <img src="https://img.shields.io/badge/works%20with-11%20agents-d78a3c?style=flat-square" alt="Works with 11 agents">
17
+ <img src="https://img.shields.io/badge/works%20with-12%20agents-d78a3c?style=flat-square" alt="Works with 12 agents">
18
18
  <a href="https://github.com/JayPokale/Chisle/actions/workflows/test.yml"><img src="https://img.shields.io/github/actions/workflow/status/JayPokale/Chisle/test.yml?style=flat-square&label=CI" alt="CI"></a>
19
19
  <img src="https://img.shields.io/badge/deps-0-2da44e?style=flat-square" alt="Zero deps">
20
20
  <img src="https://img.shields.io/badge/license-MIT-d78a3c?style=flat-square" alt="MIT">
@@ -22,13 +22,17 @@
22
22
  </p>
23
23
 
24
24
  <p align="center">
25
- <strong>83% of a bare model's output, where caveman and ponytail land above 100% &middot; 11 agents &middot; zero dependencies &middot; one command</strong>
25
+ <strong>On Pi + GPT-5.5, answers 37% the length of the same model with no ruleset (caveman 56%, ponytail 49%) &middot; 12 agents &middot; zero dependencies &middot; one command</strong>
26
26
  </p>
27
27
 
28
+ Chisle is a ruleset and hook pack that makes AI coding agents cheaper to run. It cuts what the model writes (no filler, no hedging, no speculative abstractions: the smallest code that works) and what it reads (oversized tool output is trimmed before it re-enters the context window). One `npx chisle` wires it into Claude Code, Pi, Cursor, Codex, Gemini, Copilot, OpenCode, Antigravity and four more agents. Every number below comes from committed raw transcripts, including the runs where it lost.
29
+
28
30
  <p align="center">
29
- <a href="https://chisle.jaypokale.me"><strong>chisle.jaypokale.me</strong></a> is the site version of this README, with fewer words
31
+ <img src="assets/benchmark-pi.svg" width="820" alt="Pi benchmark, 6 tasks on GPT-5.5, as percent of the same model with no ruleset. Answer length: caveman 56%, ponytail 49%, Chisle 37%. Billed tokens: caveman 72%, ponytail 59%, Chisle 61%.">
30
32
  </p>
31
33
 
34
+ <p align="center"><sub>Pi 0.85.1 + GPT-5.5, 6 tasks, one run. Answer length is what you read; on total billed tokens ponytail is leanest (59% vs 61%). On the larger 26-cell Claude Code run Chisle is the only one under 100%: <a href="#benchmarks">see below</a>.</sub></p>
35
+
32
36
  ---
33
37
 
34
38
  "Add debounce to a search input that currently fires an API call on every keystroke." Same model, same prompt, one difference: the injected ruleset. Both answers below are the **verbatim committed output** from [`benchmarks/results/raw/`](benchmarks/results/raw/):
@@ -73,7 +77,7 @@ Then two lines on why it works, and *"use `lodash.debounce` if already installed
73
77
 
74
78
  Not golfed, **boring**. Same behaviour, one less abstraction, no second file, and it names the dependency you might already have instead of reinventing it.
75
79
 
76
- Most efficiency tools compress one thing. Chisle compresses **three**:
80
+ ## What it compresses
77
81
 
78
82
  | axis | what | how |
79
83
  |---|---|---|
@@ -112,17 +116,11 @@ flowchart LR
112
116
 
113
117
  The loop on the right is the input axis: tool output is billed again on *every* later request in the session, so shrinking it once pays repeatedly. `Read`, `Edit`, and `Write` are deliberately outside it.
114
118
 
115
- <p align="center">
116
- <img src="assets/thumbnail.png" width="820" alt="Chisle: less tokens, same results. Four parts: output compressor (compress noisy tool outputs), context diet (read only what's actually relevant), terse persona (short, focused, YAGNI-first), YAGNI ladder (do less, reuse more, build only when needed).">
117
- </p>
118
-
119
- Every "be concise" tool has a worst day, the day it makes the model write *more* than no tool at all. Across 20 measured tasks over two suites, the specialists had that day **6** and **8** times, blowing up to **424%** of the baseline. Chisle had it **once**, capped at 173%, and that one failure was root-caused, fixed in the ruleset, and re-validated live at 93%, with the whole investigation [committed to the repo](benchmarks/results/2026-07-07-verify-rerun.md). Think of it as downside insurance for your token bill: not always the single cheapest answer, always the smallest worst case, from the only tool in this class that publishes its own failures. [Why not caveman or ponytail? →](docs/comparison.md)
120
-
121
119
  ---
122
120
 
123
121
  ## Install
124
122
 
125
- One command. Auto-detects your agents (Claude Code, Pi, Cursor, Windsurf, Cline, Kiro, Codex, Gemini, Copilot, OpenCode, Hermes) and wires each one. `--uninstall` puts everything back.
123
+ One command. Auto-detects your agents (Claude Code, Pi, Cursor, Windsurf, Cline, Kiro, Antigravity, Codex, Gemini, Copilot, OpenCode, Hermes) and wires each one. `--uninstall` puts everything back.
126
124
 
127
125
  ```bash
128
126
  npx chisle
@@ -140,103 +138,17 @@ irm https://raw.githubusercontent.com/JayPokale/Chisle/main/install.ps1 | iex
140
138
 
141
139
  Preview first with `npx chisle --dry-run`, scope with `--only claude` or `--only pi`, see everything with `npx chisle --help`. Remove with `npx chisle --uninstall`.
142
140
 
143
- **Requirements:** Node ≥18 (installer / `npx`) · Claude Code or Pi for `/chisle` toggling and input-side compression. The always-on ruleset still ships to every other agent.
144
-
145
- ### Upgrading
146
-
147
- ```bash
148
- npx chisle@latest --update
149
- ```
150
-
151
- That refreshes every agent that already has Chisle and installs it into none that don't. Two details it exists to handle:
152
-
153
- - **Plain `npx chisle` does not upgrade.** Every install path skips what is already present, so an upgrade run reports success and changes nothing. `--update` pairs the refresh with that check.
154
- - **`@latest` matters.** `npx chisle` can serve a cached copy of the package from a previous run, so the pin is what guarantees you get the new one.
155
-
156
- Update one agent at a time with `--only`:
157
-
158
- ```bash
159
- npx chisle@latest --update --only claude # or: claude plugin update chisle@chisle
160
- npx chisle@latest --update --only pi # or: pi install npm:chisle
161
- npx chisle@latest --update --only gemini # or: gemini extensions install https://github.com/JayPokale/Chisle
162
- npx chisle@latest --update --only codex
163
- npx chisle@latest --update --only opencode # ruleset + skills + the compression plugin
164
- npx chisle@latest --update --only hermes
165
- ```
166
-
167
- Cursor, Windsurf, Cline, Kiro and Copilot keep their rule file **inside the repo**, because that is how those agents load rules:
168
-
169
- ```bash
170
- cd ~/code/my-project
171
- npx chisle@latest --update --only cursor
172
- ```
173
-
174
- `--update` looks for that file in the current directory, so running it from `~` reports "Nothing to update" even when the project is set up correctly. Run it once per repo that has one. Full table, including what each agent's update actually refreshes: [INSTALL.md](./INSTALL.md#per-agent).
175
-
176
- Not sure what you are running? `npx chisle --list` prints the agents it detects, and `claude plugin list` shows the installed plugin and its version.
177
-
178
- Chisle checks npm on session start and mentions it once when a **major** version is out (cached 3 days, `CHISLE_UPDATE_CHECK=0` to silence). Minor and patch releases stay quiet on purpose.
179
-
180
- Upgrading to 3.0.0 from 2.x needs nothing: a `lite`/`full`/`ultra` value in `CHISLE_DEFAULT_MODE` or `config.json` is no longer meaningful, falls through to the default, and Chisle stays active. It says so once so the setting is not ignored silently. Replace it with `on`/`off` or delete it.
181
-
182
- ### Claude Code plugin (marketplace)
183
-
184
- ```bash
185
- claude plugin marketplace add JayPokale/Chisle # register the marketplace
186
- claude plugin install chisle@chisle # enable the plugin
187
- ```
188
-
189
- ### Pi package
190
-
191
- ```bash
192
- pi install npm:chisle
193
- ```
194
-
195
- The package loads the zero-dependency extension and `chisle` skill globally. Pi extensions run with your user permissions; review the source before installation. Project-local installs (`pi install -l npm:chisle`) load only after you trust that project.
196
-
197
- ### Where the savings show up
198
-
199
- Two places, both measured rather than estimated:
200
-
201
- ```bash
202
- npx chisle --stats # what the compressor has saved, cumulative
203
- ```
204
-
205
- ```text
206
- chisle: tool-output savings
207
-
208
- saved: 88,967 chars (~22,241 tokens)
209
- outputs: 18 compressed, 4,943 chars each on average
210
- ```
211
-
212
- In Claude Code the statusline badge carries the same number live: `[CHISLE] ⇣22k tok`. Pi shows it in the footer for the current session.
213
-
214
- This is the **input axis only**, and deliberately so. Chars elided have a real baseline, since the hook knows exactly what it cut. The output axis has none: there is no way to know what the model would have written without the ruleset, which is why that half is measured with A/B benchmark arms instead of a counter. A number that blended the two would be inventing the interesting half.
215
-
216
- ### See what it would do, before it does it
217
-
218
- ```bash
219
- npx chisle --dry-run # prints every file it would touch, changes nothing
220
- npx chisle --stats # prints what it has saved, changes nothing
221
- ```
222
-
223
- Want the savings measured on your own work rather than ours? Clone the repo and replay the compressor over local transcripts. It reads them locally, writes nothing, and reports the input the hook would have stripped:
224
-
225
- ```bash
226
- git clone https://github.com/JayPokale/Chisle && cd Chisle
227
- node benchmarks/replay-compress.js # Claude Code
228
- node benchmarks/replay-compress.js pi # Pi; marginal over Pi's native truncation
229
- ```
141
+ Upgrading, per-agent setup, `--stats` and config: [docs/usage.md](docs/usage.md).
230
142
 
231
143
  ---
232
144
 
233
- ## Numbers
145
+ ## Benchmarks
234
146
 
235
- Nothing here is estimated. Every figure below is recomputed from committed raw data; the 2026-07-07 [verification writeup](benchmarks/results/2026-07-07-verify-rerun.md) re-derived the old claims from scratch, re-ran the whole suite against the competitors' **installed plugins**, and retired the one claim that didn't survive.
147
+ Claude Code 2.1.285 on Haiku 4.5, 13 live prompts × 2 seeds = 26 cells per arm, billed output tokens vs the same model with no ruleset ([writeup + raw cells](benchmarks/results/2026-10-01-live-rerun.md)):
236
148
 
237
- ### Output axis, re-measured 2026-10-01 (current)
238
-
239
- Re-run with the shipped ruleset, Claude Code 2.1.285 and the rivals' current plugins (caveman `ef6050c5e184`, ponytail 4.8.3): 13 live prompts × 2 seeds = 26 cells per arm on Haiku 4.5, billed output tokens vs the no-tool baseline ([writeup + raw cells](benchmarks/results/2026-10-01-live-rerun.md)):
149
+ <p align="center">
150
+ <img src="assets/benchmark-rerun.svg" width="820" alt="2026-10-01 live rerun: total billed output as percent of the bare model. caveman 102%, ponytail 105%, Chisle 83%.">
151
+ </p>
240
152
 
241
153
  | | total bill | 95% CI | visible answer | worst cell | backfires |
242
154
  |---|--:|--:|--:|--:|--:|
@@ -244,99 +156,9 @@ Re-run with the shipped ruleset, Claude Code 2.1.285 and the rivals' current plu
244
156
  | ponytail | 105% | 86–129% | 97% | 493% | 16 / 26 |
245
157
  | **Chisle** | **83%** | **69–95%** | **76%** | **170%** | **11 / 26** |
246
158
 
247
- Chisle is the only arm measurably below a bare model, by about a third of what the June table below claimed. It pays on long answers (77%) and coding prompts (76%); on short answers it breaks even (106%), on explanation prompts ponytail is leaner (86% vs 91%), and reasoning tokens are not cut (107%). Single cells swing hard between seeds (57% → 135% on one prompt), and the bare model "backfires" against itself on 13 of 26 cells, so read totals, not backfire counts ([control](benchmarks/results/2026-10-01-backfires.md)).
248
-
249
- **Why the June headline was retired.** Its 52% leaned on one cell: the June bare `cache` answer billed 4,910 tokens, while every later run of the same prompt billed 375–813. The ruleset also carried examples matching `cache` (from June 18) and `auth-bug` (from June 27), so four cells may have been primed; without them the June total was 70%. The tables below stay as what they were.
250
-
251
- ### Output axis, June–July 2026 (superseded): vs caveman & ponytail, 20 live tasks
252
-
253
- **What this suite measures, and what it does not.** Every figure below is *billed output tokens on single-turn prompts with no tools available*. That isolates the ruleset's effect on how the model writes, which is what it was built to measure. It is not whole-session cost: a real agentic session is dominated by tool output and cached input, so a tool can win here and still fail to pay for itself end to end. The input axis below is measured separately, and against its own baseline.
254
-
255
- **59+ live model runs** across two suites (June 4-arm matrix on Haiku + Sonnet sweep; July re-verification run). Arms differ only in the injected system prompt. Billed output tokens vs the no-tool baseline:
256
-
257
- | | total bill (all 20 tasks) | average task | worst case | backfires |
258
- |---|--:|--:|--:|--:|
259
- | caveman | 80% | 98% | **424%** | 6 / 20 |
260
- | ponytail | 68% | 91% | 227% | 8 / 20 |
261
- | **Chisle** | **52%** | **69%** | **173%** | **1 / 20** |
262
-
263
- Chisle wins all four columns: it cut the total 20-task bill **nearly in half** while the specialists managed 20–32%, and it did so with the smallest worst day and a twentieth the backfire rate.
264
-
265
- <p align="center">
266
- <img src="assets/benchmark.svg" width="820" alt="Total billed output across 20 tasks as percent of the no-tool baseline. caveman 80% (worst day 424%, backfired 6), ponytail 68% (worst day 227%, backfired 8), Chisle 52% (worst day 173%, backfired 1, root-caused and fixed).">
267
- </p>
268
-
269
- The bar is the whole 20-task bill; the badge is each tool's worst single day. caveman's worst day cost **4.2×** a bare model; ponytail's, a tool whose entire job is writing less, **2.3×**. Chisle's worst day was 1.7×, it happened once, and the fix is measured and merged.
270
-
271
- In the July run all 24 answers, every arm, **graded correct**: nobody here buys token savings with wrong answers.
272
-
273
- #### Code vs. explanation
274
-
275
- Across all 20 cells, split by what the prompt actually asks for:
276
-
277
- | | n | caveman | ponytail | **Chisle** |
278
- |---|--:|--:|--:|--:|
279
- | **coding** (wants working code) | 12 | 74% | 59% | **44%** |
280
- | **non-coding** (wants an explanation) | 8 | 103% | 104% | **87%** |
281
-
282
- Code is where the YAGNI ladder has something to bite on: an abstraction to skip, a stdlib call to reach for, a file not to create. Chisle bills **44%** of a bare model there, a third less than ponytail, which is the closest thing to a dedicated lazy-code tool.
283
-
284
- On explanation-only prompts the picture is worse for everyone. Both specialists land **above 100%**: a tool whose job is writing less made the model write *more* than using nothing at all. Chisle is the only arm that stays under water (87%), which is a smaller win than the coding number and worth saying plainly.
285
-
286
- This does revise a claim the earlier Sonnet writeup made. On that suite's three prose prompts caveman was leaner (44% vs 52%), and that still holds *for those cells*. Pooled across all eight non-coding cells it does not: caveman is at 103%. The prose win was suite-specific, not general.
287
-
288
- #### Task by task
289
-
290
- Averages hide the interesting part, so here is every cell of the June suite, same six prompts, every arm, no cherry-picking:
291
-
292
- <p align="center">
293
- <img src="assets/per-task.svg" width="820" alt="Billed output per task as a percent of the no-tool baseline across the six-task June suite: Chisle is leanest on five of six, caveman wins the cache task at 8% versus Chisle's 12%.">
294
- </p>
295
-
296
- Chisle is leanest on **5 of 6**. caveman takes `cache` (8% vs our 12%) by answering in prose where we still emit working code, which is the trade you would want on a task that asked for code. Note the two prose rows where ponytail lands **above** 100%: a tool built to write less made the model write *more* than using no tool at all. That is the failure mode the worst-case column above is really about.
297
-
298
- #### The headline average is hiding the good part
299
-
300
- Split the same 20 cells at their median baseline, short answers below and long answers above, and the tools separate sharply:
301
-
302
- <p align="center">
303
- <img src="assets/by-size.svg" width="820" alt="Total billed output split by answer size. On short answers caveman and Chisle are level at about 84% of baseline and ponytail is above 100%. On long answers Chisle drops to about 45% while caveman is 79% and ponytail 59%.">
304
- </p>
305
-
306
- On **short** answers Chisle and caveman are level (84% each), because there is not much to cut in a three-line reply, and the ruleset overhead is proportionally at its worst. On **long** answers Chisle drops to **45%** while caveman only reaches 79%. The 52% headline is the blend of the two, so it understates the case where it matters and overstates the case where it doesn't.
307
-
308
- The effect is not driven by one lucky cell. Dropping the `cache` outlier (the row where the baseline invented 150 lines against a codebase it never saw) *widens* the gap on long answers: caveman degrades to **116%**, worse than using no tool, while Chisle holds at **65%**.
309
-
310
- Two honest limits. Per-task rank correlation between baseline size and leanness is weak (Spearman ρ = −0.15), so this is a difference between aggregate bills, not a tidy per-task law. With n=10 a side, treat it as a strong signal rather than a settled result. And much of the widening gap comes from the specialists getting *worse* on long answers, not only from Chisle getting better.
311
-
312
- #### Size or kind? Both, and they're tangled
313
-
314
- Coding prompts average ~1129 baseline tokens against ~393 for explanation prompts, so "long" and "code" largely describe the same cells. Crossing the two separates them as far as 20 tasks allow:
315
-
316
- | | n | caveman | ponytail | **Chisle** |
317
- |---|--:|--:|--:|--:|
318
- | coding · short | 5 | **62%** | 116% | 70% |
319
- | coding · long | 7 | 76% | 52% | **41%** |
320
- | non-coding · short | 5 | 104% | 98% | **96%** |
321
- | non-coding · long | 3 | 103% | 111% | **77%** |
322
-
323
- <p align="center">
324
- <img src="assets/by-kind.svg" width="820" alt="Billed output by task kind crossed with answer size. Code and short: caveman 62%, ponytail 116%, Chisle 70%. Code and long: caveman 76%, ponytail 52%, Chisle 41%. Explanation and short: caveman 104%, ponytail 98%, Chisle 96%. Explanation and long: caveman 103%, ponytail 111%, Chisle 77%.">
325
- </p>
326
-
327
- Size matters *within* each kind: coding goes 70% → 41% and non-coding 96% → 77%, so it isn't merely code in disguise. But the cells are thin, and the non-coding "long" bucket spans only 522–542 tokens, which is barely long at all.
328
-
329
- The one row Chisle loses is **short coding**, where caveman takes it 62% to 70%. That is the honest shape of it: on a small code question there is little to skip, and the ruleset costs more than the ladder saves. The tool earns its keep on the long ones.
330
-
331
- `SUITE=large` exists to fill the thin cells, see [below](#see-what-it-would-do-before-it-does-it).
332
-
333
- These prompts were never designed to test this, which is the real caveat. To probe it directly:
159
+ Chisle is the only arm below a bare model. It pays on long answers (77%) and coding prompts (76%); on short answers it breaks even (106%).
334
160
 
335
- ```bash
336
- SUITE=large bash benchmarks/run-live.sh <model> benchmarks/results/raw-large
337
- ```
338
-
339
- #### Where each tool actually helps
161
+ **Input side:** tool output is 67.5% of context in 171 measured Claude Code sessions, and it is re-billed on every later request. The compressor cut ~46% off every eligible output there, and 27.4% of tool output on top of Pi's own truncation ([receipts](benchmarks/results/2026-07-07-input-axis.md)).
340
162
 
341
163
  | | prose | code judgment | input/context | worst-case guard | publishes failures |
342
164
  |---|:---:|:---:|:---:|:---:|:---:|
@@ -345,75 +167,7 @@ SUITE=large bash benchmarks/run-live.sh <model> benchmarks/results/raw-large
345
167
  | headroom | ❌ | ❌ | ✅ proxy | n/a | ❌ |
346
168
  | **Chisle** | ✅ | ✅ | ✅ hook | **170%** | ✅ |
347
169
 
348
- The row that matters is the last one. Every tool here looks good on its best day; the numbers above are the only ones in this class published alongside the run that went wrong. [Full comparison →](docs/comparison.md)
349
-
350
- ### Input axis: tool-output compression (Claude Code + Pi)
351
-
352
- Measured over 171 real sessions ([receipts](benchmarks/results/2026-07-07-input-axis.md)): tool output is **67.5%** of context content, and every byte of it is re-billed on *every subsequent request* in the session (median: 171 requests). A `PostToolUse` hook shrinks it before the model reads it: deterministic, zero LLM, zero network:
353
-
354
- | tier | what it does | loss |
355
- |---|---|---|
356
- | **scrub** | strips ANSI escapes, collapses blank runs and `line repeated N×` | none |
357
- | **elide** | oversized output → head + tail, error-like lines salvaged from the cut | bounded, guarded |
358
- | **dedup** | byte-identical repeat of a tool's previous output (same session) → one-line marker | none, the copy is already in context |
359
-
360
- Replayed over the same 171 Claude Code sessions: **~61k tokens** saved one-shot, ~46% off every eligible output: a floor, not an estimate, since each saved byte also stops being re-sent on every later request. Correctness rules: allowlist only (`Bash`, `Agent`, `WebFetch`, `WebSearch`, `Grep`, `Glob`, `mcp__*`), never `Read`/`Edit`, whose exact bytes feed later edits. Successful calls only: Claude Code sends a failed call (a non-zero `Bash` exit included) to `PostToolUseFailure`, whose output a hook can add to but not replace (checked on 2.1.285), so a failing test run reaches the model through Claude Code's own ~10k-char head + tail truncation instead; the replay counts those as unreachable. Honest ledger: dedup scored **0 hits** on this corpus (rtk-filtered at source); it's kept for the test-rerun case, kill-switchable, and labeled speculative until it earns a number.
361
-
362
- Pi already truncates built-in output at 50KB/2,000 lines. Replay over 8,044 persisted Pi results measured the compressor's **marginal** saving after that truncation: **27.4% of tool-output chars** ([receipt + raw live Pi arm](benchmarks/results/2026-09-11-pi.md)). Pi compresses `bash`, `powershell`, `grep`, `find`, `ls`, MCP, and explicitly allowlisted extension-tool results; `read`/`edit`/`write` remain untouched. `CHISLE_COMPRESS_TOOLS` supplies the same explicit override for runtime and replay. Runtime dedup only compares earlier turns, so concurrently completed sibling calls cannot dedup one another.
363
-
364
- ```bash
365
- node benchmarks/replay-compress.js # Claude Code
366
- node benchmarks/replay-compress.js pi # Pi
367
- ```
368
-
369
- Outputs over 8k chars are elided, and the elided original spills to `<config>/chisle-spill/` so the dropped middle stays reachable. The marker carries the path, so recovering one line is a targeted grep rather than a re-run of the command, which matters most when the command is not idempotent: a test run, a build, a `git log` at a moment in time. The newest 40 spills are kept, owner-readable only. `stop chisle`, `CHISLE_COMPRESS=0`, `CHISLE_COMPRESS_SCRUB=0`, `CHISLE_COMPRESS_DEDUP=0`, `CHISLE_COMPRESS_SPILL=0`. Every tier has an off switch.
370
-
371
- ### Pi arm: where the output axis lost
372
-
373
- Six tasks, Pi 0.85.1 on `openai-codex/gpt-5.5`, as % of the vanilla no-tool baseline. Lower is better, bold is the winner of the row:
374
-
375
- | segment | vanilla | caveman | ponytail | **Chisle** |
376
- |---|--:|--:|--:|--:|
377
- | coding billed tokens | 100% | 87% | **49%** | 73% |
378
- | non-coding billed tokens | 100% | 43% | 76% | **39%** |
379
- | **all billed tokens** | 100% | 72% | **59%** | 61% |
380
- | all visible-answer tokens | 100% | 56% | 49% | **37%** |
381
- | all answer lines | 100% | 75% | 38% | **37%** |
382
- | tasks correct | 5/6 | 5/6 | 5/6 | 5/6 |
383
-
384
- **Chisle lost billed output here.** ponytail used 63 fewer tokens over the six tasks. Publishing that is the point of this section.
385
-
386
- The coding gap is one task. Chisle leads 4 of 6 tasks outright; `auth-bug` alone accounts for 283 of the 337-token coding gap. On that task Chisle produced the shortest answer of any arm (377 chars to ponytail's 632) and was the only arm to find the real defect: ponytail flipped `>` to `>=`, while Chisle identified the seconds-versus-milliseconds unit mismatch behind it. The grader scored every arm as failing regardless, which says more about that task than about the tools.
387
-
388
- Six tasks, one model, one run. Raw events and per-task cells: [`benchmarks/results/raw-pi/`](benchmarks/results/raw-pi/).
389
-
390
- ### Prevention: the context diet
391
-
392
- The biggest context whale (whole-file `Read`s, 5.6M chars in the measured corpus) can't be compressed without breaking later edits. So the ruleset attacks it upstream, in every agent: grep for the symbol first, read only the matching region, narrow at the source (`ls dir` not `ls -R`, pipe long output through `tail`/`grep`), never re-read what's already in context.
393
-
394
- ---
395
-
396
- ## What the output sounds like
397
-
398
- **"Why does this React component re-render?"**
399
- > New object ref each render. Inline object prop = new ref = re-render. `useMemo`.
400
-
401
- **"Add a cache for API responses."**
402
- > `@lru_cache(maxsize=1000)` on the fetch fn. Skipped a custom cache class; add one when `lru_cache` measurably falls short.
403
-
404
- ---
405
-
406
- ## Usage
407
-
408
- | Command | Effect |
409
- |---------|--------|
410
- | *(nothing)* | On automatically every session after install |
411
- | `/chisle` | Re-activate if you'd stopped it |
412
- | `/chisle off` | Deactivate |
413
- | `stop chisle` | Deactivate (ruleset *and* input-side compression) |
414
- | `normal mode` | Deactivate |
415
-
416
- Natural language works too: "activate chisle", "chisle mode", "chislify this". Code symbols, function/API names, and error strings stay verbatim, so only the noise around them compresses.
170
+ Every table, chart and caveat, including the June suite and the Pi run in full: [docs/benchmarks.md](docs/benchmarks.md). Head to head: [docs/comparison.md](docs/comparison.md).
417
171
 
418
172
  ---
419
173
 
@@ -456,109 +210,23 @@ Mark deliberate simplifications so "later" doesn't quietly become "never":
456
210
  // chisle: O(n) scan, index this when table exceeds ~10k rows
457
211
  ```
458
212
 
459
- ---
460
-
461
- ## Config
462
-
463
- **On by default.** After install, Chisle activates automatically every session, with no `/chisle` needed. Set `off` to stay dormant until you type `/chisle`:
464
-
465
- ```bash
466
- # env var (highest priority)
467
- export CHISLE_DEFAULT_MODE=off
468
-
469
- # config file (persists across shells)
470
- ~/.config/chisle/config.json → { "defaultMode": "off" }
471
- ```
472
-
473
- Resolution: env var → config file → `on`. Valid: `off`, `on`.
474
-
475
- **Output styles are detected automatically.** Claude Code's output-style mechanism also governs
476
- prose structure, so an active style and Chisle's prose section give directly conflicting
477
- instructions — the style mandates tables and bullets for comparisons, Chisle's prose section forbids
478
- manufactured tables and bullets the question didn't ask for. Chisle now reads the active style and
479
- steps its prose rules aside on its own. Nothing to configure: run `/output-style Explanatory` and the
480
- prose section stops shipping; switch back to `default` and it returns.
481
-
482
- The code rules, the ladder, the context diet and the compressor are all unaffected — output styles
483
- govern prose, so only the prose section yields.
484
-
485
- Detection reads the `outputStyle` key across Claude Code's own settings precedence
486
- (`.claude/settings.local.json`, where `/output-style` writes, then `.claude/settings.json`, then
487
- `~/.claude/settings.json`). Claude Code only — Pi and OpenCode have no output-style mechanism, so
488
- there is nothing to detect and nothing changes for them.
489
-
490
- **Suppress a rule group manually.** The same `sections` key gives explicit control, and it always
491
- beats detection — set `prose: true` to keep Chisle's prose rules even with a style active:
492
-
493
- ```json
494
- { "sections": { "prose": false } }
495
- ```
496
-
497
- `sections.prose: false` suppresses the prose rules; `sections.code: false` suppresses the code
498
- rules. Config file only, no env override. Anything absent, malformed, or non-boolean falls back to
499
- enabled, which is also what lets automatic detection apply — only an explicitly written boolean
500
- overrides it. With no style active and nothing configured, the emitted ruleset is byte-identical to
501
- before either feature existed. The always-on sections
502
- (Persistence, Thinking Is Billed Too, Auto-Clarity, When NOT to be lazy, Boundaries) ship regardless
503
- of either setting.
504
-
505
- Scope: this only affects the runtime-hook agents that read `skills/chisle/SKILL.md` live — Claude
506
- Code and Pi. The seven static-rule agents built by `scripts/build-rules.js` (Cursor, Windsurf,
507
- Cline, Kiro, Codex, Gemini, Copilot) get flat files baked at build time with no runtime config to
508
- read, so they can't honor this key.
509
-
510
- ---
511
-
512
- ## Prior art & what stacks with it
513
-
514
- Chisle borrows the best published token-saving techniques and implements the ones that fit a zero-dep hook; the rest stack cleanly alongside it:
515
-
516
- | technique | source | in Chisle? |
517
- |---|---|---|
518
- | Prose compression persona | [caveman](https://github.com/JuliusBrussee/caveman) | ✅ + code judgment it lacks |
519
- | YAGNI/lazy-code ruleset | [ponytail](https://github.com/dietrichgebert/ponytail) | ✅ + prose discipline it lacks |
520
- | Tool-output elision (head/tail) | [headroom](https://github.com/headroomlabs-ai/headroom)-style, proxy-free | ✅ hook, no proxy, works on subscription OAuth |
521
- | ANSI strip / log crush / dedup | headroom transforms | ✅ scrub + dedup tiers |
522
- | Command rewriting at the source | RTK-style `PreToolUse` ([writeup](https://andrewpatterson.dev/posts/token-savings-rtk-headroom/)) | ❌ stacks; RTK shrinks at source, Chisle catches what it can't reach (subagents, MCP, web) |
523
- | MCP/codebase-graph indexing | context-mode, [token-optimizer-mcp](https://github.com/ooples/token-optimizer-mcp) | ❌ stacks, orthogonal layer |
524
- | CLAUDE.md dieting | [community guides](https://www.firecrawl.dev/blog/claude-code-token-efficiency) | ✅ `/chisle-audit` flags bloated docs/config prose |
525
-
526
- ## Multi-agent
527
-
528
- Ships to eleven agents: Claude Code and Pi get both axes, live `/chisle` toggling, and a status badge; OpenCode gets both axes too — the global fenced ruleset, on-demand skills, and a native compression plugin — but no live toggle; Cursor, Windsurf, Cline, Kiro, Codex, Gemini, and Copilot get the always-on ruleset; Hermes gets the skills as `/chisle` commands. Per-agent static copies come from `scripts/build-rules.js`; Pi uses its package extension plus the Agent Skills standard. See [`docs/agent-portability.md`](./docs/agent-portability.md).
529
-
530
- ## FAQ
531
-
532
- **Doesn't injecting a persona every turn cost tokens?**
533
- Claude Code uses a ruleset at session start (~900 tokens) plus a ~50-token reminder per turn. Pi injects one persistent rules message only when project `AGENTS.md` does not already provide it; resume/reload does not duplicate it, and compaction restores it only if removed.
534
-
535
- Worth reading the dissent before you take that on faith: [@enc0ded](https://github.com/enc0ded) measured 173 of their own sessions ([#2](https://github.com/JayPokale/Chisle/issues/2)) and found the injection overhead roughly cancelling the compressor's savings, because the ruleset was being re-sent on every resume and clear, not just at startup. That re-injection is fixed, which removes most of the overhead they measured, but their wider point stands: prose is only ~25% of what the model emits, so the ceiling on the output axis is lower than the headline suggests, and on a one-line throwaway prompt the overhead still exceeds the saving.
536
-
537
- **Will it golf my code into clever one-liners?**
538
- No. Boring over clever. Deletion beats addition; obfuscation isn't deletion.
539
-
540
- **Does it cut corners on safety?**
541
- Never. Input validation, data-loss handling, security, and accessibility are off the table. Lazy about solutions, not about reading the problem.
542
-
543
- **Can the output compressor eat a line I needed?**
544
- Designed not to: allowlist keeps `Read`/`Edit` exact, error-looking lines are salvaged from any elided region, dedup only fires on byte-identical same-session repeats, and every tier has a kill switch. If it still bites you, file an issue. That's a bug, not the design.
213
+ ## Usage
545
214
 
546
- **Should the star count worry me?**
547
- Everyone starts at zero. Run `npx chisle --dry-run`, see what it'd do, decide. And if the receipts convinced you, [a star](https://github.com/JayPokale/Chisle/stargazers) is how the next person finds them, and it's also the only payment a zero-dep MIT tool will ever ask for.
215
+ | Command | Effect |
216
+ |---------|--------|
217
+ | *(nothing)* | On automatically every session after install |
218
+ | `/chisle` | Re-activate if you'd stopped it |
219
+ | `/chisle off` | Deactivate |
220
+ | `stop chisle` | Deactivate (ruleset *and* input-side compression) |
221
+ | `normal mode` | Deactivate |
548
222
 
549
- → [More FAQ and competitor comparison](docs/comparison.md)
223
+ Natural language works too: "activate chisle", "chisle mode", "chislify this". Code symbols, function/API names, and error strings stay verbatim, so only the noise around them compresses.
550
224
 
551
225
  ---
552
226
 
553
227
  ## Contributing
554
228
 
555
- See [CONTRIBUTING.md](CONTRIBUTING.md). Edit the skill (`skills/chisle/SKILL.md`) **and** the condensed rule body in `scripts/build-rules.js`, regenerate copies and chart, run the tests. CI enforces all three.
556
-
557
- Built by [Jay Pokale](https://github.com/JayPokale) with [Claude](https://claude.com/claude-code), [Antigravity](https://antigravity.google), and [Codex](https://openai.com/blog/openai-codex/) as co-engineers: the input-compression hook, the benchmark verification, and several of the bug hunts documented in the changelog were pair-work.
558
-
559
- ```bash
560
- npm test # 67 tests: flag safety, tracker, settings merge, installer, compressor
561
- ```
229
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Built by [Jay Pokale](https://github.com/JayPokale) with [Claude](https://claude.com/claude-code), [Antigravity](https://antigravity.google), and [Codex](https://openai.com/blog/openai-codex/) as co-engineers.
562
230
 
563
231
  ## Star History
564
232
 
package/bin/install.js CHANGED
@@ -9,7 +9,7 @@
9
9
  // - Codex → fenced ruleset appended to ~/.codex/AGENTS.md
10
10
  // - OpenCode → fenced ruleset in ~/.config/opencode/AGENTS.md + skills copy
11
11
  // - Hermes → skills copy in ~/.hermes/skills (Agent Skills standard)
12
- // - Cursor/Windsurf/Cline/Kiro/Copilot → project rule file dropped into CWD
12
+ // - Cursor/Windsurf/Cline/Kiro/Copilot/Antigravity → project rule file dropped into CWD
13
13
  //
14
14
  // Usage:
15
15
  // npx chisle auto-detect + install
@@ -57,6 +57,10 @@ const PROVIDERS = [
57
57
  rule: '.kiro/steering/chisle.md' },
58
58
  { id: 'copilot', label: 'GitHub Copilot',scope: 'project', detect: 'vscode-ext:github.copilot||vscode-ext:github.copilot-chat',
59
59
  rule: '.github/copilot-instructions.md' },
60
+ // Antigravity IDE (`antigravity`) and its CLI (`agy`, antigravity-cli) both
61
+ // load workspace rules from .agents/rules/*.md.
62
+ { id: 'antigravity', label: 'Antigravity', scope: 'project', detect: 'cmd:antigravity||cmd:agy||dir:~/.antigravity',
63
+ rule: '.agents/rules/chisle.md' },
60
64
  ];
61
65
 
62
66
  // ── argv ────────────────────────────────────────────────────────────────────
@@ -597,7 +601,7 @@ function installHermes(ctx) {
597
601
  process.stdout.write('\n');
598
602
  }
599
603
 
600
- // ── Project-scoped rule agents (Cursor/Windsurf/Cline/Kiro/Copilot) ─────────
604
+ // ── Project-scoped rule agents (Cursor/Windsurf/Cline/Kiro/Copilot/Antigravity)
601
605
  function installProjectRule(ctx, prov) {
602
606
  const { say, note, warn, opts, results } = ctx;
603
607
  results.detected++;
@@ -709,7 +713,7 @@ function uninstall(ctx) {
709
713
  const dst = path.join(ocPlugins, rel);
710
714
  if (fs.existsSync(dst)) { if (!opts.dryRun) fs.rmSync(dst, { recursive: true, force: true }); note(' removed ' + dst); touched++; }
711
715
  }
712
- for (const f of ['chisle-spill', '.chisle-compress-stats.json', '.chisle-compress-last.json']) {
716
+ for (const f of ['chisle-spill', 'chisle-dedup', '.chisle-compress-stats.json', '.chisle-compress-last.json']) {
713
717
  const p = path.join(homeDir(), '.config', 'opencode', f);
714
718
  if (fs.existsSync(p)) { if (!opts.dryRun) fs.rmSync(p, { recursive: true, force: true }); note(' removed ' + p); touched++; }
715
719
  }
@@ -723,7 +727,7 @@ function uninstall(ctx) {
723
727
 
724
728
  if (!opts.only.length || opts.only.some(id => PROVIDERS.find(p => p.id === id).scope === 'project')) {
725
729
  note('');
726
- note('Project-scoped rule files (.cursor/, .windsurf/, .clinerules/, .kiro/, .github/copilot-instructions.md)');
730
+ note('Project-scoped rule files (.cursor/, .windsurf/, .clinerules/, .kiro/, .agents/rules/, .github/copilot-instructions.md)');
727
731
  note('live in your project repos, so remove them per-project with git if you added them there.');
728
732
  }
729
733
  say(touched ? c.green(`\nUninstalled. ${touched} item(s) cleaned.`) : c.yellow('\nNothing to uninstall.'));
@@ -836,7 +840,7 @@ function main() {
836
840
  if (opts.update) {
837
841
  warn('Nothing to update: Chisle is not installed for any detected agent.');
838
842
  note('Run `npx chisle` to install it, or `npx chisle --list` to see what we look for.');
839
- note('Project-scoped agents (Cursor, Windsurf, Cline, Kiro, Copilot) are per-repo:');
843
+ note('Project-scoped agents (Cursor, Windsurf, Cline, Kiro, Copilot, Antigravity) are per-repo:');
840
844
  note('run this from the project that has the rule file.');
841
845
  return;
842
846
  }
@@ -88,6 +88,46 @@ const SALVAGE_RE = /\b(error|err!|fail(ed|ure|ing)?|exception|traceback|panic|fa
88
88
  const MAX_SALVAGED = 12; // error lines rescued from the elided middle
89
89
  const MAX_SALVAGE_LINE = 300; // per-line char cap on salvaged lines
90
90
 
91
+ function splitsSurrogatePair(text, index) {
92
+ if (index <= 0 || index >= text.length) return false;
93
+ const before = text.charCodeAt(index - 1);
94
+ const after = text.charCodeAt(index);
95
+ return before >= 0xD800 && before <= 0xDBFF && after >= 0xDC00 && after <= 0xDFFF;
96
+ }
97
+
98
+ function safePrefix(text, maxChars) {
99
+ let end = Math.max(0, Math.min(text.length, maxChars));
100
+ if (splitsSurrogatePair(text, end)) end -= 1;
101
+ return text.slice(0, end);
102
+ }
103
+
104
+ function safeSuffix(text, maxChars) {
105
+ let start = Math.max(0, text.length - Math.max(0, maxChars));
106
+ if (splitsSurrogatePair(text, start)) start += 1;
107
+ return text.slice(start);
108
+ }
109
+
110
+ function safeSlice(text, start, end) {
111
+ let safeStart = Math.max(0, Math.min(text.length, start));
112
+ let safeEnd = Math.max(safeStart, Math.min(text.length, end));
113
+ if (splitsSurrogatePair(text, safeStart)) safeStart += 1;
114
+ if (splitsSurrogatePair(text, safeEnd)) safeEnd -= 1;
115
+ return text.slice(safeStart, Math.max(safeStart, safeEnd));
116
+ }
117
+
118
+ function salvageLine(line) {
119
+ const match = SALVAGE_RE.exec(line.replace(/error-like/gi, 'diagnostic'));
120
+ if (!match) return null;
121
+ if (line.length <= MAX_SALVAGE_LINE) return line;
122
+ const contentLimit = MAX_SALVAGE_LINE - 2;
123
+ const start = Math.max(0, Math.min(
124
+ match.index - Math.floor((contentLimit - match[0].length) / 2),
125
+ line.length - contentLimit,
126
+ ));
127
+ const end = start + contentLimit;
128
+ return (start > 0 ? '…' : '') + safeSlice(line, start, end) + (end < line.length ? '…' : '');
129
+ }
130
+
91
131
  function toolAllowed(name) {
92
132
  if (!name || typeof name !== 'string') return false;
93
133
  if (NEVER_COMPRESS.includes(name.toLowerCase())) return false;
@@ -107,28 +147,54 @@ function copilotToolAllowed(name) {
107
147
  return list.includes(name);
108
148
  }
109
149
 
110
- // OpenCode runtime tool names are lowercase (bash, grep, glob, webfetch, ...)
111
- // and MCP tools are `server_tool` (an underscore), not Claude's mcp__ prefix.
150
+ // OpenCode runtime tool names are lowercase (bash, grep, glob, webfetch, ...).
151
+ // MCP tools use `server_tool`, not Claude's mcp__ prefix; their names do not
152
+ // establish read-only safety, so they require explicit allowlisting.
112
153
  // read/edit/write/patch are excluded on purpose: their output feeds exact-match
113
154
  // edits, exactly as SAFE_TOOLS omits Read/Edit/Write.
114
- const SAFE_TOOLS_OPENCODE = ['bash', 'grep', 'glob', 'webfetch', 'websearch', 'task', 'list'];
115
- const UNSAFE_TOOLS_OPENCODE = ['read', 'edit', 'write', 'patch', 'todowrite', 'todoread'];
155
+ const SAFE_TOOLS_OPENCODE = ['bash', 'grep', 'glob', 'webfetch', 'websearch', 'list'];
156
+ const UNSAFE_TOOLS_OPENCODE = [
157
+ 'read', 'edit', 'write', 'patch', 'apply_patch', 'multiedit',
158
+ 'notebookedit', 'notebookread', 'notebook_write', 'notebook_edit',
159
+ 'create', 'str_replace', 'insert', 'todowrite', 'todoread',
160
+ ];
116
161
 
117
162
  function opencodeToolAllowed(name) {
118
163
  if (!name || typeof name !== 'string') return false;
119
164
  const lower = name.toLowerCase();
120
- // read/edit/write/patch are never compressed, even via CHISLE_COMPRESS_TOOLS:
121
- // their output feeds exact-match edits (the invariant INSTALL.md promises).
165
+ // Source-bearing and mutating tools are never compressed, even via
166
+ // CHISLE_COMPRESS_TOOLS: their output feeds exact-match edits, an invariant
167
+ // INSTALL.md promises rather than an overridable default.
122
168
  if (UNSAFE_TOOLS_OPENCODE.includes(lower)) return false;
123
169
  if (process.env.CHISLE_COMPRESS_TOOLS) {
124
170
  return process.env.CHISLE_COMPRESS_TOOLS
125
171
  .split(',').map(s => s.trim()).filter(Boolean).includes(name);
126
172
  }
127
- if (SAFE_TOOLS_OPENCODE.includes(lower)) return true;
128
- // Heuristic: underscore = MCP `server_tool`, treated as read-only info tool
129
- // like SAFE_TOOLS' mcp__ clause. Set CHISLE_COMPRESS_TOOLS to override if a
130
- // custom underscore-named tool ever needs protecting.
131
- return name.includes('_');
173
+ return SAFE_TOOLS_OPENCODE.includes(lower);
174
+ }
175
+
176
+ function boundForOpencode(text, spillPath, maxChars) {
177
+ const outputLimit = maxChars || THRESHOLDS.maxChars;
178
+ if (text.length <= outputLimit) return text;
179
+ if (outputLimit < 7) return safePrefix(text, outputLimit);
180
+ const recover = spillPath ? ' Full output: ' + spillPath + ' (grep it, do not re-run)' : '';
181
+ let marker = '... [chisle: rebound compressed output to ' +
182
+ outputLimit.toLocaleString('en-US') + ' chars.' + recover + '] ...';
183
+ if (marker.length > outputLimit - 2) marker = safePrefix(marker, outputLimit - 7) + '] ...';
184
+ const middle = [marker];
185
+ // Keep head/tail context without excluding diagnostics under small bounds.
186
+ const contextReserve = Math.min(2050, Math.floor(outputLimit / 4));
187
+ for (const line of text.split('\n')) {
188
+ if (middle.length > MAX_SALVAGED) break;
189
+ const candidate = salvageLine(line);
190
+ if (!candidate) continue;
191
+ if (middle.join('\n').length + candidate.length + 1 > outputLimit - contextReserve) break;
192
+ if (!middle.includes(candidate)) middle.push(candidate);
193
+ }
194
+ const center = middle.join('\n');
195
+ const remaining = Math.max(0, outputLimit - center.length - 2);
196
+ const headChars = Math.ceil(remaining / 2);
197
+ return safePrefix(text, headChars) + '\n' + center + '\n' + safeSuffix(text, Math.floor(remaining / 2));
132
198
  }
133
199
 
134
200
  // One OpenCode tool_result → replacement string, or null to keep the original.
@@ -149,7 +215,19 @@ function compressForOpencode(toolName, text, opts) {
149
215
  if (!opencodeToolAllowed(toolName)) return null;
150
216
  if (typeof text !== 'string' || !text) return null;
151
217
  const stateDir = opts.stateDir || process.env.CHISLE_STATE_DIR || getOpencodeDir();
152
- return transform(text, limitsFor(), toolName, stateDir);
218
+ const limits = limitsFor();
219
+ const dup = dedupCheck(toolName, text, opts.sessionId, opts.callId, stateDir, true);
220
+ if (dup != null && dup.length < text.length) return boundForOpencode(dup, null, limits.maxChars);
221
+ if (text.length <= SCRUB_MIN && text.length <= limits.maxChars) return null;
222
+ const spillPath = text.length > limits.maxChars && toolName ? spill(text, toolName, stateDir) : null;
223
+ let updated = process.env.CHISLE_COMPRESS_SCRUB === '0' ? text : scrub(text);
224
+ if (updated.length > limits.maxChars && updated.split('\n').length > limits.headLines + limits.tailLines) {
225
+ const elided = compress(updated, limits, spillPath);
226
+ if (elided != null) updated = elided;
227
+ }
228
+ updated = boundForOpencode(updated, spillPath, limits.maxChars);
229
+ if (text.length > limits.maxChars) return updated;
230
+ return text.length - updated.length >= MIN_WIN ? updated : null;
153
231
  }
154
232
 
155
233
  // Copilot CLI's postToolUse payload is flat and camelCase:
@@ -240,26 +318,103 @@ function duplicateMarker(toolName, text, reference = 'previous') {
240
318
  const lines = text.split('\n');
241
319
  const prior = reference === 'previous' ? 'the previous' : 'an earlier';
242
320
  const preview = lines.slice(0, 5)
243
- .map(l => l.length > MAX_SALVAGE_LINE ? l.slice(0, MAX_SALVAGE_LINE) + '…' : l);
321
+ .map(l => l.length > MAX_SALVAGE_LINE ? safePrefix(l, MAX_SALVAGE_LINE - 1) + '…' : l);
244
322
  return '[chisle: output byte-identical to ' + prior + ' ' + toolName + ' result — ' +
245
323
  text.length.toLocaleString('en-US') + ' chars / ' + lines.length +
246
324
  ' lines, unchanged. First lines:]\n' + preview.join('\n');
247
325
  }
248
326
 
249
- // `stateDir` defaults to getClaudeDir() so every existing Claude/Pi call site
250
- // and test (none of which pass a third argument) keeps its exact prior
251
- // behaviour. Copilot's caller (processPayload, below) passes getCopilotDir().
252
- function dedupCheck(toolName, text, sessionId, toolUseId, stateDir) {
327
+ const DEDUP_SESSION_KEEP = 20;
328
+ const DEDUP_DIR = 'chisle-dedup';
329
+
330
+ function readDedupFile(p) {
331
+ let st;
332
+ try { st = fs.lstatSync(p); }
333
+ catch (e) { return { state: null, unsafe: e.code !== 'ENOENT' }; }
334
+ if (st.isSymbolicLink() || !st.isFile()) return { state: null, unsafe: true };
335
+ const noFollow = typeof fs.constants.O_NOFOLLOW === 'number' ? fs.constants.O_NOFOLLOW : 0;
336
+ let fd;
337
+ let raw;
338
+ try {
339
+ fd = fs.openSync(p, fs.constants.O_RDONLY | noFollow);
340
+ raw = fs.readFileSync(fd, 'utf8');
341
+ } catch (e) {
342
+ return { state: null, unsafe: true };
343
+ } finally {
344
+ if (fd !== undefined) { try { fs.closeSync(fd); } catch (e) {} }
345
+ }
346
+ try { return { state: JSON.parse(raw) || null, unsafe: false }; }
347
+ catch (e) { return { state: null, unsafe: false }; }
348
+ }
349
+
350
+ function dedupStateDir(stateDir) {
351
+ const dir = path.join(stateDir || getClaudeDir(), DEDUP_DIR);
352
+ try { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); } catch (e) { return null; }
353
+ try {
354
+ const st = fs.lstatSync(dir);
355
+ return st.isDirectory() && !st.isSymbolicLink() ? dir : null;
356
+ } catch (e) { return null; }
357
+ }
358
+
359
+ function writeDedupState(p, state) {
360
+ const tmp = p + '.' + process.pid + '.' + crypto.randomBytes(6).toString('hex') + '.tmp';
361
+ const noFollow = typeof fs.constants.O_NOFOLLOW === 'number' ? fs.constants.O_NOFOLLOW : 0;
362
+ const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow;
363
+ let fd;
364
+ try {
365
+ fd = fs.openSync(tmp, flags, 0o600);
366
+ fs.writeFileSync(fd, JSON.stringify(state));
367
+ try { fs.fchmodSync(fd, 0o600); } catch (e) {}
368
+ fs.closeSync(fd);
369
+ fd = undefined;
370
+ fs.renameSync(tmp, p);
371
+ } catch (e) {
372
+ if (fd !== undefined) { try { fs.closeSync(fd); } catch (closeError) {} }
373
+ try { fs.unlinkSync(tmp); } catch (unlinkError) {}
374
+ throw e;
375
+ }
376
+ }
377
+
378
+ function pruneDedupSessions(dir) {
379
+ try {
380
+ const files = fs.readdirSync(dir)
381
+ .filter(name => /^[a-f0-9]{64}\.json$/.test(name))
382
+ .map(name => {
383
+ const p = path.join(dir, name);
384
+ const st = fs.lstatSync(p);
385
+ return st.isFile() || st.isSymbolicLink() ? { p, t: st.mtimeMs } : null;
386
+ })
387
+ .filter(Boolean)
388
+ .sort((a, b) => b.t - a.t || a.p.localeCompare(b.p));
389
+ for (const file of files.slice(DEDUP_SESSION_KEEP)) {
390
+ try { fs.unlinkSync(file.p); } catch (e) {}
391
+ }
392
+ } catch (e) {}
393
+ }
394
+
395
+ // `stateDir` defaults to getClaudeDir() so existing Claude/Pi callers keep
396
+ // their prior storage path. Copilot passes getCopilotDir(); OpenCode passes
397
+ // getOpencodeDir() and opts into per-session files for interleaved sessions.
398
+ function dedupCheck(toolName, text, sessionId, toolUseId, stateDir, perSession) {
253
399
  if (process.env.CHISLE_COMPRESS_DEDUP === '0') return null;
254
400
  if (!sessionId || typeof sessionId !== 'string') return null;
255
401
  if (text.length < DEDUP_MIN) return null;
256
402
  try {
257
- const dir = stateDir || getClaudeDir();
258
- const p = path.join(dir, '.chisle-compress-last.json');
259
- try { if (fs.lstatSync(p).isSymbolicLink()) return null; } catch (e) { if (e.code !== 'ENOENT') return null; }
260
- let state = {};
261
- try { state = JSON.parse(fs.readFileSync(p, 'utf8')) || {}; } catch (e) {}
262
- if (state.session !== sessionId) state = { session: sessionId, tools: {} };
403
+ const dir = perSession ? dedupStateDir(stateDir) : stateDir || getClaudeDir();
404
+ if (!dir) return null;
405
+ const p = path.join(dir, perSession
406
+ ? crypto.createHash('sha256').update(sessionId).digest('hex') + '.json'
407
+ : '.chisle-compress-last.json');
408
+ let state;
409
+ if (perSession) {
410
+ const loaded = readDedupFile(p);
411
+ if (loaded.unsafe) return null;
412
+ state = loaded.state;
413
+ } else {
414
+ try { if (fs.lstatSync(p).isSymbolicLink()) return null; } catch (e) { if (e.code !== 'ENOENT') return null; }
415
+ try { state = JSON.parse(fs.readFileSync(p, 'utf8')) || {}; } catch (e) {}
416
+ }
417
+ if (!state || state.session !== sessionId) state = { session: sessionId, tools: {} };
263
418
  if (!state.tools || typeof state.tools !== 'object') state.tools = {};
264
419
  const hash = crypto.createHash('sha256').update(text).digest('hex');
265
420
  // A second hook invocation for the SAME tool call is not the model seeing
@@ -274,10 +429,15 @@ function dedupCheck(toolName, text, sessionId, toolUseId, stateDir) {
274
429
  const sameCall = !!(toolUseId && rec.id && rec.id === toolUseId);
275
430
  const dup = rec.hash === hash && !sameCall;
276
431
  state.tools[toolName] = { hash, id: toolUseId || rec.id || null };
277
- fs.mkdirSync(dir, { recursive: true });
278
- const tmp = p + '.' + process.pid + '.tmp';
279
- fs.writeFileSync(tmp, JSON.stringify(state), { mode: 0o600 });
280
- fs.renameSync(tmp, p);
432
+ if (perSession) {
433
+ writeDedupState(p, state);
434
+ pruneDedupSessions(dir);
435
+ } else {
436
+ fs.mkdirSync(dir, { recursive: true });
437
+ const tmp = p + '.' + process.pid + '.tmp';
438
+ fs.writeFileSync(tmp, JSON.stringify(state), { mode: 0o600 });
439
+ fs.renameSync(tmp, p);
440
+ }
281
441
  return dup ? duplicateMarker(toolName, text) : null;
282
442
  } catch (e) { return null; }
283
443
  }
@@ -336,9 +496,9 @@ function compress(text, limits, spillPath) {
336
496
  const keep = Math.floor(maxChars / 2);
337
497
  const elided = text.length - 2 * keep;
338
498
  if (elided <= 0) return null;
339
- return text.slice(0, keep) +
499
+ return safePrefix(text, keep) +
340
500
  '\n... [chisle: elided ' + elided.toLocaleString('en-US') + ' chars from the middle.' + recover + '] ...\n' +
341
- text.slice(-keep);
501
+ safeSuffix(text, keep);
342
502
  }
343
503
 
344
504
  const head = lines.slice(0, headLines);
@@ -348,7 +508,8 @@ function compress(text, limits, spillPath) {
348
508
  const salvaged = [];
349
509
  for (const line of middle) {
350
510
  if (salvaged.length >= MAX_SALVAGED) break;
351
- if (SALVAGE_RE.test(line)) salvaged.push(line.length > MAX_SALVAGE_LINE ? line.slice(0, MAX_SALVAGE_LINE) + '…' : line);
511
+ const candidate = salvageLine(line);
512
+ if (candidate) salvaged.push(candidate);
352
513
  }
353
514
 
354
515
  const marker = '... [chisle: elided ' + middle.length.toLocaleString('en-US') +
@@ -513,5 +674,6 @@ module.exports = {
513
674
  isCopilotPayload, copilotToolAllowed, SAFE_TOOLS_COPILOT, NEVER_COMPRESS_COPILOT,
514
675
  // OpenCode-specific additions (plugin uses tool.execute.after +
515
676
  // experimental.chat.messages.transform):
516
- opencodeToolAllowed, compressForOpencode, SAFE_TOOLS_OPENCODE,
677
+ opencodeToolAllowed, compressForOpencode, SAFE_TOOLS_OPENCODE, UNSAFE_TOOLS_OPENCODE,
678
+ boundForOpencode, recordSavings,
517
679
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chisle",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "description": "Maximum-efficiency dev mode for AI coding agents. Zero-fluff prose, YAGNI-first code, and safe tool-output compression.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -17,6 +17,7 @@
17
17
  "cursor",
18
18
  "codex",
19
19
  "copilot",
20
+ "antigravity",
20
21
  "opencode",
21
22
  "hermes-agent",
22
23
  "hermes",
@@ -93,6 +94,7 @@
93
94
  ".windsurf/",
94
95
  ".clinerules/",
95
96
  ".kiro/",
97
+ ".agents/",
96
98
  ".github/copilot-instructions.md",
97
99
  ".claude-plugin/",
98
100
  ".opencode/",