chisle 3.0.0 → 3.1.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chisle",
3
- "version": "3.0.0",
3
+ "version": "3.1.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": [
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-8%20agents-d78a3c?style=flat-square" alt="Works with 8 agents">
17
+ <img src="https://img.shields.io/badge/works%20with-9%20agents-d78a3c?style=flat-square" alt="Works with 9 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">
@@ -79,30 +79,30 @@ Most efficiency tools compress one thing. Chisle compresses **three**:
79
79
  |---|---|---|
80
80
  | **Output: prose** | filler, hedging, manufactured structure | zero-fluff ruleset, injected per session |
81
81
  | **Output: code** | speculative abstractions, unrequested boilerplate | YAGNI efficiency ladder |
82
- | **Input: context** | oversized tool output flooding the window | `PostToolUse` hook — scrub, elide, dedup — plus prevention rules |
82
+ | **Input: context** | oversized tool output flooding the window | Claude `PostToolUse` / Pi `tool_result` — scrub, elide, dedup — plus prevention rules |
83
83
 
84
84
  Where each one attaches to a session:
85
85
 
86
86
  ```mermaid
87
87
  flowchart LR
88
88
  subgraph S["Session start"]
89
- H1["SessionStart hook<br/>~1.6k tokens, new sessions only"]
89
+ H1["Ruleset injection<br/>once per active session"]
90
90
  end
91
91
  subgraph T["Every turn"]
92
- H2["UserPromptSubmit hook<br/>~50-token reminder"]
92
+ H2["Mode tracking<br/>Claude + Pi"]
93
93
  end
94
94
  subgraph L["Every tool call"]
95
- H3["PostToolUse hook<br/>scrub → elide → dedup"]
95
+ H3["PostToolUse / tool_result<br/>scrub → elide → dedup"]
96
96
  end
97
97
 
98
98
  H1 --> M(["Model"])
99
99
  H2 --> M
100
100
  M -->|writes| O["Output:<br/>terser prose,<br/>YAGNI-first code"]
101
- M -->|calls a tool| TOOL[["Bash / Grep / WebFetch / mcp__*"]]
101
+ M -->|calls a tool| TOOL[["Bash / grep / web / extension tools"]]
102
102
  TOOL -->|raw output| H3
103
103
  H3 -->|"compressed, rebuilt into<br/>the tool's own shape"| M
104
104
 
105
- RE["Read / Edit"] -.->|"never touched —<br/>exact bytes feed later edits"| M
105
+ RE["Read / Edit / Write"] -.->|"never touched —<br/>exact bytes feed later edits"| M
106
106
 
107
107
  style M fill:#1f2937,stroke:#d78a3c,color:#e6edf3
108
108
  style O fill:#14532d,stroke:#2da44e,color:#e6edf3
@@ -110,7 +110,7 @@ flowchart LR
110
110
  style RE fill:#3f1d1d,stroke:#cf3b3b,color:#e6edf3
111
111
  ```
112
112
 
113
- 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` and `Edit` are deliberately outside it.
113
+ 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
114
 
115
115
  <p align="center">
116
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).">
@@ -122,7 +122,7 @@ Every "be concise" tool has a worst day — the day it makes the model write *mo
122
122
 
123
123
  ## Install
124
124
 
125
- One command. Auto-detects your agents (Claude Code, Cursor, Windsurf, Cline, Kiro, Codex, Gemini, Copilot) and wires each one. `--uninstall` puts everything back.
125
+ One command. Auto-detects your agents (Claude Code, Pi, Cursor, Windsurf, Cline, Kiro, Codex, Gemini, Copilot) and wires each one. `--uninstall` puts everything back.
126
126
 
127
127
  ```bash
128
128
  npx chisle
@@ -138,9 +138,9 @@ curl -fsSL https://raw.githubusercontent.com/JayPokale/Chisle/main/install.sh |
138
138
  irm https://raw.githubusercontent.com/JayPokale/Chisle/main/install.ps1 | iex
139
139
  ```
140
140
 
141
- Preview first with `npx chisle --dry-run`, scope with `--only claude`, see everything with `npx chisle --help`. Remove with `npx chisle --uninstall`.
141
+ 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
142
 
143
- **Requirements:** Node ≥18 (installer / `npx`) · Claude Code for `/chisle` toggling and input-side compression — the always-on ruleset still ships to every other agent.
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
144
 
145
145
  ### Upgrading
146
146
 
@@ -167,17 +167,26 @@ claude plugin marketplace add JayPokale/Chisle # register the marketplace
167
167
  claude plugin install chisle@chisle # enable the plugin
168
168
  ```
169
169
 
170
+ ### Pi package
171
+
172
+ ```bash
173
+ pi install npm:chisle
174
+ ```
175
+
176
+ 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.
177
+
170
178
  ### See what it would do, before it does it
171
179
 
172
180
  ```bash
173
181
  npx chisle --dry-run # prints every file it would touch, changes nothing
174
182
  ```
175
183
 
176
- Want the savings measured on your own work rather than ours? Clone the repo and replay the compressor over your existing Claude Code transcripts — it reads them locally, writes nothing, and reports the tokens the input-side hook would have stripped:
184
+ 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:
177
185
 
178
186
  ```bash
179
187
  git clone https://github.com/JayPokale/Chisle && cd Chisle
180
- node benchmarks/replay-compress.js
188
+ node benchmarks/replay-compress.js # Claude Code
189
+ node benchmarks/replay-compress.js pi # Pi; marginal over Pi's native truncation
181
190
  ```
182
191
 
183
192
  ---
@@ -283,7 +292,7 @@ SUITE=large bash benchmarks/run-live.sh <model> benchmarks/results/raw-large
283
292
 
284
293
  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)
285
294
 
286
- ### Input axis — tool-output compression (Claude Code)
295
+ ### Input axis — tool-output compression (Claude Code + Pi)
287
296
 
288
297
  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:
289
298
 
@@ -293,10 +302,13 @@ Measured over 171 real sessions ([receipts](benchmarks/results/2026-07-07-input-
293
302
  | **elide** | oversized output → head + tail, error-like lines salvaged from the cut | bounded, guarded |
294
303
  | **dedup** | byte-identical repeat of a tool's previous output (same session) → one-line marker | none — the copy is already in context |
295
304
 
296
- Replayed over the same 171 sessions: **~61k tokens** saved one-shot, ~46% off every eligible output — floor, not 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. 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. Replay it on your own transcripts:
305
+ Replayed over the same 171 Claude Code sessions: **~61k tokens** saved one-shot, ~46% off every eligible output — floor, not 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. 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.
306
+
307
+ 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.
297
308
 
298
309
  ```bash
299
- node benchmarks/replay-compress.js # what it would have saved you
310
+ node benchmarks/replay-compress.js # Claude Code
311
+ node benchmarks/replay-compress.js pi # Pi
300
312
  ```
301
313
 
302
314
  Outputs over 8k chars are elided. `stop chisle`, `CHISLE_COMPRESS=0`, `CHISLE_COMPRESS_SCRUB=0`, `CHISLE_COMPRESS_DEDUP=0` — every tier has an off switch.
@@ -404,12 +416,12 @@ Chisle borrows the best published token-saving techniques and implements the one
404
416
 
405
417
  ## Multi-agent
406
418
 
407
- Primarily a Claude Code plugin, but ships to every agent with a rules/context file — Cursor, Windsurf, Cline, Kiro, Codex, Gemini, Copilot. Per-agent copies generated by `scripts/build-rules.js` (a condensed mirror of the skill — edit both, CI checks sync). See [`docs/agent-portability.md`](./docs/agent-portability.md). Input-side compression is Claude Code-only for now: no other agent exposes a post-tool output rewrite hook.
419
+ Ships to nine agents: Claude Code and Pi get both axes, live `/chisle` toggling, and a status badge; Cursor, Windsurf, Cline, Kiro, Codex, Gemini, and Copilot get the always-on ruleset. 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).
408
420
 
409
421
  ## FAQ
410
422
 
411
423
  **Doesn't injecting a persona every turn cost tokens?**
412
- Yes — a ruleset at session start (~1.6k tokens) plus a ~50-token reminder per turn. Output is where it pays back: coding answers shrink 40–60% (benchmarks), and output bills several × higher than input.
424
+ Claude Code uses a ruleset at session start (~1.6k 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.
413
425
 
414
426
  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.
415
427
 
package/bin/install.js CHANGED
@@ -4,6 +4,7 @@
4
4
  // Detects the AI coding agents on this machine and installs Chisle for each:
5
5
  // - Claude Code → plugin (marketplace add + install), fallback to standalone
6
6
  // hooks + settings.json merge + statusline badge
7
+ // - Pi → Pi package (extension + skill)
7
8
  // - Gemini CLI → gemini extensions install
8
9
  // - Codex → fenced ruleset appended to ~/.codex/AGENTS.md
9
10
  // - Cursor/Windsurf/Cline/Kiro/Copilot → project rule file dropped into CWD
@@ -39,6 +40,7 @@ const REPO_ROOT = path.resolve(__dirname, '..');
39
40
  // (that's how Cursor/Cline/Copilot rules actually work).
40
41
  const PROVIDERS = [
41
42
  { id: 'claude', label: 'Claude Code', scope: 'global', detect: 'cmd:claude' },
43
+ { id: 'pi', label: 'Pi', scope: 'global', detect: 'cmd:pi' },
42
44
  { id: 'gemini', label: 'Gemini CLI', scope: 'global', detect: 'cmd:gemini' },
43
45
  { id: 'codex', label: 'Codex CLI', scope: 'global', detect: 'cmd:codex||dir:~/.codex' },
44
46
  { id: 'cursor', label: 'Cursor', scope: 'project', detect: 'cmd:cursor||dir:~/.cursor',
@@ -196,6 +198,18 @@ function installClaude(ctx) {
196
198
  else results.failed.push(['claude-hooks', r]);
197
199
  } else {
198
200
  note(' hooks: plugin manifest handles SessionStart + UserPromptSubmit + PostToolUse');
201
+ // Plugin path won — drop any standalone wiring left by an earlier run, or
202
+ // both copies of every hook run on each event and the PostToolUse pair
203
+ // dedups first-seen output against itself.
204
+ const settingsPath = path.join(claudeDir(opts), 'settings.json');
205
+ const settings = SETTINGS.readSettings(settingsPath);
206
+ if (settings && SETTINGS.removeHooks(settings, 'chisle-') > 0) {
207
+ if (!opts.dryRun) {
208
+ SETTINGS.validateHookFields(settings);
209
+ SETTINGS.writeSettings(settingsPath, settings);
210
+ }
211
+ note(' removed superseded standalone hook entries from settings.json');
212
+ }
199
213
  }
200
214
  process.stdout.write('\n');
201
215
  }
@@ -209,7 +223,7 @@ function installClaudeHooks(ctx) {
209
223
  const hooksDst = path.join(cfg, 'chisle-hooks');
210
224
  const settingsPath = path.join(cfg, 'settings.json');
211
225
  const HOOK_FILES = ['package.json', 'chisle-config.js', 'chisle-activate.js',
212
- 'chisle-mode-tracker.js', 'chisle-compress-output.js',
226
+ 'chisle-mode.js', 'chisle-mode-tracker.js', 'chisle-compress-output.js',
213
227
  'chisle-statusline.sh', 'chisle-statusline.ps1'];
214
228
 
215
229
  if (opts.dryRun) {
@@ -263,6 +277,30 @@ function installClaudeHooks(ctx) {
263
277
  return 'ok';
264
278
  }
265
279
 
280
+ // ── Pi ──────────────────────────────────────────────────────────────────────
281
+ function piSources(output) {
282
+ return String(output || '').split('\n').map(line => line.trim())
283
+ .filter(line => /^npm:chisle(?:@|$)/i.test(line) || /^git:.*github\.com\/JayPokale\/Chisle(?:@|$)/i.test(line))
284
+ .map(source => source.replace(/@[^/@]+$/, ''));
285
+ }
286
+
287
+ function installPi(ctx) {
288
+ const { say, note, opts, results } = ctx;
289
+ results.detected++;
290
+ say('→ Pi detected');
291
+ if (!opts.force) {
292
+ const installed = piSources(capture('pi', ['list']).stdout);
293
+ if (installed.length) {
294
+ note(' chisle package already installed (use --force)');
295
+ results.skipped.push(['pi', 'already installed']); process.stdout.write('\n'); return;
296
+ }
297
+ }
298
+ const r = run('pi', ['install', 'npm:chisle'], opts.dryRun);
299
+ if ((r.status || 0) === 0) results.installed.push('pi');
300
+ else results.failed.push(['pi', 'pi package install failed']);
301
+ process.stdout.write('\n');
302
+ }
303
+
266
304
  // ── Gemini ──────────────────────────────────────────────────────────────────
267
305
  function installGemini(ctx) {
268
306
  const { say, note, opts, results } = ctx;
@@ -347,53 +385,66 @@ function uninstall(ctx) {
347
385
  say(c.orange('Chisle uninstall'));
348
386
  let touched = 0;
349
387
 
350
- // Claude plugin
351
- if (hasCmd('claude') && !opts.dryRun) {
352
- const r = capture('claude', ['plugin', 'list']);
353
- if (r.status === 0 && /chisle/i.test(r.stdout || '')) {
354
- run('claude', ['plugin', 'uninstall', 'chisle@chisle'], opts.dryRun); touched++;
388
+ const wants = id => !opts.only.length || opts.only.includes(id);
389
+
390
+ if (wants('claude')) {
391
+ // Claude plugin
392
+ if (hasCmd('claude') && !opts.dryRun) {
393
+ const r = capture('claude', ['plugin', 'list']);
394
+ if (r.status === 0 && /chisle/i.test(r.stdout || '')) {
395
+ run('claude', ['plugin', 'uninstall', 'chisle@chisle'], opts.dryRun); touched++;
396
+ }
355
397
  }
356
- }
357
398
 
358
- // Claude standalone hooks + statusline
359
- const cfg = claudeDir(opts);
360
- const settingsPath = path.join(cfg, 'settings.json');
361
- const settings = SETTINGS.readSettings(settingsPath);
362
- if (settings) {
363
- const removed = SETTINGS.removeHooks(settings, 'chisle-');
364
- let slRemoved = false;
365
- if (settings.statusLine && typeof settings.statusLine.command === 'string'
366
- && settings.statusLine.command.includes('chisle-statusline')) { delete settings.statusLine; slRemoved = true; }
367
- if (removed > 0 || slRemoved) {
368
- if (!opts.dryRun) { SETTINGS.validateHookFields(settings); SETTINGS.writeSettings(settingsPath, settings); }
369
- note(` removed ${removed} hook entr${removed === 1 ? 'y' : 'ies'}${slRemoved ? ' + statusline' : ''} from settings.json`);
370
- touched++;
399
+ // Claude standalone hooks + statusline
400
+ const cfg = claudeDir(opts);
401
+ const settingsPath = path.join(cfg, 'settings.json');
402
+ const settings = SETTINGS.readSettings(settingsPath);
403
+ if (settings) {
404
+ const removed = SETTINGS.removeHooks(settings, 'chisle-');
405
+ let slRemoved = false;
406
+ if (settings.statusLine && typeof settings.statusLine.command === 'string'
407
+ && settings.statusLine.command.includes('chisle-statusline')) { delete settings.statusLine; slRemoved = true; }
408
+ if (removed > 0 || slRemoved) {
409
+ if (!opts.dryRun) { SETTINGS.validateHookFields(settings); SETTINGS.writeSettings(settingsPath, settings); }
410
+ note(` removed ${removed} hook entr${removed === 1 ? 'y' : 'ies'}${slRemoved ? ' + statusline' : ''} from settings.json`);
411
+ touched++;
412
+ }
413
+ }
414
+ const hooksDst = path.join(cfg, 'chisle-hooks');
415
+ if (fs.existsSync(hooksDst)) { if (!opts.dryRun) fs.rmSync(hooksDst, { recursive: true, force: true }); note(` removed ${hooksDst}`); touched++; }
416
+
417
+ for (const f of ['.chisle-active', '.chisle-session-turns', '.chisle-statusline-suffix',
418
+ '.chisle-compress-stats.json', '.chisle-compress-last.json', '.chisle-update-check.json']) {
419
+ const p = path.join(cfg, f);
420
+ if (fs.existsSync(p)) { if (!opts.dryRun) { try { fs.unlinkSync(p); } catch (_) {} } note(` removed ${p}`); touched++; }
371
421
  }
372
422
  }
373
- const hooksDst = path.join(cfg, 'chisle-hooks');
374
- if (fs.existsSync(hooksDst)) { if (!opts.dryRun) fs.rmSync(hooksDst, { recursive: true, force: true }); note(` removed ${hooksDst}`); touched++; }
375
423
 
376
- // Flag files
377
- for (const f of ['.chisle-active', '.chisle-session-turns', '.chisle-statusline-suffix',
378
- '.chisle-compress-stats.json', '.chisle-compress-last.json', '.chisle-update-check.json']) {
379
- const p = path.join(cfg, f);
380
- if (fs.existsSync(p)) { if (!opts.dryRun) { try { fs.unlinkSync(p); } catch (_) {} } note(` removed ${p}`); touched++; }
424
+ if (wants('pi') && hasCmd('pi')) {
425
+ const sources = piSources(capture('pi', ['list']).stdout);
426
+ for (const source of sources) {
427
+ if ((run('pi', ['remove', source], opts.dryRun).status || 0) === 0) touched++;
428
+ }
381
429
  }
382
430
 
383
- // Codex fenced block
384
- const codexMd = path.join(os.homedir(), '.codex', 'AGENTS.md');
385
- if (fs.existsSync(codexMd)) {
386
- const txt = fs.readFileSync(codexMd, 'utf8');
387
- if (txt.includes(FENCE_BEGIN)) {
388
- const stripped = txt.replace(new RegExp(`\\n?${FENCE_BEGIN}[\\s\\S]*?${FENCE_END}\\n?`), '\n').replace(/\n{3,}/g, '\n\n');
389
- if (!opts.dryRun) fs.writeFileSync(codexMd, stripped, { mode: 0o644 });
390
- note(` removed chisle block from ${codexMd}`); touched++;
431
+ if (wants('codex')) {
432
+ const codexMd = path.join(os.homedir(), '.codex', 'AGENTS.md');
433
+ if (fs.existsSync(codexMd)) {
434
+ const txt = fs.readFileSync(codexMd, 'utf8');
435
+ if (txt.includes(FENCE_BEGIN)) {
436
+ const stripped = txt.replace(new RegExp(`\\n?${FENCE_BEGIN}[\\s\\S]*?${FENCE_END}\\n?`), '\n').replace(/\n{3,}/g, '\n\n');
437
+ if (!opts.dryRun) fs.writeFileSync(codexMd, stripped, { mode: 0o644 });
438
+ note(` removed chisle block from ${codexMd}`); touched++;
439
+ }
391
440
  }
392
441
  }
393
442
 
394
- note('');
395
- note('Project-scoped rule files (.cursor/, .windsurf/, .clinerules/, .kiro/, .github/copilot-instructions.md)');
396
- note('live in your project repos — remove them per-project with git if you added them there.');
443
+ if (!opts.only.length || opts.only.some(id => PROVIDERS.find(p => p.id === id).scope === 'project')) {
444
+ note('');
445
+ note('Project-scoped rule files (.cursor/, .windsurf/, .clinerules/, .kiro/, .github/copilot-instructions.md)');
446
+ note('live in your project repos — remove them per-project with git if you added them there.');
447
+ }
397
448
  say(touched ? c.green(`\nUninstalled. ${touched} item(s) cleaned.`) : c.yellow('\nNothing to uninstall.'));
398
449
  }
399
450
 
@@ -406,7 +457,7 @@ Usage:
406
457
 
407
458
  Flags:
408
459
  --list Detect agents and print them; install nothing
409
- --only <id> Install only for one agent (repeatable)
460
+ --only <id> Limit install/uninstall to one agent (repeatable)
410
461
  --dry-run Print actions, change nothing
411
462
  --force Reinstall / overwrite even if already present
412
463
  --uninstall, -u Remove what chisle installed
@@ -463,6 +514,7 @@ function main() {
463
514
 
464
515
  for (const p of targets) {
465
516
  if (p.id === 'claude') installClaude(ctx);
517
+ else if (p.id === 'pi') installPi(ctx);
466
518
  else if (p.id === 'gemini') installGemini(ctx);
467
519
  else if (p.id === 'codex') installCodex(ctx);
468
520
  else installProjectRule(ctx, p);
@@ -476,7 +528,7 @@ function main() {
476
528
  if (results.failed.length) say(c.red(` failed: ${results.failed.map(s => s[0] + ' (' + s[1] + ')').join(', ')}`));
477
529
  say('');
478
530
  if (!opts.dryRun && results.installed.length) {
479
- say(c.orange('Done.') + ' Restart your agent. Type ' + c.orange('/chisle') + ' (Claude Code) or just start coding.');
531
+ say(c.orange('Done.') + ' Restart your agent. Type ' + c.orange('/chisle') + ' in Claude Code or Pi, or just start coding.');
480
532
  note('If Chisle earns its keep, a star helps others find it: https://github.com/JayPokale/Chisle');
481
533
  }
482
534
  process.exit(results.failed.length ? 1 : 0);
@@ -130,7 +130,17 @@ function scrub(text) {
130
130
  // skipped when the payload carries none.
131
131
  const DEDUP_MIN = 2048;
132
132
 
133
- function dedupCheck(toolName, text, sessionId) {
133
+ function duplicateMarker(toolName, text, reference = 'previous') {
134
+ const lines = text.split('\n');
135
+ const prior = reference === 'previous' ? 'the previous' : 'an earlier';
136
+ const preview = lines.slice(0, 5)
137
+ .map(l => l.length > MAX_SALVAGE_LINE ? l.slice(0, MAX_SALVAGE_LINE) + '…' : l);
138
+ return '[chisle: output byte-identical to ' + prior + ' ' + toolName + ' result — ' +
139
+ text.length.toLocaleString('en-US') + ' chars / ' + lines.length +
140
+ ' lines, unchanged. First lines:]\n' + preview.join('\n');
141
+ }
142
+
143
+ function dedupCheck(toolName, text, sessionId, toolUseId) {
134
144
  if (process.env.CHISLE_COMPRESS_DEDUP === '0') return null;
135
145
  if (!sessionId || typeof sessionId !== 'string') return null;
136
146
  if (text.length < DEDUP_MIN) return null;
@@ -142,18 +152,22 @@ function dedupCheck(toolName, text, sessionId) {
142
152
  if (state.session !== sessionId) state = { session: sessionId, tools: {} };
143
153
  if (!state.tools || typeof state.tools !== 'object') state.tools = {};
144
154
  const hash = crypto.createHash('sha256').update(text).digest('hex');
145
- const dup = state.tools[toolName] === hash;
146
- state.tools[toolName] = hash;
155
+ // A second hook invocation for the SAME tool call is not the model seeing
156
+ // the output twice. That happens whenever this hook is registered more than
157
+ // once (plugin manifest + a leftover settings.json entry), because both
158
+ // copies share this state file: copy A stores the hash, copy B matches it
159
+ // and reports first-seen output as a duplicate of itself. Old string-valued
160
+ // entries read through `rec`, so an existing state file needs no migration,
161
+ // and a payload with no tool_use_id keeps today's exact behaviour.
162
+ const prev = state.tools[toolName];
163
+ const rec = (prev && typeof prev === 'object') ? prev : { hash: prev, id: null };
164
+ const sameCall = !!(toolUseId && rec.id && rec.id === toolUseId);
165
+ const dup = rec.hash === hash && !sameCall;
166
+ state.tools[toolName] = { hash, id: toolUseId || rec.id || null };
147
167
  const tmp = p + '.' + process.pid + '.tmp';
148
168
  fs.writeFileSync(tmp, JSON.stringify(state), { mode: 0o600 });
149
169
  fs.renameSync(tmp, p);
150
- if (!dup) return null;
151
- const lines = text.split('\n');
152
- const preview = lines.slice(0, 5)
153
- .map(l => l.length > MAX_SALVAGE_LINE ? l.slice(0, MAX_SALVAGE_LINE) + '…' : l);
154
- return '[chisle: output byte-identical to the previous ' + toolName + ' result — ' +
155
- text.length.toLocaleString('en-US') + ' chars / ' + lines.length +
156
- ' lines, unchanged. First lines:]\n' + preview.join('\n');
170
+ return dup ? duplicateMarker(toolName, text) : null;
157
171
  } catch (e) { return null; }
158
172
  }
159
173
 
@@ -259,7 +273,7 @@ function processPayload(payload, mode) {
259
273
 
260
274
  const text = extractText(payload.tool_response != null ? payload.tool_response : payload.tool_output);
261
275
  if (!text) return null;
262
- const dup = dedupCheck(payload.tool_name, text, payload.session_id);
276
+ const dup = dedupCheck(payload.tool_name, text, payload.session_id, payload.tool_use_id);
263
277
  if (dup != null && dup.length < text.length) return dup;
264
278
  return transform(text, limitsFor());
265
279
  }
@@ -293,4 +307,4 @@ function main() {
293
307
 
294
308
  if (require.main === module) main();
295
309
 
296
- module.exports = { extractText, rebuildResponse, scrub, compress, transform, limitsFor, toolAllowed, processPayload, THRESHOLDS, SAFE_TOOLS };
310
+ module.exports = { extractText, rebuildResponse, scrub, compress, transform, limitsFor, toolAllowed, processPayload, duplicateMarker, THRESHOLDS, SAFE_TOOLS };
@@ -5,7 +5,8 @@
5
5
 
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
- const { getDefaultMode, getClaudeDir, safeWriteFlag, readFlag } = require('./chisle-config');
8
+ const { getClaudeDir, safeWriteFlag, readFlag } = require('./chisle-config');
9
+ const { requestedMode } = require('./chisle-mode');
9
10
 
10
11
  const claudeDir = getClaudeDir();
11
12
  const flagPath = path.join(claudeDir, '.chisle-active');
@@ -16,39 +17,12 @@ process.stdin.on('end', () => {
16
17
  try {
17
18
  const data = JSON.parse(input.replace(/^/, ''));
18
19
  const prompt = (data.prompt || '').trim();
19
- const promptLower = prompt.toLowerCase();
20
+ const request = requestedMode(prompt);
20
21
 
21
- // Natural language activation
22
- if (/\b(activate|enable|turn on|start|use)\b.*\bchisle\b/i.test(promptLower) ||
23
- /\bchisle\b.*\b(mode|activate|enable|on)\b/i.test(promptLower) ||
24
- /\bchislif(y|ier)\b/i.test(promptLower)) {
25
- if (!/\b(stop|disable|turn off|deactivate|off)\b/i.test(promptLower)) {
26
- const mode = getDefaultMode();
27
- if (mode !== 'off') safeWriteFlag(flagPath, mode);
28
- }
29
- }
30
-
31
- // /chisle slash commands. No level argument any more: /chisle on,
32
- // /chisle off, nothing else.
33
- if (/^\/chisle(\b|:chisle\b)/.test(promptLower)) {
34
- const parts = promptLower.split(/\s+/);
35
- const arg = parts[1] || '';
36
- if (arg === 'off' || arg === 'stop' || arg === 'disable') {
37
- try { fs.unlinkSync(flagPath); } catch (e) {}
38
- } else {
39
- const mode = getDefaultMode();
40
- if (mode !== 'off') safeWriteFlag(flagPath, mode);
41
- }
42
- }
43
-
44
- // Natural language deactivation.
45
- // Only fire when the off-verb actually targets chisle — NOT when chisle merely
46
- // appears in a sentence that also mentions turning something else off.
47
- // ("use chisle to turn off the logger" must NOT deactivate.)
48
- if (/\b(turn off|disable|deactivate|stop|kill|exit)\s+chisle\b/i.test(promptLower) ||
49
- /\bchisle\s+(mode\s+)?(off|stop|disable|deactivate)\b/i.test(promptLower) ||
50
- /\bnormal mode\b/i.test(promptLower)) {
22
+ if (request === 'off') {
51
23
  try { fs.unlinkSync(flagPath); } catch (e) {}
24
+ } else if (request === 'on') {
25
+ safeWriteFlag(flagPath, 'on');
52
26
  }
53
27
 
54
28
  // Per-turn reinforcement
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ // Shared mode-directive parser. Harness adapters decide how to persist mode.
4
+ function requestedMode(prompt) {
5
+ const text = String(prompt || '').trim().toLowerCase();
6
+
7
+ if (/\b(turn off|disable|deactivate|stop|kill|exit)\s+chisle\b/i.test(text) ||
8
+ /\bchisle\s+(mode\s+)?(off|stop|disable|deactivate)\b/i.test(text) ||
9
+ /\bnormal mode\b/i.test(text)) return 'off';
10
+
11
+ if (/^\/chisle(\b|:chisle\b)/.test(text) ||
12
+ /\b(activate|enable|turn on|start|use)\b.*\bchisle\b/i.test(text) ||
13
+ /\bchisle\b.*\b(mode|activate|enable|on)\b/i.test(text) ||
14
+ /\bchislif(y|ier)\b/i.test(text)) return 'on';
15
+
16
+ return null;
17
+ }
18
+
19
+ module.exports = { requestedMode };
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "chisle",
3
- "version": "3.0.0",
4
- "description": "Maximum-efficiency dev mode for Claude Code. Zero-fluff prose + YAGNI-first code decisions. One plugin that does both.",
3
+ "version": "3.1.0",
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",
7
7
  "claude-code-plugin",
8
+ "pi-package",
9
+ "pi-coding-agent",
8
10
  "productivity",
9
11
  "yagni",
10
12
  "minimal"
@@ -19,10 +21,6 @@
19
21
  "name": "Claude (Anthropic)",
20
22
  "url": "https://claude.com/claude-code"
21
23
  },
22
- {
23
- "name": "Antigravity (Google)",
24
- "url": "https://antigravity.google"
25
- },
26
24
  {
27
25
  "name": "Codex (OpenAI)",
28
26
  "url": "https://openai.com/codex/"
@@ -42,6 +40,14 @@
42
40
  "engines": {
43
41
  "node": ">=18"
44
42
  },
43
+ "pi": {
44
+ "extensions": [
45
+ "./pi-extension/index.js"
46
+ ],
47
+ "skills": [
48
+ "./skills"
49
+ ]
50
+ },
45
51
  "scripts": {
46
52
  "test": "node --test tests/*.js",
47
53
  "build:rules": "node scripts/build-rules.js",
@@ -55,6 +61,7 @@
55
61
  "files": [
56
62
  "bin/",
57
63
  "hooks/",
64
+ "pi-extension/",
58
65
  "skills/",
59
66
  "commands/",
60
67
  "rules/",
@@ -0,0 +1,188 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { getDefaultMode } = require('../hooks/chisle-config');
7
+ const { duplicateMarker, limitsFor, transform } = require('../hooks/chisle-compress-output');
8
+ const { requestedMode } = require('../hooks/chisle-mode');
9
+
10
+ const RULES_TYPE = 'chisle-rules';
11
+ const MODE_TYPE = 'chisle-mode';
12
+ const SAFE_TOOLS = new Set(['bash', 'powershell', 'grep', 'find', 'ls']);
13
+ const NEVER_COMPRESS = new Set(['read', 'edit', 'write']);
14
+ const DEDUP_MIN = 2048;
15
+ const FALLBACK_RULES = `CHISLE ACTIVE
16
+
17
+ Chisle: maximum-efficiency dev mode. Zero-fluff prose. YAGNI-first code.
18
+
19
+ Drop articles, filler, pleasantries, and hedging. Fragments OK; technical terms exact.
20
+ Code ladder: YAGNI → reuse → stdlib → native → installed dependency → one line → minimum code.
21
+ Never simplify away input validation, data-loss prevention, security, accessibility, or explicit requirements.`;
22
+
23
+ function loadRules() {
24
+ try {
25
+ const skill = fs.readFileSync(path.join(__dirname, '..', 'skills', 'chisle', 'SKILL.md'), 'utf8');
26
+ return 'CHISLE ACTIVE\n\n' + skill.replace(/^---[\s\S]*?---\s*/, '');
27
+ } catch (_) {
28
+ return FALLBACK_RULES;
29
+ }
30
+ }
31
+
32
+ function hasRulesEntry(entries) {
33
+ return (entries || []).some(entry =>
34
+ (entry.type === 'custom_message' && entry.customType === RULES_TYPE) ||
35
+ (entry.type === 'message' && entry.message && entry.message.role === 'custom' &&
36
+ entry.message.customType === RULES_TYPE));
37
+ }
38
+
39
+ function promptHasRules(event) {
40
+ const containsRules = text => /^# Chisle\s*$/m.test(text || '') &&
41
+ (text || '').includes('## Prose: zero fluff') &&
42
+ (text || '').includes('## Code: the efficiency ladder');
43
+ if (containsRules(event.systemPrompt)) return true;
44
+ return (event.systemPromptOptions && event.systemPromptOptions.contextFiles || [])
45
+ .some(file => containsRules(file.content));
46
+ }
47
+
48
+ function piToolAllowed(name) {
49
+ if (!name || NEVER_COMPRESS.has(name.toLowerCase())) return false;
50
+ if (process.env.CHISLE_COMPRESS_TOOLS) {
51
+ return process.env.CHISLE_COMPRESS_TOOLS.split(',').map(s => s.trim()).filter(Boolean).includes(name);
52
+ }
53
+ return SAFE_TOOLS.has(name.toLowerCase()) || name.startsWith('mcp__');
54
+ }
55
+
56
+ function chisleExtension(pi) {
57
+ const rules = loadRules();
58
+ let active = getDefaultMode() !== 'off';
59
+ let rulesInContext = false;
60
+ let savedChars = 0;
61
+ let lastByToolBlock = new Map();
62
+ let dedupBaseline = new Map();
63
+ const stopRulesSignal = pi.events.on('chisle:rules-injected', () => { rulesInContext = true; });
64
+ const stopModeSignal = pi.events.on('chisle:mode', mode => { active = mode === 'on'; });
65
+
66
+ function updateStatus(ctx) {
67
+ if (!ctx.hasUI) return;
68
+ if (!active) return ctx.ui.setStatus('chisle', undefined);
69
+ const tokens = Math.floor(savedChars / 4);
70
+ const saved = tokens >= 1000 ? ` ⇣${Math.floor(tokens / 1000)}k tok` : tokens ? ` ⇣${tokens} tok` : '';
71
+ ctx.ui.setStatus('chisle', ctx.ui.theme.fg('accent', `[CHISLE]${saved}`));
72
+ }
73
+
74
+ function restoreBranch(ctx) {
75
+ active = getDefaultMode() !== 'off';
76
+ savedChars = 0;
77
+ lastByToolBlock = new Map();
78
+ dedupBaseline = new Map();
79
+
80
+ const modeEntry = ctx.sessionManager.getBranch()
81
+ .filter(entry => entry.type === 'custom' && entry.customType === MODE_TYPE).pop();
82
+ if (modeEntry && modeEntry.data && ['on', 'off'].includes(modeEntry.data.mode)) {
83
+ active = modeEntry.data.mode === 'on';
84
+ }
85
+ rulesInContext = hasRulesEntry(ctx.sessionManager.buildContextEntries());
86
+ updateStatus(ctx);
87
+ }
88
+
89
+ function setMode(request, ctx) {
90
+ active = request === 'on';
91
+ pi.events.emit('chisle:mode', active ? 'on' : 'off');
92
+ pi.appendEntry(MODE_TYPE, { mode: active ? 'on' : 'off' });
93
+ updateStatus(ctx);
94
+ if (ctx.hasUI) ctx.ui.notify(active ? 'Chisle enabled.' : 'Chisle disabled.', 'info');
95
+ }
96
+
97
+ pi.registerCommand('chisle', {
98
+ description: 'Enable Chisle or set /chisle on|off',
99
+ handler: async (args, ctx) => {
100
+ const request = requestedMode(`/chisle ${args || ''}`) || 'on';
101
+ setMode(request, ctx);
102
+ },
103
+ });
104
+
105
+ pi.on('session_start', async (_event, ctx) => restoreBranch(ctx));
106
+ pi.on('session_tree', async (_event, ctx) => restoreBranch(ctx));
107
+
108
+ pi.on('session_shutdown', async (_event, ctx) => {
109
+ lastByToolBlock.clear();
110
+ dedupBaseline.clear();
111
+ stopRulesSignal();
112
+ stopModeSignal();
113
+ if (ctx.hasUI) ctx.ui.setStatus('chisle', undefined);
114
+ });
115
+
116
+ pi.on('session_compact', async (_event, ctx) => {
117
+ lastByToolBlock.clear();
118
+ dedupBaseline.clear();
119
+ rulesInContext = hasRulesEntry(ctx.sessionManager.buildContextEntries());
120
+ });
121
+
122
+ pi.on('input', async (event, ctx) => {
123
+ if (event.source === 'extension') return { action: 'continue' };
124
+ const request = requestedMode(event.text);
125
+ if (!request) return { action: 'continue' };
126
+ setMode(request, ctx);
127
+ return request === 'off' ? { action: 'handled' } : { action: 'continue' };
128
+ });
129
+
130
+ pi.on('before_agent_start', async event => {
131
+ if (!active) {
132
+ if (event.systemPrompt.includes('CHISLE DISABLED.')) return;
133
+ return { systemPrompt: event.systemPrompt + '\n\nCHISLE DISABLED. Ignore only Chisle style and efficiency-ladder instructions.' };
134
+ }
135
+ if (rulesInContext || promptHasRules(event)) return;
136
+ rulesInContext = true;
137
+ pi.events.emit('chisle:rules-injected');
138
+ return { message: { customType: RULES_TYPE, content: rules, display: false } };
139
+ });
140
+
141
+ pi.on('context', async event => {
142
+ if (active) return;
143
+ return { messages: event.messages.filter(message =>
144
+ !(message.role === 'custom' && message.customType === RULES_TYPE)) };
145
+ });
146
+
147
+ pi.on('turn_start', async () => {
148
+ dedupBaseline = new Map(lastByToolBlock);
149
+ });
150
+
151
+ pi.on('tool_result', async (event, ctx) => {
152
+ try {
153
+ if (!active || process.env.CHISLE_COMPRESS === '0' || !piToolAllowed(event.toolName) ||
154
+ !Array.isArray(event.content)) return;
155
+
156
+ let changed = false;
157
+ const content = event.content.map((block, index) => {
158
+ if (!block || block.type !== 'text' || typeof block.text !== 'string') return block;
159
+ const text = block.text;
160
+ const key = `${event.toolName}:${index}`;
161
+ let updated = null;
162
+
163
+ if (process.env.CHISLE_COMPRESS_DEDUP !== '0' && text.length >= DEDUP_MIN) {
164
+ const hash = crypto.createHash('sha256').update(text).digest('hex');
165
+ const previous = dedupBaseline.get(key);
166
+ lastByToolBlock.set(key, hash);
167
+ if (previous === hash) updated = duplicateMarker(event.toolName, text, 'earlier');
168
+ }
169
+ if (updated == null) updated = transform(text, limitsFor());
170
+ if (updated == null || updated.length >= text.length) return block;
171
+ changed = true;
172
+ savedChars += text.length - updated.length;
173
+ return { ...block, text: updated };
174
+ });
175
+
176
+ if (!changed) return;
177
+ updateStatus(ctx);
178
+ return { content };
179
+ } catch (_) {
180
+ return;
181
+ }
182
+ });
183
+ }
184
+
185
+ module.exports = chisleExtension;
186
+ module.exports.loadRules = loadRules;
187
+ module.exports.hasRulesEntry = hasRulesEntry;
188
+ module.exports.piToolAllowed = piToolAllowed;