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.
- package/.claude-plugin/plugin.json +1 -1
- package/.clinerules/chisle.md +1 -1
- package/.cursor/rules/chisle.mdc +1 -1
- package/.github/copilot-instructions.md +1 -1
- package/.kiro/steering/chisle.md +1 -1
- package/.windsurf/rules/chisle.md +1 -1
- package/AGENTS.md +2 -2
- package/README.md +180 -42
- package/bin/install.js +92 -40
- package/commands/chisle.toml +2 -3
- package/hooks/chisle-activate.js +57 -28
- package/hooks/chisle-compress-output.js +69 -25
- package/hooks/chisle-config.js +29 -4
- package/hooks/chisle-mode-tracker.js +8 -50
- package/hooks/chisle-mode.js +19 -0
- package/hooks/chisle-statusline.ps1 +4 -9
- package/hooks/chisle-statusline.sh +3 -8
- package/package.json +16 -7
- package/pi-extension/index.js +188 -0
- package/rules/chisle-activate.md +1 -1
- package/skills/chisle/SKILL.md +10 -18
- package/skills/chisle-help/SKILL.md +3 -12
package/commands/chisle.toml
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# chisle slash command
|
|
2
2
|
# Maximum-efficiency dev mode
|
|
3
|
-
# Usage: /chisle
|
|
3
|
+
# Usage: /chisle · /chisle off
|
|
4
4
|
|
|
5
5
|
[command]
|
|
6
6
|
name = "chisle"
|
|
7
|
-
description = "Activate chisle
|
|
8
|
-
argument-hint = "[lite|full|ultra]"
|
|
7
|
+
description = "Activate chisle: zero-fluff prose + YAGNI ladder code decisions. /chisle off to stop."
|
package/hooks/chisle-activate.js
CHANGED
|
@@ -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
|
|
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.
|
|
76
|
-
|
|
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
|
-
|
|
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
|
|
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"
|
|
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
|
|
135
|
-
'(
|
|
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
|
-
|
|
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 >
|
|
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
|
|
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
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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(
|
|
70
|
-
const base = THRESHOLDS
|
|
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
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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:
|
|
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 };
|
package/hooks/chisle-config.js
CHANGED
|
@@ -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. '
|
|
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
|
-
|
|
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 '
|
|
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 = {
|
|
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 {
|
|
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
|
|
20
|
+
const request = requestedMode(prompt);
|
|
20
21
|
|
|
21
|
-
|
|
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
|
|
36
|
+
'CHISLE ACTIVE. ' +
|
|
79
37
|
'Prose: drop articles/filler/pleasantries/hedging. Fragments OK. ' +
|
|
80
|
-
|
|
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
|
-
'
|
|
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
|
-
|
|
51
|
-
|
|
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|
|
|
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
|
-
|
|
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": "
|
|
4
|
-
"description": "Maximum-efficiency dev mode for
|
|
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/",
|