chisle 2.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,8 +1,7 @@
1
1
  # chisle slash command
2
2
  # Maximum-efficiency dev mode
3
- # Usage: /chisle [lite|full|ultra]
3
+ # Usage: /chisle · /chisle off
4
4
 
5
5
  [command]
6
6
  name = "chisle"
7
- description = "Activate chisle mode: zero-fluff prose + YAGNI ladder code decisions. Levels: lite, full (default), ultra."
8
- argument-hint = "[lite|full|ultra]"
7
+ description = "Activate chisle: zero-fluff prose + YAGNI ladder code decisions. /chisle off to stop."
@@ -3,13 +3,13 @@
3
3
  //
4
4
  // 1. Writes flag at $CLAUDE_CONFIG_DIR/.chisle-active
5
5
  // 2. Resets session turn counter
6
- // 3. Emits chisle ruleset (filtered to active level) as system context
6
+ // 3. Emits the chisle ruleset as system context
7
7
  // 4. Nudges user to configure statusline if missing
8
8
  // 5. Major-version update notice (majors only, cached, fail-silent)
9
9
 
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
- const { getDefaultMode, getClaudeDir, safeWriteFlag } = require('./chisle-config');
12
+ const { getDefaultMode, getClaudeDir, safeWriteFlag, legacySetting } = require('./chisle-config');
13
13
 
14
14
  // ── Update-notice helpers (pure — tested directly) ─────────────────────────
15
15
  // Minor/patch releases stay quiet: a nudge per major is signal, more is spam.
@@ -55,8 +55,16 @@ async function latestVersion(cachePath) {
55
55
  } catch (e) { return null; }
56
56
  }
57
57
 
58
+ // SessionStart fires on startup, resume, clear and compact. Only a genuinely
59
+ // new session needs the whole ruleset: on the other three the conversation
60
+ // already carries it, and the UserPromptSubmit reminder re-states the active
61
+ // behaviour every turn anyway. Re-sending ~1.6k tokens each time was the bulk
62
+ // of the plugin's own overhead and piled standing instructions on top of what
63
+ // the user actually asked for. Measured and reported by @enc0ded (#2).
64
+ const FULL_INJECT_SOURCES = new Set(['startup']);
65
+
58
66
  // ── Hook body ───────────────────────────────────────────────────────────────
59
- function run() {
67
+ function run(source) {
60
68
  const claudeDir = getClaudeDir();
61
69
  const flagPath = path.join(claudeDir, '.chisle-active');
62
70
  const settingsPath = path.join(claudeDir, 'settings.json');
@@ -72,8 +80,17 @@ function run() {
72
80
  // 1. Write flag
73
81
  safeWriteFlag(flagPath, mode);
74
82
 
75
- // 2. Read SKILL.md — single source of truth for behavior
76
- const modeLabel = mode;
83
+ // 2. Resumed/cleared/compacted session: reactivate, don't re-teach.
84
+ if (!FULL_INJECT_SOURCES.has(source)) {
85
+ process.stdout.write(
86
+ 'CHISLE ACTIVE (resumed). ' +
87
+ 'Ruleset already in context; see the chisle skill if it is not.'
88
+ );
89
+ return;
90
+ }
91
+
92
+ // 3. Read SKILL.md — single source of truth for behaviour. No level
93
+ // filtering any more: there is one mode, so the whole body ships.
77
94
  let skillContent = '';
78
95
  try {
79
96
  skillContent = fs.readFileSync(
@@ -84,31 +101,14 @@ function run() {
84
101
  let output;
85
102
 
86
103
  if (skillContent) {
87
- const body = skillContent.replace(/^---[\s\S]*?---\s*/, '');
88
-
89
- const filtered = body.split('\n').reduce((acc, line) => {
90
- const tableRowMatch = line.match(/^\|\s*\*\*(\S+?)\*\*\s*\|/);
91
- if (tableRowMatch) {
92
- if (tableRowMatch[1] === modeLabel) acc.push(line);
93
- return acc;
94
- }
95
- const exampleMatch = line.match(/^- (\S+?):\s/);
96
- if (exampleMatch) {
97
- if (exampleMatch[1] === modeLabel) acc.push(line);
98
- return acc;
99
- }
100
- acc.push(line);
101
- return acc;
102
- }, []);
103
-
104
- output = 'CHISLE MODE ACTIVE — level: ' + modeLabel + '\n\n' + filtered.join('\n');
104
+ output = 'CHISLE ACTIVE\n\n' + skillContent.replace(/^---[\s\S]*?---\s*/, '');
105
105
  } else {
106
106
  // Fallback ruleset when SKILL.md not found
107
107
  output =
108
- 'CHISLE MODE ACTIVE — level: ' + modeLabel + '\n\n' +
108
+ 'CHISLE ACTIVE\n\n' +
109
109
  'Chisle: maximum-efficiency dev mode. Zero-fluff prose. YAGNI-first code.\n\n' +
110
110
  '## Persistence\n\n' +
111
- 'ACTIVE EVERY RESPONSE. Off only: "stop chisle" / "normal mode". Switch: `/chisle lite|full|ultra`.\n\n' +
111
+ 'ACTIVE EVERY RESPONSE. Off only: "stop chisle" / "normal mode".\n\n' +
112
112
  '## Prose\n\n' +
113
113
  'Drop articles/filler/pleasantries/hedging. Fragments OK. Technical terms exact.\n\n' +
114
114
  '## Code\n\n' +
@@ -131,14 +131,25 @@ function run() {
131
131
  const statusLineSnippet =
132
132
  '"statusLine": { "type": "command", "command": ' + JSON.stringify(command) + ' }';
133
133
  output += '\n\n' +
134
- 'STATUSLINE SETUP NEEDED: The chisle plugin includes a statusline badge showing active mode ' +
135
- '(e.g. [CHISLE], [CHISLE:ULTRA]) with token savings. It is not configured yet. ' +
134
+ 'STATUSLINE SETUP NEEDED: The chisle plugin includes a statusline badge ' +
135
+ '([CHISLE]) with token savings. It is not configured yet. ' +
136
136
  'To enable, add this to ' + settingsPath + ': ' +
137
137
  statusLineSnippet + ' ' +
138
138
  'Proactively offer to set this up for the user on first interaction.';
139
139
  }
140
140
  } catch (e) {}
141
141
 
142
+ // 3b. One-time courtesy for the 3.0.0 upgrade: a lite/full/ultra setting is
143
+ // no longer meaningful. Chisle still runs (the value falls through to the
144
+ // default), but silently ignoring a setting someone chose is worse than
145
+ // saying so once.
146
+ const legacy = legacySetting();
147
+ if (legacy) {
148
+ output += '\n\nCHISLE NOTE: `' + legacy.value + '` came from ' + legacy.source +
149
+ '. Intensity levels were removed in 3.0.0 — there is one mode now, and it is ' +
150
+ 'active. Set it to `on` or `off`, or drop it entirely. Mention this once.';
151
+ }
152
+
142
153
  // 4. Update notice, then emit. CHISLE_UPDATE_CHECK=0 disables.
143
154
  (async () => {
144
155
  try {
@@ -155,6 +166,24 @@ function run() {
155
166
  })();
156
167
  }
157
168
 
158
- if (require.main === module) run();
169
+ // Claude Code pipes the hook payload (including `source`) on stdin. If it is
170
+ // absent or unparseable, fall back to a full inject — over-teaching once is a
171
+ // far cheaper failure than a session that never receives the ruleset at all.
172
+ function main() {
173
+ let input = '';
174
+ process.stdin.on('data', chunk => { input += chunk; });
175
+ process.stdin.on('end', () => {
176
+ let source = 'startup';
177
+ try {
178
+ const parsed = JSON.parse(input.replace(/^/, ''));
179
+ if (parsed && typeof parsed.source === 'string') source = parsed.source;
180
+ } catch (e) {}
181
+ run(source);
182
+ });
183
+ // stdin never opened (manual run): behave like a fresh session.
184
+ if (process.stdin.isTTY) run('startup');
185
+ }
186
+
187
+ if (require.main === module) main();
159
188
 
160
189
  module.exports = { majorOf, majorUpdateNotice };
@@ -19,11 +19,11 @@
19
19
  // scrub (output > 1k) — lossless: strip ANSI escapes, collapse blank-line
20
20
  // runs, collapse ≥4 identical consecutive lines to
21
21
  // one + "[repeated N×]". No information lost.
22
- // elide (output > mode threshold) — head + tail + error salvage, as above.
22
+ // elide (output > threshold) — head + tail + error salvage, as above.
23
23
  // Plus dedup: a tool output byte-identical to that tool's immediately previous
24
24
  // output is replaced by a short marker — the content is already in context.
25
25
  //
26
- // Compression tracks the /chisle mode flag (off → untouched). Tunables via env:
26
+ // Compression follows the /chisle flag (off → untouched). Tunables via env:
27
27
  // CHISLE_COMPRESS=0 — kill switch
28
28
  // CHISLE_COMPRESS_SCRUB=0 — disable the lossless scrub tier
29
29
  // CHISLE_COMPRESS_DEDUP=0 — disable duplicate-output markers
@@ -39,12 +39,10 @@ const path = require('path');
39
39
  const crypto = require('crypto');
40
40
  const { getClaudeDir, readFlag } = require('./chisle-config');
41
41
 
42
- // Mode → thresholds. Tighter mode, tighter budget. Env overrides all.
43
- const THRESHOLDS = {
44
- lite: { maxChars: 16000, headLines: 100, tailLines: 80 },
45
- full: { maxChars: 8000, headLines: 60, tailLines: 40 },
46
- ultra: { maxChars: 5000, headLines: 40, tailLines: 30 },
47
- };
42
+ // One threshold. Env overrides all.
43
+ // These are the values the old 'full' level used — the default,
44
+ // and the only one the published benchmarks ever ran at.
45
+ const THRESHOLDS = { maxChars: 8000, headLines: 60, tailLines: 40 };
48
46
 
49
47
  // Tools whose output is safe to elide. Read/Edit/Write are absent on purpose:
50
48
  // their output feeds exact-match edits. mcp__* are read-only info tools.
@@ -66,8 +64,8 @@ function envInt(name, fallback) {
66
64
  return Number.isFinite(n) && n > 0 ? n : fallback;
67
65
  }
68
66
 
69
- function limitsFor(mode) {
70
- const base = THRESHOLDS[mode] || THRESHOLDS.full;
67
+ function limitsFor() {
68
+ const base = THRESHOLDS;
71
69
  return {
72
70
  maxChars: envInt('CHISLE_COMPRESS_MAX_CHARS', base.maxChars),
73
71
  headLines: envInt('CHISLE_COMPRESS_HEAD_LINES', base.headLines),
@@ -132,7 +130,17 @@ function scrub(text) {
132
130
  // skipped when the payload carries none.
133
131
  const DEDUP_MIN = 2048;
134
132
 
135
- 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) {
136
144
  if (process.env.CHISLE_COMPRESS_DEDUP === '0') return null;
137
145
  if (!sessionId || typeof sessionId !== 'string') return null;
138
146
  if (text.length < DEDUP_MIN) return null;
@@ -144,18 +152,22 @@ function dedupCheck(toolName, text, sessionId) {
144
152
  if (state.session !== sessionId) state = { session: sessionId, tools: {} };
145
153
  if (!state.tools || typeof state.tools !== 'object') state.tools = {};
146
154
  const hash = crypto.createHash('sha256').update(text).digest('hex');
147
- const dup = state.tools[toolName] === hash;
148
- 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 };
149
167
  const tmp = p + '.' + process.pid + '.tmp';
150
168
  fs.writeFileSync(tmp, JSON.stringify(state), { mode: 0o600 });
151
169
  fs.renameSync(tmp, p);
152
- if (!dup) return null;
153
- const lines = text.split('\n');
154
- const preview = lines.slice(0, 5)
155
- .map(l => l.length > MAX_SALVAGE_LINE ? l.slice(0, MAX_SALVAGE_LINE) + '…' : l);
156
- return '[chisle: output byte-identical to the previous ' + toolName + ' result — ' +
157
- text.length.toLocaleString('en-US') + ' chars / ' + lines.length +
158
- ' lines, unchanged. First lines:]\n' + preview.join('\n');
170
+ return dup ? duplicateMarker(toolName, text) : null;
159
171
  } catch (e) { return null; }
160
172
  }
161
173
 
@@ -225,6 +237,32 @@ function transform(text, limits) {
225
237
  return text.length - t.length >= MIN_WIN ? t : null;
226
238
  }
227
239
 
240
+ // Put the compressed text back into the ORIGINAL response shape.
241
+ //
242
+ // Claude Code validates updatedToolOutput against the tool's own output schema
243
+ // before applying it. Bash results are objects ({stdout, stderr, ...}), so
244
+ // handing back a bare string is rejected on every call — the hook appears to
245
+ // work, logs savings, and the model still receives the full output. Returning
246
+ // null (skip) is always safer than emitting a shape the harness will refuse.
247
+ // Reported with the transcript evidence by @sovdchains (#3).
248
+ function rebuildResponse(response, updated) {
249
+ if (response == null || typeof response === 'string') return updated;
250
+ if (typeof response === 'object' && !Array.isArray(response)) {
251
+ for (const key of ['stdout', 'output', 'content', 'text', 'result']) {
252
+ if (typeof response[key] === 'string') {
253
+ const out = { ...response, [key]: updated };
254
+ // extractText already folded stderr into the compressed text; leaving
255
+ // the original stderr in place would duplicate it.
256
+ if (key === 'stdout' && typeof response.stderr === 'string') out.stderr = '';
257
+ return out;
258
+ }
259
+ }
260
+ }
261
+ // Unrecognized shape (e.g. MCP content-block arrays). Skip rather than guess:
262
+ // a wrong shape is silently rejected, which is the bug this function fixes.
263
+ return null;
264
+ }
265
+
228
266
  // Full pipeline on one hook payload → updated output string, or null to keep
229
267
  // the original. Pure given (payload, mode, env) except dedup state.
230
268
  function processPayload(payload, mode) {
@@ -235,9 +273,9 @@ function processPayload(payload, mode) {
235
273
 
236
274
  const text = extractText(payload.tool_response != null ? payload.tool_response : payload.tool_output);
237
275
  if (!text) return null;
238
- 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);
239
277
  if (dup != null && dup.length < text.length) return dup;
240
- return transform(text, limitsFor(mode));
278
+ return transform(text, limitsFor());
241
279
  }
242
280
 
243
281
  function main() {
@@ -249,12 +287,18 @@ function main() {
249
287
  const mode = readFlag(path.join(getClaudeDir(), '.chisle-active'));
250
288
  const updated = processPayload(payload, mode);
251
289
  if (updated == null) return;
252
- const original = extractText(payload.tool_response != null ? payload.tool_response : payload.tool_output);
290
+ const response = payload.tool_response != null ? payload.tool_response : payload.tool_output;
291
+ const rebuilt = rebuildResponse(response, updated);
292
+ // Shape we can't safely rebuild — emit nothing and count nothing.
293
+ if (rebuilt == null) return;
294
+ const original = extractText(response);
295
+ // Only after we know a replacement is actually going out, so the stats
296
+ // file stops crediting savings the harness never applied.
253
297
  recordSavings(original.length - updated.length);
254
298
  process.stdout.write(JSON.stringify({
255
299
  hookSpecificOutput: {
256
300
  hookEventName: 'PostToolUse',
257
- updatedToolOutput: updated,
301
+ updatedToolOutput: rebuilt,
258
302
  },
259
303
  }));
260
304
  } catch (e) {} // silent — never break the tool pipeline
@@ -263,4 +307,4 @@ function main() {
263
307
 
264
308
  if (require.main === module) main();
265
309
 
266
- module.exports = { extractText, scrub, compress, transform, limitsFor, toolAllowed, processPayload, THRESHOLDS, SAFE_TOOLS };
310
+ module.exports = { extractText, rebuildResponse, scrub, compress, transform, limitsFor, toolAllowed, processPayload, duplicateMarker, THRESHOLDS, SAFE_TOOLS };
@@ -4,13 +4,35 @@
4
4
  // Resolution order for default mode:
5
5
  // 1. CHISLE_DEFAULT_MODE environment variable
6
6
  // 2. User config: $XDG_CONFIG_HOME/chisle/config.json, ~/.config/chisle/config.json
7
- // 3. 'full'
7
+ // 3. 'on'
8
8
 
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
11
  const os = require('os');
12
12
 
13
- const VALID_MODES = ['off', 'lite', 'full', 'ultra'];
13
+ // One mode. The intensity levels are gone: three dials on a tool whose whole
14
+ // argument is "fewer knobs" was the joke writing itself, and the middle setting
15
+ // was the only one ever benchmarked. 'on' or 'off'.
16
+ const VALID_MODES = ['off', 'on'];
17
+
18
+ // Values that meant something before 3.0.0. Kept only so the upgrade can say
19
+ // "your setting no longer does anything" instead of ignoring it in silence.
20
+ const LEGACY_MODES = ['lite', 'full', 'ultra'];
21
+
22
+ // Returns the stale value and where it came from, or null. Never throws.
23
+ function legacySetting() {
24
+ const env = process.env.CHISLE_DEFAULT_MODE;
25
+ if (env && LEGACY_MODES.includes(String(env).toLowerCase())) {
26
+ return { value: String(env).toLowerCase(), source: 'CHISLE_DEFAULT_MODE' };
27
+ }
28
+ try {
29
+ const p = path.join(getConfigDir(), 'config.json');
30
+ const c = JSON.parse(fs.readFileSync(p, 'utf8'));
31
+ const v = c && c.defaultMode ? String(c.defaultMode).toLowerCase() : null;
32
+ if (v && LEGACY_MODES.includes(v)) return { value: v, source: p };
33
+ } catch (e) {}
34
+ return null;
35
+ }
14
36
 
15
37
  function getClaudeDir() {
16
38
  return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
@@ -48,7 +70,7 @@ function getDefaultMode() {
48
70
  }
49
71
  const userMode = readModeFromConfigFile(path.join(getConfigDir(), 'config.json'));
50
72
  if (userMode) return userMode;
51
- return 'full';
73
+ return 'on';
52
74
  }
53
75
 
54
76
  // Symlink-safe, atomic flag write with 0600 perms.
@@ -149,4 +171,7 @@ function readFlag(flagPath) {
149
171
  }
150
172
  }
151
173
 
152
- module.exports = { getDefaultMode, getClaudeDir, VALID_MODES, safeWriteFlag, readFlag };
174
+ module.exports = {
175
+ LEGACY_MODES, legacySetting, getDefaultMode, getClaudeDir,
176
+ VALID_MODES, safeWriteFlag, readFlag,
177
+ };
@@ -5,7 +5,8 @@
5
5
 
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
- const { getDefaultMode, getClaudeDir, VALID_MODES, 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,68 +17,25 @@ 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
32
- if (/^\/chisle(\b|:chisle\b)/.test(promptLower)) {
33
- const parts = promptLower.split(/\s+/);
34
- const cmd = parts[0];
35
- const arg = parts[1] || '';
36
-
37
- let mode = null;
38
-
39
- if (cmd === '/chisle' || cmd === '/chisle:chisle') {
40
- if (!arg) {
41
- mode = getDefaultMode();
42
- } else if (arg === 'off' || arg === 'stop' || arg === 'disable') {
43
- mode = 'off';
44
- } else if (VALID_MODES.includes(arg)) {
45
- mode = arg;
46
- }
47
- }
48
-
49
- if (mode && mode !== 'off') {
50
- safeWriteFlag(flagPath, mode);
51
- } else if (mode === 'off') {
52
- try { fs.unlinkSync(flagPath); } catch (e) {}
53
- }
54
- }
55
-
56
- // Natural language deactivation.
57
- // Only fire when the off-verb actually targets chisle — NOT when chisle merely
58
- // appears in a sentence that also mentions turning something else off.
59
- // ("use chisle to turn off the logger" must NOT deactivate.)
60
- if (/\b(turn off|disable|deactivate|stop|kill|exit)\s+chisle\b/i.test(promptLower) ||
61
- /\bchisle\s+(mode\s+)?(off|stop|disable|deactivate)\b/i.test(promptLower) ||
62
- /\bnormal mode\b/i.test(promptLower)) {
22
+ if (request === 'off') {
63
23
  try { fs.unlinkSync(flagPath); } catch (e) {}
24
+ } else if (request === 'on') {
25
+ safeWriteFlag(flagPath, 'on');
64
26
  }
65
27
 
66
28
  // Per-turn reinforcement
67
29
  const activeMode = readFlag(flagPath);
68
30
  if (activeMode && activeMode !== 'off') {
69
31
  // Inject compact reminder — keeps chisle visible across context compression
70
- const ladderHint = activeMode === 'ultra'
71
- ? 'YAGNI extremist: delete before add, challenge req in same breath.'
72
- : 'Code: YAGNI ladder first (stdlib → native → dep → one line → min code).';
73
-
74
32
  process.stdout.write(JSON.stringify({
75
33
  hookSpecificOutput: {
76
34
  hookEventName: 'UserPromptSubmit',
77
35
  additionalContext:
78
- 'CHISLE MODE ACTIVE (' + activeMode + '). ' +
36
+ 'CHISLE ACTIVE. ' +
79
37
  'Prose: drop articles/filler/pleasantries/hedging. Fragments OK. ' +
80
- ladderHint + ' ' +
38
+ 'Code: YAGNI ladder first (reuse → stdlib → native → dep → one line → min code). ' +
81
39
  'Code/commits/security: write normal.'
82
40
  }
83
41
  }));
@@ -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 };
@@ -21,10 +21,8 @@ $mode = $raw.Substring(0, [Math]::Min(64, $raw.Length)).ToLower()
21
21
  $mode = ($mode -replace '[^a-z0-9-]', '')
22
22
 
23
23
  switch ($mode) {
24
- 'off' { }
25
- 'lite' { }
26
- 'full' { }
27
- 'ultra' { }
24
+ 'off' { }
25
+ 'on' { }
28
26
  default { exit 0 }
29
27
  }
30
28
 
@@ -47,8 +45,5 @@ if ($sitem -and -not ($sitem.Attributes -band [IO.FileAttributes]::ReparsePoint)
47
45
  }
48
46
  }
49
47
 
50
- if ([string]::IsNullOrEmpty($mode) -or $mode -eq 'full') {
51
- Write-Host -NoNewline "$orange[CHISLE]$reset$suffix"
52
- } else {
53
- Write-Host -NoNewline ("{0}[CHISLE:{1}]{2}{3}" -f $orange, $mode.ToUpper(), $reset, $suffix)
54
- }
48
+ # One mode, one badge.
49
+ Write-Host -NoNewline "$orange[CHISLE]$reset$suffix"
@@ -21,7 +21,7 @@ MODE=$(printf '%s' "$MODE" | tr -cd 'a-z0-9-')
21
21
 
22
22
  # Whitelist — render nothing for unknown values rather than echo attacker bytes.
23
23
  case "$MODE" in
24
- off|lite|full|ultra) ;;
24
+ off|on) ;;
25
25
  *) exit 0 ;;
26
26
  esac
27
27
 
@@ -85,10 +85,5 @@ if [ -f "$STATS" ] && [ ! -L "$STATS" ]; then
85
85
  fi
86
86
  fi
87
87
 
88
- # Orange badge + loading bars trailing outside the bracket
89
- if [ -z "$MODE" ] || [ "$MODE" = "full" ]; then
90
- printf '\033[38;5;172m[CHISLE]\033[0m%s' "$LIMITS"
91
- else
92
- SUFFIX=$(printf '%s' "$MODE" | tr '[:lower:]' '[:upper:]')
93
- printf '\033[38;5;172m[CHISLE:%s]\033[0m%s' "$SUFFIX" "$LIMITS"
94
- fi
88
+ # Orange badge + loading bars trailing outside the bracket. One mode, one badge.
89
+ printf '\033[38;5;172m[CHISLE]\033[0m%s' "$LIMITS"
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "chisle",
3
- "version": "2.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,17 +40,28 @@
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",
48
54
  "check:rules": "node scripts/build-rules.js --check",
49
55
  "build:chart": "node scripts/build-chart.js",
50
56
  "check:chart": "node scripts/build-chart.js --check",
51
- "bench": "bash benchmarks/run-live.sh"
57
+ "bench": "bash benchmarks/run-live.sh",
58
+ "build:samples": "node scripts/build-samples.js",
59
+ "check:samples": "node scripts/build-samples.js --check"
52
60
  },
53
61
  "files": [
54
62
  "bin/",
55
63
  "hooks/",
64
+ "pi-extension/",
56
65
  "skills/",
57
66
  "commands/",
58
67
  "rules/",