v3.7.4: brewtools:think-short — new skill for toggle-able terse-output mode

Injects brevity directives into main conversation (SessionStart) and
sub-agent prompts (PreToolUse:Task). 3 profiles (light/medium/aggressive),
NL parsing (RU+EN), blacklist (debate/docs-writer/architect by default),
atomic state writes with O_NOFOLLOW. Disabled by default.

Files:
- brewtools/skills/think-short/ — new skill (SKILL.md, helpers, profiles, 10 e2e scenarios)
- brewtools/hooks/session-start.mjs, pre-task.mjs — think-short injection
- brewtools/.claude-plugin/plugin.json — config.think_short defaults
- web/docs/src/content/docs/brewtools/skills/think-short.mdx — MDX page
- web/docs/src/utils/navigation.ts — nav entry
- brewdoc/skills/guide/references/topic-skills-catalog.md — catalog sync (brewtools 8→11)
- README.md, brewtools/README.md, RELEASE-NOTES.md — updates
This commit is contained in:
kochetkov-ma
2026-04-20 18:32:13 +01:00
parent 4769af19b4
commit 47bdf6b0e9
34 changed files with 1720 additions and 20 deletions
+5 -5
View File
@@ -6,13 +6,13 @@
},
"metadata": {
"description": "Claude Code plugin suite: brewcode for infinite task execution, brewdoc for documentation tools, brewtools for text utilities, brewui for UI/visual/creative tools",
"version": "3.7.3"
"version": "3.7.4"
},
"plugins": [
{
"name": "brewcode",
"description": "Brewcode - full-featured development platform for Claude Code: infinite focus tasks, prompt optimization, skill/agent creation, quorum reviews, rules management",
"version": "3.7.3",
"version": "3.7.4",
"category": "productivity",
"keywords": [
"brewcode",
@@ -46,7 +46,7 @@
{
"name": "brewdoc",
"description": "Brewdoc - Claude Code documentation tools: auto-sync for skills/agents/rules, my-claude installation docs, memory optimization, md-to-pdf conversion",
"version": "3.7.3",
"version": "3.7.4",
"category": "productivity",
"keywords": [
"brewdoc",
@@ -74,7 +74,7 @@
{
"name": "brewtools",
"description": "Brewtools - universal utilities for Claude Code: text optimization, humanization, secrets scanning",
"version": "3.7.3",
"version": "3.7.4",
"category": "productivity",
"keywords": [
"brewtools",
@@ -102,7 +102,7 @@
{
"name": "brewui",
"description": "Brewui - UI/visual/creative tools for Claude Code: AI image generation, design-to-code conversion",
"version": "3.7.3",
"version": "3.7.4",
"category": "productivity",
"keywords": [
"brewui",
+1
View File
@@ -25,3 +25,4 @@ data/
!web/docs/public/glm-image-badge.png
d2c-output/
.playwright-mcp/
brewtools/skills/think-short/tests/e2e/results/
+3 -2
View File
@@ -58,7 +58,7 @@ After all commands succeed, run `/reload-plugins`. If `/reload-plugins` is unava
|--------|---------|--------|---------|
| [brewcode](brewcode/README.md) | Infinite task execution, quorum reviews, skill/agent creation, semantic search | 13 | `claude plugin install brewcode@claude-brewcode` |
| [brewdoc](brewdoc/README.md) | Documentation tools: auto-sync, memory optimization, PDF conversion, publishing | 6 | `claude plugin install brewdoc@claude-brewcode` |
| [brewtools](brewtools/README.md) | Universal text utilities: token optimization, humanization, secrets scanning, plugin updates | 7 | `claude plugin install brewtools@claude-brewcode` |
| [brewtools](brewtools/README.md) | Universal text utilities: token optimization, humanization, secrets scanning, plugin updates | 11 | `claude plugin install brewtools@claude-brewcode` |
| [brewui](brewui/README.md) | UI/visual tools: AI image generation, design-to-code conversion | 2 | `claude plugin install brewui@claude-brewcode` |
## Installation
@@ -219,12 +219,13 @@ After `/brewcode:setup`, each task follows the cycle: `spec` -> `plan` -> `start
| `/brewdoc:publish` | Publish to brewpage.app -- returns public URL |
| `/brewdoc:guide` | Interactive tutorial for the plugin suite |
### Brewtools (7 skills)
### Brewtools (11 skills)
| Skill | Purpose |
|-------|---------|
| `/brewtools:text-optimize` | LLM token efficiency optimization (30+ rules) |
| `/brewtools:text-human` | Remove AI artifacts, humanize code |
| `/brewtools:think-short` | Toggle terse-output mode -- inject brevity directives via SessionStart + PreToolUse:Task hooks |
| `/brewtools:secrets-scan` | Scan git-tracked files for leaked secrets |
| `/brewtools:ssh` | SSH server management -- connect, configure, deploy |
| `/brewtools:deploy` | GitHub Actions deployment -- workflows, releases, GHCR, CI/CD |
+10
View File
@@ -2,6 +2,16 @@
---
## v3.7.4 (2026-04-20)
> Docs: [brewtools:think-short](https://doc-claude.brewcode.app/brewtools/skills/think-short/)
### brewtools
#### Added
- **think-short:** new skill — toggle terse-output mode for Claude Code sessions. Injects brevity directives into the main conversation (SessionStart hook) and into sub-agent prompts (PreToolUse:Task hook). 3 profiles — `light` / `medium` / `aggressive`. Commands: `on`, `off`, `profile X`, `status`, `blacklist add|remove`. NL parsing (RU+EN): `включись`, `уровень 3`, `агрессивный`, `level 2`, `максимально`, etc. Default blacklist skips agents that need verbose output (`debate`, `docs-writer`, `architect`). State merged via chain `env → project → global → plugin.json → hardcoded`. Atomic state writes (O_NOFOLLOW, 0600). Cache-friendly — SessionStart profile is injected once and cached; PreToolUse:Task prepends first 2 lines of the profile fresh per sub-agent spawn. Disabled by default.
---
## v3.7.3 (2026-04-19)
> Docs: [brewtools:provider-switch](https://doc-claude.brewcode.app/brewtools/skills/provider-switch/)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "brewcode",
"version": "3.7.3",
"version": "3.7.4",
"description": "Brewcode - full-featured development platform for Claude Code: infinite focus tasks, prompt optimization, skill/agent creation, quorum reviews, rules management",
"author": {
"name": "Maksim Kochetkov",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "claude-plugin-brewcode",
"version": "3.7.3",
"version": "3.7.4",
"description": "Infinite task execution with automatic handoff for Claude Code",
"keywords": [
"claude-code",
@@ -36,6 +36,6 @@
},
"claude-plugin": {
"name": "brewcode",
"version": "3.7.3"
"version": "3.7.4"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "brewdoc",
"version": "3.7.3",
"version": "3.7.4",
"description": "Brewdoc - Claude Code documentation tools: auto-sync for skills/agents/rules, my-claude installation docs, memory optimization",
"author": {
"name": "Maksim Kochetkov",
@@ -39,7 +39,7 @@ Documentation tools. Sync, generate, optimize, export, publish.
| `/brewdoc:guide` | Interactive teaching for the plugin suite (this guide) |
| `/brewdoc:publish` | Publish content to brewpage.app — text, markdown, or files |
## Section 3: Brewtools Skills (8)
## Section 3: Brewtools Skills (11)
Universal utilities. Work in any project, no setup needed.
@@ -53,6 +53,9 @@ Universal utilities. Work in any project, no setup needed.
| `/brewtools:debate` | Evidence-based multi-agent debate with Discovery phase and 3 modes |
| `/brewtools:plugin-update` | Check, install, or update brewcode suite plugins from the marketplace |
| `/brewtools:provider-switch` | Configure alternative API providers — Z.ai/GLM, Qwen, MiniMax, OpenRouter |
| `/brewtools:skill-toggle` | Disable/enable individual plugin skills, survives plugin updates |
| `/brewtools:agent-toggle` | Disable/enable individual plugin agents, survives plugin updates |
| `/brewtools:think-short` | Toggle terse-output mode (light/medium/aggressive) to cut token bloat |
These are standalone — no project configuration required. Run them anywhere.
+8 -2
View File
@@ -1,6 +1,6 @@
{
"name": "brewtools",
"version": "3.7.3",
"version": "3.7.4",
"description": "Brewtools - universal utilities for Claude Code: text optimization, humanization, secrets scanning",
"author": {
"name": "Maksim Kochetkov",
@@ -16,5 +16,11 @@
"secrets-scan",
"developer-tools"
],
"skills": "./skills/"
"skills": "./skills/",
"config": {
"think_short": {
"default_enabled": false,
"default_profile": "medium"
}
}
}
+4 -1
View File
@@ -5,7 +5,7 @@
| Field | Value |
|-------|-------|
| Version | 3.6.0 |
| Skills | 10 |
| Skills | 11 |
| Agents | 3 |
## Install
@@ -86,6 +86,7 @@ claude --plugin-dir ./brewtools
| [`/brewtools:provider-switch`](skills/provider-switch/README.md) | Configure alternative API providers (Z.ai/GLM, Qwen, MiniMax, OpenRouter) | opus | `[status\|setup\|help\|<provider>]` |
| [`/brewtools:skill-toggle`](skills/skill-toggle/README.md) | Disable/enable individual plugin skills (survives plugin updates) | sonnet | `<op> [plugin:name] [--scope=global\|project]` |
| [`/brewtools:agent-toggle`](skills/agent-toggle/README.md) | Disable/enable individual plugin agents (survives plugin updates) | sonnet | `<op> [plugin:name] [--scope=global\|project]` |
| [`/brewtools:think-short`](skills/think-short/README.md) | Toggle terse-output mode — cut preamble/filler via SessionStart + PreToolUse:Task injection | sonnet | `[on\|off\|profile <light\|medium\|aggressive>\|status\|blacklist add\|remove <agent>]` |
## Agents
@@ -116,6 +117,7 @@ brewtools/
| +-- provider-switch/ # Alternative API provider management
| +-- skill-toggle/ # Disable/enable individual plugin skills
| +-- agent-toggle/ # Disable/enable individual plugin agents
| +-- think-short/ # Terse-output mode toggle
+-- agents/
+-- text-optimizer.md # Text optimization agent
+-- ssh-admin.md # SSH and server administration
@@ -148,6 +150,7 @@ Full docs: [doc-claude.brewcode.app/brewtools/overview](https://doc-claude.brewc
| Provider Switch | [provider-switch](https://doc-claude.brewcode.app/brewtools/skills/provider-switch/) |
| Skill Toggle | [skill-toggle](https://doc-claude.brewcode.app/brewtools/skills/skill-toggle/) |
| Agent Toggle | [agent-toggle](https://doc-claude.brewcode.app/brewtools/skills/agent-toggle/) |
| Think Short | [think-short](https://doc-claude.brewcode.app/brewtools/skills/think-short/) |
| Release Notes | [RELEASE-NOTES.md](../RELEASE-NOTES.md) |
Author: Maksim Kochetkov | License: MIT
+87 -3
View File
@@ -2,8 +2,81 @@
/**
* PreToolUse hook for Task|Agent tools
* Injects BT_PLUGIN_ROOT into sub-agent prompts
* Optionally prepends think-short profile-lite directive (first 2 non-empty lines)
*/
import { readStdin, output, log } from './lib/utils.mjs';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { readStdin, output, log as rawLog } from './lib/utils.mjs';
import { resolveEffectiveState, log as tsLog } from '../skills/think-short/helpers/state.mjs';
async function tryInjectThinkShort(updatedPrompt, tool_input, cwd, session_id) {
const agent = tool_input.subagent_type;
try {
let state;
try {
state = await resolveEffectiveState(cwd);
} catch (err) {
tsLog('info', `Task(${agent}) — resolve failed (${err.message}), skipping`, cwd, session_id);
return { prompt: updatedPrompt, injected: false };
}
if (state.enabled === false) {
tsLog('info', `Task(${agent}) — SKIP (enabled=false)`, cwd, session_id);
return { prompt: updatedPrompt, injected: false };
}
if (Array.isArray(state.blacklist)) {
const agentShort = typeof agent === 'string' && agent.includes(':')
? agent.split(':').pop()
: agent;
const blocked = state.blacklist.some(b => b === agent || b === agentShort);
if (blocked) {
tsLog('info', `Task(${agent}) — SKIP (agent in blacklist)`, cwd, session_id);
return { prompt: updatedPrompt, injected: false };
}
}
// Resolve profile file path
const profile = state.profile;
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
let profilePath;
if (pluginRoot) {
profilePath = path.join(pluginRoot, 'skills/think-short/profiles', `${profile}.md`);
} else {
// Fallback: derive from this file's URL (hooks/ → up 2 → plugin root)
const here = path.dirname(fileURLToPath(import.meta.url));
profilePath = path.resolve(here, '..', 'skills/think-short/profiles', `${profile}.md`);
}
let content;
try {
content = await readFile(profilePath, 'utf8');
} catch (err) {
tsLog('info', `Task(${agent}) — profile read failed at ${profilePath} (${err.message}), skipping`, cwd, session_id);
return { prompt: updatedPrompt, injected: false };
}
const lines = content.split('\n').map(l => l.trim()).filter(l => l.length > 0).slice(0, 2);
if (lines.length === 0) {
tsLog('info', `Task(${agent}) — profile empty at ${profilePath}, skipping`, cwd, session_id);
return { prompt: updatedPrompt, injected: false };
}
const header = `<!-- think-short:${profile} -->\n${lines.join('\n')}\n\n`;
const newPrompt = `${header}${updatedPrompt}`;
const injectedLen = header.length;
const origLen = updatedPrompt.length;
tsLog('info', `Task(${agent}) — injecting profile-lite (~${Math.ceil(injectedLen / 4)} tok)`, cwd, session_id);
if (process.env.CLAUDE_DEBUG === '1') {
tsLog('info', `original prompt length=${origLen}, injected length=${injectedLen}, total=${origLen + injectedLen}`, cwd, session_id);
}
return { prompt: newPrompt, injected: true };
} catch (err) {
tsLog('info', `Task(${agent}) — unexpected error (${err.message}), skipping`, cwd, session_id);
return { prompt: updatedPrompt, injected: false };
}
}
async function main() {
let cwd = null;
@@ -28,7 +101,18 @@ async function main() {
if (pluginRoot) {
updatedPrompt = `BT_PLUGIN_ROOT=${pluginRoot}\n\n${updatedPrompt}`;
modified = true;
log('debug', '[pre-task]', `Injected BT_PLUGIN_ROOT for ${tool_input.subagent_type}`, cwd, session_id);
rawLog('debug', '[pre-task]', `Injected BT_PLUGIN_ROOT for ${tool_input.subagent_type}`, cwd, session_id);
}
// think-short injection (isolated — never breaks BT_PLUGIN_ROOT)
try {
const result = await tryInjectThinkShort(updatedPrompt, tool_input, cwd, session_id);
if (result.injected) {
updatedPrompt = result.prompt;
modified = true;
}
} catch (err) {
tsLog('info', `Task(${tool_input.subagent_type}) — outer guard caught (${err.message}), skipping`, cwd, session_id);
}
if (modified) {
@@ -46,7 +130,7 @@ async function main() {
output({});
}
} catch (error) {
log('error', '[pre-task]', `Error: ${error.message}`, cwd, session_id);
rawLog('error', '[pre-task]', `Error: ${error.message}`, cwd, session_id);
output({});
}
}
+57 -1
View File
@@ -1,6 +1,60 @@
#!/usr/bin/env node
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { readStdin, output, log } from './lib/utils.mjs';
async function injectThinkShort(baseContext, cwd, session_id) {
try {
const { resolveEffectiveState } = await import('../skills/think-short/helpers/state.mjs');
let state;
try {
state = await resolveEffectiveState(cwd);
} catch (err) {
log('info', '[session-start]', `think-short: state resolve failed (${err.message}), skipping injection`, cwd, session_id);
return baseContext;
}
if (!state || state.enabled !== true) {
log('info', '[session-start]', `think-short: SessionStart — SKIP (enabled=false)`, cwd, session_id);
return baseContext;
}
const profile = state.profile || 'medium';
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT
|| path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const profilePath = path.join(pluginRoot, 'skills/think-short/profiles', `${profile}.md`);
let profileText;
let usedProfile = profile;
try {
profileText = await readFile(profilePath, 'utf8');
} catch {
log('info', '[session-start]', `think-short: failed to read profile ${profilePath}, fallback to light`, cwd, session_id);
const fallbackPath = path.join(pluginRoot, 'skills/think-short/profiles', 'light.md');
try {
profileText = await readFile(fallbackPath, 'utf8');
usedProfile = 'light';
} catch (err2) {
log('warn', '[session-start]', `think-short: fallback light.md read failed (${err2.message}), skipping injection`, cwd, session_id);
return baseContext;
}
}
const approxTokens = Math.ceil(profileText.length / 4);
log('info', '[session-start]', `think-short: SessionStart — injecting profile=${usedProfile} (~${approxTokens} tok)`, cwd, session_id);
if (process.env.CLAUDE_DEBUG === '1') {
const preview = profileText.slice(0, 80).replace(/\n/g, ' ');
log('debug', '[session-start]', `think-short: profile preview = ${preview}`, cwd, session_id);
}
return `${baseContext}\n\n<!-- think-short:${usedProfile} -->\n${profileText}`;
} catch (err) {
log('info', '[session-start]', `think-short: injection block error (${err.message}), skipping`, cwd, session_id);
return baseContext;
}
}
async function main() {
let cwd = null;
let session_id = null;
@@ -13,10 +67,12 @@ async function main() {
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || '';
const sessionShort = session_id?.slice(0, 8) || 'unknown';
const context = pluginRoot
let context = pluginRoot
? `BT_PLUGIN_ROOT=${pluginRoot}\nbrewtools: active | session: ${sessionShort}`
: `brewtools: active | session: ${sessionShort}`;
context = await injectThinkShort(context, cwd, session_id);
output({
systemMessage: `brewtools: ${pluginRoot} | session: ${sessionShort}`,
hookSpecificOutput: {
+57
View File
@@ -0,0 +1,57 @@
# Think-Short
Toggle terse-output mode — cuts preamble and filler via SessionStart + PreToolUse:Task injection.
## Commands
| Command | What it does |
|---------|-------------|
| `/brewtools:think-short on [--scope global\|project]` | Enable terse mode (default scope: project) |
| `/brewtools:think-short off` | Disable terse mode |
| `/brewtools:think-short profile <light\|medium\|aggressive>` | Set compression profile |
| `/brewtools:think-short status` | Print effective state, source, state files, last 10 log lines |
| `/brewtools:think-short blacklist add\|remove <agent>` | Exclude/include agent from terse injection |
## NL prompts (RU+EN)
| Phrase | Resolves to |
|--------|-------------|
| `включи терсный`, `be terse`, `think-short on` | `on` |
| `выключи терсный`, `turn off`, `think-short off` | `off` |
| `лёгкий режим`, `light`, `уровень 1`, `level 1` | `profile light` |
| `средний режим`, `medium`, `уровень 2`, `level 2` | `profile medium` |
| `агрессивный`, `макс`, `aggressive`, `уровень 3`, `level 3` | `profile aggressive` |
| `включись максимально`, `be terse max` | `on` + `profile aggressive` (combo) |
| `что сейчас`, `think-short status` | `status` |
Ambiguous input triggers `AskUserQuestion` with candidate operations.
## State files
| Scope | Path |
|-------|------|
| Global | `~/.claude/plugins/data/brewtools-claude-brewcode/think-short.json` |
| Project | `.claude/brewtools/think-short.json` |
Project state wins over global (merge precedence). Default scope for writes is `project` (silent).
State schema: `{"version":1, "enabled":false, "profile":"medium", "blacklist":["debate","docs-writer","architect"], "updated_at":"ISO"}`
## Profiles
| Profile | Directives | Approx tokens | Typical use |
|---------|-----------|---------------|-------------|
| `light` | Be terse. Results first, reasoning only if asked. | ~10 tokens | Light reduction, keeps reasoning visible |
| `medium` | Be terse. No preamble, no filler. Skip AI phrasings. Direct answers. No sycophancy, no unsolicited alternatives. | ~35 tokens | Balanced — recommended default |
| `aggressive` | Be terse. ASCII only. No preamble, closing fluff, sycophancy, restatement, unsolicited alternatives, "as an AI" framing. Results first. Prefer Edit over Write. | ~60 tokens | Maximum suppression — long automated runs |
## Logs
File: `.claude/brewtools.log`
Prefix: `think-short:`
Every NL resolution and scope selection is logged at INFO level.
## Docs
Full docs: [https://doc-claude.brewcode.app/brewtools/skills/think-short/](https://doc-claude.brewcode.app/brewtools/skills/think-short/)
+267
View File
@@ -0,0 +1,267 @@
---
name: brewtools:think-short
description: |
Toggle terse-output mode for Claude Code — injects brief directives into main conversation (SessionStart) and sub-agent prompts (PreToolUse:Task) to reduce preamble, filler, and AI artifacts. Profiles: light / medium / aggressive. State is global or project-scoped with merge precedence (project > global).
Triggers: "think-short", "терсный режим", "короче отвечай", "reduce tokens mode", "be terse", "think shorter", "токен-экономия", "think-short on", "think-short off", "think-short profile", "think-short status", "think-short blacklist", "включи терсный", "выключи терсный", "сделай покороче", "меньше воды", "уровень 1", "уровень 2", "уровень 3", "level 1", "level 2", "level 3", "макс", "максимум", "максимально", "агрессивный режим", "средний режим", "лёгкий режим", "легкий режим".
<example>
user: "/brewtools:think-short on"
<commentary>Structural `on` — default scope=project, AskUserQuestion to confirm Project/Global.</commentary>
</example>
<example>
user: "включись максимально"
<commentary>NL combo — resolves to `on` + `profile aggressive`. Log the resolution, execute both.</commentary>
</example>
<example>
user: "think-short status"
<commentary>Print effective state, source, state-file existence, plugin defaults, env override, last 10 log lines.</commentary>
</example>
<example>
user: "покороче"
<commentary>Ambiguous (matches `on` OR `profile light`?) — AskUserQuestion to disambiguate.</commentary>
</example>
<example>
user: "уровень 3"
<commentary>Bare level → resolves to `profile aggressive`. Default scope=project (silent). Log resolution.</commentary>
</example>
<example>
user: "level 2"
<commentary>Bare English level → resolves to `profile medium`. Default scope=project (silent).</commentary>
</example>
<example>
user: "макс"
<commentary>Shorthand for aggressive → resolves to `profile aggressive`. Default scope=project (silent).</commentary>
</example>
argument-hint: "[on|off|profile <light|medium|aggressive>|status|blacklist add|remove <agent>] [--scope global|project]"
allowed-tools: Read, Bash, AskUserQuestion
model: sonnet
user-invocable: true
---
# Think-Short
> **Toggle terse-output mode.** Writes state to `$CLAUDE_PLUGIN_DATA/think-short.json` (global) or `.claude/brewtools/think-short.json` (project). Hooks (authored separately) read state and inject profile-specific directives into SessionStart + PreToolUse:Task. This skill ONLY parses intent and mutates state.
<instructions>
## Robustness Rules
| Rule | Applies |
|------|---------|
| Every Bash call ends with `&& echo "OK ..." \|\| echo "FAILED ..."` | ALL |
| Never use `Write`/`Edit` on `~/.claude/*` or `$CLAUDE_PLUGIN_DATA` — use Bash + Node `fs` via helpers | ALL |
| State writes go through `skills/think-short/helpers/safe-write.mjs` (atomic, O_NOFOLLOW, 0600) | P2 |
| State reads go through `skills/think-short/helpers/state.mjs` (merge global+project, project wins) | P0, status |
| NL-prompt resolution ALWAYS logged via `brewtools/hooks/lib/utils.mjs` `log()` at INFO level, prefix `think-short`, to `.claude/brewtools.log` | P0 |
Paths (substitute literally in Bash):
- Global state: `$CLAUDE_PLUGIN_DATA/think-short.json` (fallback: `~/.claude/plugins/data/brewtools-claude-brewcode/think-short.json`)
- Project state: `$PWD/.claude/brewtools/think-short.json`
- State helper: `$CLAUDE_PLUGIN_ROOT/skills/think-short/helpers/state.mjs`
- Safe-write helper: `$CLAUDE_PLUGIN_ROOT/skills/think-short/helpers/safe-write.mjs`
- Log helper: `$CLAUDE_PLUGIN_ROOT/hooks/lib/utils.mjs`
- Log file: `$PWD/.claude/brewtools.log`
State schema:
```json
{"version":1, "enabled":false, "profile":"medium", "blacklist":["debate","docs-writer","architect"], "updated_at":"ISO"}
```
---
## P0: Parse Intent
Parse `$ARGUMENTS` into structured form:
```
{ op: on|off|profile|status|blacklist, profile?: light|medium|aggressive, blacklistOp?: add|remove, agent?: string, scope?: global|project }
```
### Structural match (exact)
| Input | Resolves to |
|-------|-------------|
| `on [--scope global\|project]` | `{op:on, scope}` |
| `off` | `{op:off}` |
| `profile <light\|medium\|aggressive>` | `{op:profile, profile}` |
| `status` | `{op:status}` |
| `blacklist add <agent>` | `{op:blacklist, blacklistOp:add, agent}` |
| `blacklist remove <agent>` | `{op:blacklist, blacklistOp:remove, agent}` |
### NL-prompt fallback (MANDATORY)
If no structural match, treat argument as NL prompt. Algorithm:
1. Trim + lowercase.
2. Tokenize + apply synonym table:
| Regex / keyword | Resolves to |
|-----------------|-------------|
| `включи\|включись\|enable\|активируй\|turn on\|^on$` | `on` |
| `выключи\|выключись\|disable\|отключи\|turn off\|^off$` | `off` |
| `light\|лайт\|лёгкий\|легкий\|уровень 1\|level 1\|\b1\b` | `profile light` |
| `medium\|мид\|средний\|уровень 2\|level 2\|\b2\b` | `profile medium` |
| `aggressive\|агрессив\|агрессивный\|макс\|максимально\|max\|уровень 3\|level 3\|\b3\b` | `profile aggressive` |
| `status\|статус\|как дела\|что сейчас` | `status` |
3. **Combos allowed** — e.g. `включись максимально``on` + `profile aggressive`. Execute BOTH ops in sequence.
4. **Ambiguous** (0 matches OR >1 mutually-exclusive match that isn't a combo) → `AskUserQuestion` with candidate operations as options.
5. **After resolution**, INFO log via utils.mjs:
```
think-short: NL-prompt "<input>" → resolved as <command>
```
**EXECUTE** using Bash tool (resolve + log):
```bash
node --input-type=module -e "
import {log} from '$CLAUDE_PLUGIN_ROOT/hooks/lib/utils.mjs';
log('info', 'think-short:', 'NL-prompt \"INPUT\" → resolved as RESOLVED', process.cwd(), process.env.CLAUDE_SESSION_ID || null);
" && echo "OK log" || echo "FAILED log"
```
Replace `INPUT` and `RESOLVED` literally.
---
## P1: Scope Selection
**Default = `project` scope.** Silent — no AskUserQuestion unless the user explicitly asks for disambiguation.
Rules:
| Signal | Scope |
|--------|-------|
| `--scope global` or `--scope=global` present | `global` |
| `--scope project` or `--scope=project` present | `project` |
| User prompt contains explicit ambiguity ("для всех проектов или только здесь", "global or project?", `--ask-scope`) | Use `AskUserQuestion` — options: Project (default) / Global |
| Otherwise (including `--print` / headless / no tty) | `project` (silent default) |
**ALWAYS log the chosen scope at INFO** using utils.mjs `log()`:
```
think-short: scope=<project|global> (<default|--scope|user-choice>, --scope <not specified|explicit>)
```
For `status` — no scope question (reads merged state). For `blacklist` — defaults to project scope silently.
If user is invoking from a combo (e.g. `включись максимально`) — determine scope ONCE via the rules above, apply to all ops in the combo.
---
## P2: Mutate State
**EXECUTE** using Bash tool (generic pattern — substitute `OP_BLOCK`):
```bash
node --input-type=module -e "
import {readMerged, globalPath, projectPath} from '$CLAUDE_PLUGIN_ROOT/skills/think-short/helpers/state.mjs';
import {safeWrite} from '$CLAUDE_PLUGIN_ROOT/skills/think-short/helpers/safe-write.mjs';
import fs from 'node:fs';
const scope = 'SCOPE'; // 'global' | 'project'
const fp = scope === 'project' ? projectPath(process.cwd()) : globalPath();
// Load existing scope-local state (not merged — we write only this scope)
let st = {version:1, enabled:false, profile:'medium', blacklist:['debate','docs-writer','architect']};
try { if (fs.existsSync(fp)) st = JSON.parse(fs.readFileSync(fp,'utf8')); } catch {}
// OP_BLOCK — one of:
// on: st.enabled = true;
// off: st.enabled = false;
// profile: st.profile = 'PROFILE_VALUE';
// blacklist-add: if (!st.blacklist.includes('AGENT')) st.blacklist.push('AGENT');
// blacklist-remove: st.blacklist = st.blacklist.filter(a => a !== 'AGENT');
st.updated_at = new Date().toISOString();
safeWrite(fp, JSON.stringify(st, null, 2));
console.log(JSON.stringify({scope, file:fp, state:st}));
" && echo "OK mutate" || echo "FAILED mutate"
```
Substitute `SCOPE`, `PROFILE_VALUE`, `AGENT`, and the `OP_BLOCK` line(s) per operation.
On combo ops (`on` + `profile`) run both mutations in a single node invocation to keep `updated_at` atomic.
---
## P3: Status Output
For `op=status` — read merged state + metadata and print this shape:
```
think-short: ENABLED (source: project-state)
profile: medium (source: project-state)
blacklist: [debate, docs-writer, architect]
state files:
project: .claude/brewtools/think-short.json (exists, updated 2026-04-20T12:34:56Z)
global: ~/.claude/plugins/data/brewtools-claude-brewcode/think-short.json (missing)
plugin.json defaults: enabled=false, profile=medium
env override: THINK_SHORT_DEFAULT=(unset)
recent log:
<last 10 lines from .claude/brewtools.log matching `think-short`>
```
**EXECUTE** using Bash tool:
```bash
node --input-type=module -e "
import {readMerged, globalPath, projectPath, effectiveSource} from '$CLAUDE_PLUGIN_ROOT/skills/think-short/helpers/state.mjs';
import fs from 'node:fs';
const g = globalPath(), p = projectPath(process.cwd());
const merged = readMerged(process.cwd());
const gExists = fs.existsSync(g), pExists = fs.existsSync(p);
const envOverride = process.env.THINK_SHORT_DEFAULT || '(unset)';
console.log(JSON.stringify({merged, g, p, gExists, pExists, envOverride,
gMtime: gExists ? fs.statSync(g).mtime.toISOString() : null,
pMtime: pExists ? fs.statSync(p).mtime.toISOString() : null}));
" && echo "OK status" || echo "FAILED status"
# Append last 10 log lines matching think-short
grep 'think-short' .claude/brewtools.log 2>/dev/null | tail -10 || echo "(no log entries)"
```
Render the final output to the user in the shape shown above. Omit sections that are N/A (e.g. no log file).
---
## P4: Notify + Reload Reminder
After mutation (non-status ops), render a compact result:
```
# Think-Short — <op>
Scope: <project|global>
File: <absolute path>
State: enabled=<bool>, profile=<light|medium|aggressive>, blacklist=[...]
> Hooks pick up new state on next SessionStart / PreToolUse:Task — no reload needed.
```
For combo ops, show the final merged state after all mutations.
---
## Sub-operation: blacklist
- `blacklist add <agent>` — append `<agent>` to state.blacklist if absent.
- `blacklist remove <agent>` — remove `<agent>` from state.blacklist if present.
- Scope defaults to **project** (no AskUserQuestion). Override via `--scope=global`.
- Log every mutation at INFO level with prefix `think-short`.
---
## Guards
| Condition | Response |
|-----------|----------|
| `$CLAUDE_PLUGIN_ROOT` unset (likely not inside plugin runtime) | ERROR: `think-short: $CLAUDE_PLUGIN_ROOT missing — skill must run as plugin, not from raw file.` STOP. |
| `helpers/state.mjs` or `helpers/safe-write.mjs` missing | ERROR: `think-short: helpers not installed yet (created by separate task) — cannot read/write state.` STOP. |
| NL prompt matches nothing | AskUserQuestion: "Which action? [on / off / profile light / profile medium / profile aggressive / status / cancel]" |
| NL prompt matches >1 mutually-exclusive op (not a combo) | AskUserQuestion with the matched candidates as options. |
| User picks `cancel` in any AskUserQuestion | Abort. No state mutation. Log at INFO: `think-short: user cancelled`. |
</instructions>
@@ -0,0 +1,59 @@
import fs from 'node:fs/promises';
import fsConstants from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import crypto from 'node:crypto';
/**
* Atomically write JSON to filePath with restrictive perms, symlink-safe.
* @returns {Promise<void>}
*/
export async function safeWriteJson(filePath, obj) {
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
const tempPath = `${filePath}.tmp.${process.pid}.${crypto.randomBytes(6).toString('hex')}`;
// O_NOFOLLOW prevents symlink attacks on the temp path
const flags = fsConstants.constants.O_WRONLY | fsConstants.constants.O_CREAT | fsConstants.constants.O_TRUNC | fsConstants.constants.O_NOFOLLOW;
let handle;
try {
handle = await fs.open(tempPath, flags, 0o600);
await handle.writeFile(JSON.stringify(obj, null, 2) + '\n', 'utf8');
await handle.sync();
await handle.close();
handle = null;
await fs.rename(tempPath, filePath);
} catch (err) {
if (handle) {
try { await handle.close(); } catch {}
}
try { await fs.unlink(tempPath); } catch {}
throw err;
}
}
/**
* Read JSON from filePath. Returns null on ENOENT, throws otherwise.
* @returns {Promise<object|null>}
*/
export async function safeReadJson(filePath) {
const flags = fsConstants.constants.O_RDONLY | fsConstants.constants.O_NOFOLLOW;
let handle;
try {
handle = await fs.open(filePath, flags);
} catch (err) {
if (err.code === 'ENOENT') return null;
throw err;
}
try {
const content = await handle.readFile('utf8');
try {
return JSON.parse(content);
} catch (parseErr) {
throw new Error(`Failed to parse JSON at ${filePath}: ${parseErr.message}`);
}
} finally {
await handle.close();
}
}
@@ -0,0 +1,168 @@
import path from 'node:path';
import os from 'node:os';
import { safeReadJson, safeWriteJson } from './safe-write.mjs';
import { log as utilsLog } from '../../../hooks/lib/utils.mjs';
const HARDCODED = {
enabled: false,
profile: 'medium',
blacklist: ['debate', 'docs-writer', 'architect'],
};
const VALID_PROFILES = ['light', 'medium', 'aggressive'];
const ENV_ON = ['on', 'enable'];
const ENV_OFF = ['off', 'disable'];
export function log(level, message, cwd, sessionId) {
utilsLog(level, 'think-short', message, cwd, sessionId);
}
export function getPaths(cwd) {
const dataRoot = process.env.CLAUDE_PLUGIN_DATA
|| path.join(os.homedir(), '.claude/plugins/data/brewtools-claude-brewcode');
const globalPath = path.join(dataRoot, 'think-short.json');
const projectPath = path.join(cwd, '.claude/brewtools/think-short.json');
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
const pluginJsonPath = pluginRoot ? path.join(pluginRoot, '.claude-plugin/plugin.json') : null;
return { globalPath, projectPath, pluginJsonPath };
}
/**
* @returns {Promise<{enabled:boolean, profile:string}>}
*/
export async function readPluginDefaults(pluginJsonPath) {
const fallback = { enabled: false, profile: 'medium' };
if (!pluginJsonPath) return fallback;
try {
const data = await safeReadJson(pluginJsonPath);
const ts = data && data.config && data.config.think_short;
if (!ts) return fallback;
return {
enabled: typeof ts.default_enabled === 'boolean' ? ts.default_enabled : fallback.enabled,
profile: VALID_PROFILES.includes(ts.default_profile) ? ts.default_profile : fallback.profile,
};
} catch {
return fallback;
}
}
function applyEnvOverride(state, cwd) {
const raw = process.env.THINK_SHORT_DEFAULT;
if (!raw) return { applied: false, field: null };
const val = raw.trim().toLowerCase();
if (ENV_ON.includes(val)) {
state.enabled = true;
return { applied: true, field: 'enabled' };
}
if (ENV_OFF.includes(val)) {
state.enabled = false;
return { applied: true, field: 'enabled' };
}
if (VALID_PROFILES.includes(val)) {
state.profile = val;
return { applied: true, field: 'profile' };
}
log('warn', `Ignoring unknown THINK_SHORT_DEFAULT value: ${raw}`, cwd, null);
return { applied: false, field: null };
}
/**
* @returns {Promise<{enabled:boolean, profile:string, blacklist:string[], sources:object, raw:object}>}
*/
export async function resolveEffectiveState(cwd) {
const { globalPath, projectPath, pluginJsonPath } = getPaths(cwd);
const pluginDefaults = await readPluginDefaults(pluginJsonPath);
const global = await safeReadJson(globalPath);
const project = await safeReadJson(projectPath);
const sources = {
enabled: 'hardcoded',
profile: 'hardcoded',
blacklist: 'hardcoded',
};
const merged = {
enabled: HARDCODED.enabled,
profile: HARDCODED.profile,
blacklist: [...HARDCODED.blacklist],
};
if (pluginDefaults) {
merged.enabled = pluginDefaults.enabled;
merged.profile = pluginDefaults.profile;
sources.enabled = 'plugin.json';
sources.profile = 'plugin.json';
}
if (global && typeof global === 'object') {
if (typeof global.enabled === 'boolean') { merged.enabled = global.enabled; sources.enabled = 'global-state'; }
if (VALID_PROFILES.includes(global.profile)) { merged.profile = global.profile; sources.profile = 'global-state'; }
if (Array.isArray(global.blacklist)) { merged.blacklist = global.blacklist; sources.blacklist = 'global-state'; }
}
if (project && typeof project === 'object') {
if (typeof project.enabled === 'boolean') { merged.enabled = project.enabled; sources.enabled = 'project-state'; }
if (VALID_PROFILES.includes(project.profile)) { merged.profile = project.profile; sources.profile = 'project-state'; }
if (Array.isArray(project.blacklist)) { merged.blacklist = project.blacklist; sources.blacklist = 'project-state'; }
}
const envResult = applyEnvOverride(merged, cwd);
if (envResult.applied) {
sources[envResult.field] = 'env';
}
return {
enabled: merged.enabled,
profile: merged.profile,
blacklist: merged.blacklist,
sources,
raw: {
global,
project,
pluginDefaults,
env: process.env.THINK_SHORT_DEFAULT || null,
},
};
}
async function resolveNonEnvDefaults(cwd) {
const prevEnv = process.env.THINK_SHORT_DEFAULT;
delete process.env.THINK_SHORT_DEFAULT;
try {
return await resolveEffectiveState(cwd);
} finally {
if (prevEnv !== undefined) process.env.THINK_SHORT_DEFAULT = prevEnv;
}
}
/**
* @returns {Promise<{path:string, before:object|null, after:object}>}
*/
export async function writeState(scope, patch, cwd) {
if (scope !== 'global' && scope !== 'project') {
throw new Error(`Invalid scope: ${scope}`);
}
const { globalPath, projectPath } = getPaths(cwd);
const targetPath = scope === 'global' ? globalPath : projectPath;
const before = await safeReadJson(targetPath);
const defaults = await resolveNonEnvDefaults(cwd);
const base = {
version: 1,
enabled: defaults.enabled,
profile: defaults.profile,
blacklist: defaults.blacklist,
};
const after = {
...base,
...(before || {}),
...patch,
version: 1,
updated_at: new Date().toISOString(),
};
await safeWriteJson(targetPath, after);
return { path: targetPath, before, after };
}
@@ -0,0 +1,12 @@
Be terse. ASCII only — no em-dash, no smart quotes.
No preamble ("Let me…", "I'll…", "Sure!").
No closing fluff ("Hope this helps!", "Let me know!").
No sycophantic affirmations ("Great question!", "Absolutely!").
No restatement of the question before answering.
No unsolicited alternatives / refactoring suggestions.
No "as an AI" framing, no disclaimers.
Results first. Reasoning only if explicitly asked.
Minimize chain-of-thought verbalization in output.
Prefer Edit over Write. Diff over full file.
Test before declaring done.
User instructions always override these rules.
@@ -0,0 +1,2 @@
Be terse. No preamble, no filler, no AI phrasings.
Results first, reasoning only if asked.
@@ -0,0 +1,5 @@
Be terse. No preamble, no filler.
Skip "Let me…", "I'll now…", "Great question".
Direct answers — results first, reasoning only if asked.
Minimize chain-of-thought verbalization.
No sycophancy, no unsolicited alternatives.
@@ -0,0 +1,24 @@
# 01a — Toggle on (fresh state)
## Given
- Clean test-project fixture (no pre-existing state files)
- `CLAUDE_PLUGIN_DATA` set to a temp dir (no global state)
- No `THINK_SHORT_DEFAULT` env var
## When
user prompt: `/brewtools:think-short on`
(The skill defaults to `project` scope silently under `--print` — no AskUserQuestion.
The runner pre-seeds no state files, so the skill writes a fresh project state.)
## Then
- claude exits 0
- file `.claude/brewtools/think-short.json` exists in the working dir
- that file contains `"enabled": true`
- log `.claude/brewtools.log` contains a line matching `think-short`
- stdout JSON does not indicate an error
## Assert
ASSERT_EXIT_CODE: 0
ASSERT_STATE_PROJECT_JSON_CONTAINS: "enabled": true
ASSERT_LOG_CONTAINS: think-short
@@ -0,0 +1,21 @@
# 01b — Toggle off (pre-seeded enabled:true)
## Given
- Project state file pre-seeded with `{enabled:true, profile:medium, blacklist:[...]}`
- `CLAUDE_PLUGIN_DATA` set to a temp dir (no global state)
- No `THINK_SHORT_DEFAULT` env var
## When
user prompt: `/brewtools:think-short off`
(The skill defaults to `project` scope silently under `--print`. It flips enabled→false.)
## Then
- claude exits 0
- `.claude/brewtools/think-short.json` exists with `"enabled": false`
- log contains `think-short`
## Assert
ASSERT_EXIT_CODE: 0
ASSERT_STATE_PROJECT_JSON_CONTAINS: "enabled": false
ASSERT_LOG_CONTAINS: think-short
@@ -0,0 +1,21 @@
# 02 — Profile switch
## Given
- Clean test-project fixture
- Project state pre-seeded: `{"version":1,"enabled":true,"profile":"medium","blacklist":["debate","docs-writer","architect"]}`
- No `THINK_SHORT_DEFAULT` env var
## When
user prompt: `/brewtools:think-short profile aggressive`
## Then
- exit 0
- `.claude/brewtools/think-short.json` contains `"profile": "aggressive"`
- `"enabled": true` is preserved (profile switch must not flip enabled)
- log `.claude/brewtools.log` contains `think-short`
## Assert
ASSERT_EXIT_CODE: 0
ASSERT_STATE_PROJECT_JSON_CONTAINS: "profile": "aggressive"
ASSERT_STATE_PROJECT_JSON_CONTAINS: "enabled": true
ASSERT_LOG_CONTAINS: think-short
@@ -0,0 +1,24 @@
# 03 — NL prompt (Russian combo: включись агрессивно)
## Given
- Clean test-project fixture (no state files)
- No `THINK_SHORT_DEFAULT` env var
## When
user prompt: `включись агрессивно`
(NL parser should match `включись``on`, `агрессивно``profile aggressive`.
Skill executes both mutations for project scope.)
## Then
- exit 0
- `.claude/brewtools/think-short.json` contains `"enabled": true`
- `.claude/brewtools/think-short.json` contains `"profile": "aggressive"`
- log contains `think-short: NL-prompt` and `resolved as`
## Assert
ASSERT_EXIT_CODE: 0
ASSERT_STATE_PROJECT_JSON_CONTAINS: "enabled": true
ASSERT_STATE_PROJECT_JSON_CONTAINS: "profile": "aggressive"
ASSERT_LOG_CONTAINS: think-short: NL-prompt
ASSERT_LOG_CONTAINS: resolved as
@@ -0,0 +1,21 @@
# 04 — NL prompt level number (уровень 3)
## Given
- Clean test-project fixture (no state files)
- No `THINK_SHORT_DEFAULT` env var
## When
user prompt: `уровень 3`
(NL parser matches `уровень 3``profile aggressive` per synonym table.)
## Then
- exit 0
- `.claude/brewtools/think-short.json` contains `"profile": "aggressive"`
- log contains `think-short: NL-prompt` and `resolved as`
## Assert
ASSERT_EXIT_CODE: 0
ASSERT_STATE_PROJECT_JSON_CONTAINS: "profile": "aggressive"
ASSERT_LOG_CONTAINS: think-short: NL-prompt
ASSERT_LOG_CONTAINS: resolved as
@@ -0,0 +1,25 @@
# 05 — SessionStart injects profile into additionalContext
## Given
- Clean test-project fixture
- Project state pre-seeded: `{"version":1,"enabled":true,"profile":"medium","blacklist":["debate","docs-writer","architect"]}`
- No `THINK_SHORT_DEFAULT` env var
## When
user prompt: `say hi`
(trivial prompt — just enough to trigger SessionStart hook)
## Then
- exit 0
- log `.claude/brewtools.log` contains `SessionStart — injecting profile=medium`
## Assert
ASSERT_EXIT_CODE: 0
ASSERT_LOG_CONTAINS: SessionStart — injecting profile=medium
## Notes
ALLOW_SKIP_ON_NO_TRIGGER
The hook fires via SessionStart event when claude launches. If the log line is absent it means the
hook did not fire (e.g. plugin not loaded, or session-start.mjs not executed). The runner marks this
as SKIP rather than FAIL when ALLOW_SKIP_ON_NO_TRIGGER is set, since CI may not load the plugin.
@@ -0,0 +1,25 @@
# 06 — Pre-task injects profile-lite into sub-agent prompt
## Given
- Clean test-project fixture
- Project state pre-seeded: `{"version":1,"enabled":true,"profile":"medium","blacklist":["debate","docs-writer","architect"]}`
- No `THINK_SHORT_DEFAULT` env var
## When
user prompt: `Use the Task tool with subagent_type=general-purpose to say the word "hello"`
(causes claude to call the Task tool, triggering PreToolUse:Task hook)
## Then
- exit 0
- log `.claude/brewtools.log` contains `injecting profile-lite`
## Assert
ASSERT_EXIT_CODE: 0
ASSERT_LOG_CONTAINS: injecting profile-lite
## Notes
ALLOW_SKIP_ON_NO_TRIGGER
Whether the Task invocation occurs depends on whether the model complies. If the log line is absent
the runner marks as SKIP. Confirmed triggers: pre-task.mjs fires on PreToolUse for any tool_name
Task or Agent. The log prefix is `think-short` and message contains `injecting profile-lite`.
@@ -0,0 +1,27 @@
# 07 — Blacklist: architect agent skipped
## Given
- Clean test-project fixture
- Project state pre-seeded: `{"version":1,"enabled":true,"profile":"medium","blacklist":["debate","docs-writer","architect"]}`
- No `THINK_SHORT_DEFAULT` env var
## When
user prompt: `Use the Task tool with subagent_type=architect to briefly describe what a plugin is`
(Task tool called with subagent_type=architect, which is in the default blacklist)
## Then
- exit 0
- log `.claude/brewtools.log` contains `SKIP (agent in blacklist)` with `architect` in the surrounding context
- log does NOT contain `injecting profile-lite` for the architect call
## Assert
ASSERT_EXIT_CODE: 0
ASSERT_LOG_CONTAINS: SKIP (agent in blacklist)
ASSERT_LOG_NOT_CONTAINS: architect) — injecting profile-lite
## Notes
ALLOW_SKIP_ON_NO_TRIGGER
The assertion `ASSERT_LOG_NOT_CONTAINS` uses a substring that would only appear if the hook wrongly
injected for the blacklisted agent. If the Task tool was never called, the log lines are absent
entirely — the runner treats this as SKIP rather than FAIL.
@@ -0,0 +1,29 @@
# 08 — Project state overrides global state
## Given
- Clean test-project fixture
- Global state pre-seeded in `$CLAUDE_PLUGIN_DATA/think-short.json`:
`{"version":1,"enabled":false,"profile":"light","blacklist":["debate","docs-writer","architect"]}`
- Project state pre-seeded in `.claude/brewtools/think-short.json`:
`{"version":1,"enabled":true,"profile":"aggressive","blacklist":["debate","docs-writer","architect"]}`
- No `THINK_SHORT_DEFAULT` env var
## When
user prompt: `/brewtools:think-short status`
(reads merged state — project must win over global)
## Then
- exit 0
- stdout contains `enabled` and something indicating true/ENABLED
- stdout contains `aggressive`
- stdout contains `project` (indicating project-state as source)
## Assert
ASSERT_EXIT_CODE: 0
ASSERT_STDOUT_CONTAINS: aggressive
ASSERT_STDOUT_CONTAINS: project
## Notes
The status command reads the merged effective state. Because project-state has `enabled:true` and
`profile:aggressive`, those values must appear in the output. The runner checks stdout from out.json
(the `result` field of claude's JSON output) for the directive strings.
@@ -0,0 +1,46 @@
# 09 — Logging visibility: CLAUDE_DEBUG=1 exposes debug lines
## Given
- Clean test-project fixture
- Project state pre-seeded: `{"version":1,"enabled":true,"profile":"medium","blacklist":["debate","docs-writer","architect"]}`
- `CLAUDE_DEBUG=1` set in environment
## When
user prompt: `say hi`
(trivial prompt — triggers SessionStart hook)
## Then
- exit 0
- log `.claude/brewtools.log` does NOT lack debug detail for the think-short session inject path
(When CLAUDE_DEBUG=1, session-start.mjs logs `think-short: profile preview =` line)
- stdout does not contain error
## Assert
ASSERT_EXIT_CODE: 0
ASSERT_LOG_CONTAINS: think-short: profile preview =
ASSERT_STDOUT_NOT_CONTAINS_REGEX: "error":\s*"
## Notes
ALLOW_SKIP_ON_NO_TRIGGER
The `profile preview` line is only emitted when `CLAUDE_DEBUG === '1'` (see session-start.mjs line 47).
If SessionStart hook does not fire (plugin not loaded), the runner marks as SKIP.
---
# Without CLAUDE_DEBUG (second pass, same scenario dir, fresh log)
## Given
- Same fixture, SAME state file
- `CLAUDE_DEBUG` unset
## When
user prompt: `say hi`
## Then
- log does NOT contain `think-short: profile preview =`
(debug line must be absent when CLAUDE_DEBUG is not set)
## Assert
ASSERT_EXIT_CODE: 0
ASSERT_LOG_NOT_CONTAINS: think-short: profile preview =
@@ -0,0 +1,8 @@
# think-short e2e test fixture
Minimal project root used by `run-e2e.sh` as the working directory for each scenario.
Each test copies this directory into a fresh temp location under `tests/e2e/results/<ts>/<scenario>/`
so that state files and log files are isolated between runs.
No source code is present — only the `.claude/` directory skeleton required by the skill and hooks.
+516
View File
@@ -0,0 +1,516 @@
#!/usr/bin/env bash
# run-e2e.sh — E2E test runner for brewtools:think-short
# Usage:
# ./run-e2e.sh # run all 9 scenarios
# ./run-e2e.sh --dry-run # print plan without invoking claude
# ./run-e2e.sh 03 07 # run specific scenario numbers only
#
# Requirements: bash >=4, jq, node, claude CLI in PATH
# Compatible with zsh (shebang uses bash explicitly).
set -euo pipefail
# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../../.." && pwd)"
BREWTOOLS_DIR="${REPO_ROOT}/brewtools"
FIXTURES_DIR="${SCRIPT_DIR}/fixtures/test-project"
SCENARIOS_DIR="${SCRIPT_DIR}/e2e"
RESULTS_BASE="${SCRIPT_DIR}/e2e/results"
TS="$(date +%Y%m%d-%H%M%S)"
RESULTS_DIR="${RESULTS_BASE}/${TS}"
# ---------------------------------------------------------------------------
# Options
# ---------------------------------------------------------------------------
DRY_RUN=0
FILTER_SCENARIOS=()
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
[0-9][0-9]|[0-9][0-9][a-z]) FILTER_SCENARIOS+=("$arg") ;;
*) echo "Unknown arg: $arg" >&2; exit 1 ;;
esac
done
# ---------------------------------------------------------------------------
# Colour helpers (no-op when not a tty)
# ---------------------------------------------------------------------------
if [ -t 1 ]; then
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; RESET='\033[0m'; BOLD='\033[1m'
else
RED=''; GREEN=''; YELLOW=''; CYAN=''; RESET=''; BOLD=''
fi
pass() { echo -e "${GREEN}PASS${RESET} $*"; }
fail() { echo -e "${RED}FAIL${RESET} $*"; }
skip() { echo -e "${YELLOW}SKIP${RESET} $*"; }
info() { echo -e "${CYAN}INFO${RESET} $*"; }
# ---------------------------------------------------------------------------
# Counters
# ---------------------------------------------------------------------------
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
FAILED_NAMES=()
# ---------------------------------------------------------------------------
# Trap for failures
# ---------------------------------------------------------------------------
CURRENT_SCENARIO="(none)"
trap 'echo -e "\n${RED}TRAP${RESET}: unexpected failure during scenario: ${CURRENT_SCENARIO}" >&2' ERR
# ---------------------------------------------------------------------------
# Parse a single scenario .md file.
# Extracts: WHEN prompt, ASSERT_* directives, ALLOW_SKIP_ON_NO_TRIGGER flag.
# Outputs variables: SCENARIO_PROMPT, ALLOW_SKIP, ASSERTS (array of "KEY:VALUE")
# ---------------------------------------------------------------------------
parse_scenario() {
local md_file="$1"
SCENARIO_PROMPT=""
ALLOW_SKIP=0
ASSERTS=()
local in_when=0
while IFS= read -r line; do
# Detect ## When section
if [[ "$line" =~ ^##[[:space:]]When ]]; then
in_when=1
continue
fi
# Leave When section on next ##
if [[ "$in_when" -eq 1 && "$line" =~ ^##[[:space:]] ]]; then
in_when=0
fi
# Capture prompt line inside When
if [[ "$in_when" -eq 1 && "$line" =~ ^user[[:space:]]prompt:[[:space:]] ]]; then
# Strip leading: user prompt: and surrounding backticks
SCENARIO_PROMPT="${line#*user prompt: }"
SCENARIO_PROMPT="${SCENARIO_PROMPT#\`}"
SCENARIO_PROMPT="${SCENARIO_PROMPT%\`}"
fi
# ASSERT directives
if [[ "$line" =~ ^ASSERT_([A-Z_]+):[[:space:]]*(.*) ]]; then
local key="${BASH_REMATCH[1]}"
local val="${BASH_REMATCH[2]}"
ASSERTS+=("${key}:${val}")
fi
# ALLOW_SKIP_ON_NO_TRIGGER
if [[ "$line" =~ ALLOW_SKIP_ON_NO_TRIGGER ]]; then
ALLOW_SKIP=1
fi
done < "$md_file"
}
# ---------------------------------------------------------------------------
# Evaluate a single ASSERT directive.
# Returns 0=pass, 1=fail, 2=skip (when ALLOW_SKIP and trigger absent)
# ---------------------------------------------------------------------------
evaluate_assert() {
local key="$1"
local val="$2"
local workdir="$3"
local out_json="$4"
local exit_code_actual="$5"
local allow_skip="$6"
local plugin_data_dir="$7"
local project_state="${workdir}/.claude/brewtools/think-short.json"
local global_state="${plugin_data_dir}/think-short.json"
local log_file="${workdir}/.claude/brewtools.log"
case "$key" in
EXIT_CODE)
local expected_code="${val// /}"
if [[ "$exit_code_actual" -eq "$expected_code" ]]; then
return 0
else
echo " ASSERT_EXIT_CODE: expected=${expected_code} actual=${exit_code_actual}" >&2
return 1
fi
;;
STATE_PROJECT_JSON_CONTAINS)
if [[ ! -f "$project_state" ]]; then
if [[ "$allow_skip" -eq 1 ]]; then
echo " ALLOW_SKIP: project state file absent — hook may not have fired" >&2
return 2
fi
echo " ASSERT_STATE_PROJECT_JSON_CONTAINS: file absent: ${project_state}" >&2
return 1
fi
if grep -qF -- "$val" "$project_state"; then
return 0
else
echo " ASSERT_STATE_PROJECT_JSON_CONTAINS: '${val}' not found in ${project_state}" >&2
return 1
fi
;;
STATE_GLOBAL_JSON_CONTAINS)
if [[ ! -f "$global_state" ]]; then
echo " ASSERT_STATE_GLOBAL_JSON_CONTAINS: file absent: ${global_state}" >&2
return 1
fi
if grep -qF -- "$val" "$global_state"; then
return 0
else
echo " ASSERT_STATE_GLOBAL_JSON_CONTAINS: '${val}' not found in ${global_state}" >&2
return 1
fi
;;
LOG_CONTAINS)
if [[ ! -f "$log_file" ]]; then
if [[ "$allow_skip" -eq 1 ]]; then
echo " ALLOW_SKIP: log file absent — hook may not have fired" >&2
return 2
fi
echo " ASSERT_LOG_CONTAINS: log file absent: ${log_file}" >&2
return 1
fi
if grep -qF -- "$val" "$log_file"; then
return 0
else
if [[ "$allow_skip" -eq 1 ]]; then
echo " ALLOW_SKIP: '${val}' not in log — hook may not have fired" >&2
return 2
fi
echo " ASSERT_LOG_CONTAINS: '${val}' not found in ${log_file}" >&2
return 1
fi
;;
LOG_NOT_CONTAINS)
if [[ ! -f "$log_file" ]]; then
# absent log = line definitely not present = pass
return 0
fi
if grep -qF -- "$val" "$log_file"; then
echo " ASSERT_LOG_NOT_CONTAINS: '${val}' was found (unexpected) in ${log_file}" >&2
return 1
else
return 0
fi
;;
STDOUT_CONTAINS)
if [[ ! -f "$out_json" ]]; then
echo " ASSERT_STDOUT_CONTAINS: out.json absent" >&2
return 1
fi
# Extract result field from claude JSON output (may be array of messages)
local stdout_text
stdout_text="$(jq -r '
if type == "array" then
[.[] | if .type == "result" then .result // "" else "" end] | join("\n")
elif type == "object" then
.result // ""
else ""
end' "$out_json" 2>/dev/null || cat "$out_json")"
if echo "$stdout_text" | grep -qF -- "$val"; then
return 0
else
echo " ASSERT_STDOUT_CONTAINS: '${val}' not found in stdout" >&2
return 1
fi
;;
STDOUT_NOT_CONTAINS_REGEX)
if [[ ! -f "$out_json" ]]; then
# no output = not present = pass
return 0
fi
local stdout_text
stdout_text="$(jq -r '
if type == "array" then
[.[] | if .type == "result" then .result // "" else "" end] | join("\n")
elif type == "object" then
.result // ""
else ""
end' "$out_json" 2>/dev/null || cat "$out_json")"
if echo "$stdout_text" | grep -qE -- "$val"; then
echo " ASSERT_STDOUT_NOT_CONTAINS_REGEX: pattern '${val}' matched (unexpected)" >&2
return 1
else
return 0
fi
;;
*)
echo " Unknown assert directive: ASSERT_${key} — skipping" >&2
return 0
;;
esac
}
# ---------------------------------------------------------------------------
# Run one scenario
# ---------------------------------------------------------------------------
run_scenario() {
local md_file="$1"
local scenario_num
scenario_num="$(basename "$md_file" | cut -d'-' -f1)"
local scenario_name
scenario_name="$(basename "$md_file" .md)"
CURRENT_SCENARIO="$scenario_name"
TOTAL=$((TOTAL + 1))
local workdir="${RESULTS_DIR}/${scenario_name}"
local plugin_data_dir="${workdir}/_plugin_data"
local out_json="${workdir}/out.json"
# Parse scenario
parse_scenario "$md_file"
if [[ "$DRY_RUN" -eq 1 ]]; then
echo ""
echo -e "${BOLD}[DRY-RUN] ${scenario_name}${RESET}"
echo " prompt : ${SCENARIO_PROMPT}"
echo " allow_skip: ${ALLOW_SKIP}"
echo " asserts :"
for a in "${ASSERTS[@]}"; do
echo " ASSERT_${a%%:*}: ${a#*:}"
done
PASSED=$((PASSED + 1))
return 0
fi
info "Running ${scenario_name} ..."
# Create isolated workdir
mkdir -p "$workdir" "$plugin_data_dir"
cp -r "${FIXTURES_DIR}/." "${workdir}/"
# -------------------------------------------------------------------------
# Scenario-specific pre-seeding
# -------------------------------------------------------------------------
case "$scenario_num" in
01b)
mkdir -p "${workdir}/.claude/brewtools"
printf '{"version":1,"enabled":true,"profile":"medium","blacklist":["debate","docs-writer","architect"]}\n' \
> "${workdir}/.claude/brewtools/think-short.json"
;;
02)
mkdir -p "${workdir}/.claude/brewtools"
printf '{"version":1,"enabled":true,"profile":"medium","blacklist":["debate","docs-writer","architect"]}\n' \
> "${workdir}/.claude/brewtools/think-short.json"
;;
05|06|07)
mkdir -p "${workdir}/.claude/brewtools"
printf '{"version":1,"enabled":true,"profile":"medium","blacklist":["debate","docs-writer","architect"]}\n' \
> "${workdir}/.claude/brewtools/think-short.json"
;;
08)
# Global off, project on — project must win
mkdir -p "${workdir}/.claude/brewtools"
printf '{"version":1,"enabled":false,"profile":"light","blacklist":["debate","docs-writer","architect"]}\n' \
> "${plugin_data_dir}/think-short.json"
printf '{"version":1,"enabled":true,"profile":"aggressive","blacklist":["debate","docs-writer","architect"]}\n' \
> "${workdir}/.claude/brewtools/think-short.json"
;;
09)
mkdir -p "${workdir}/.claude/brewtools"
printf '{"version":1,"enabled":true,"profile":"medium","blacklist":["debate","docs-writer","architect"]}\n' \
> "${workdir}/.claude/brewtools/think-short.json"
;;
esac
# -------------------------------------------------------------------------
# Build env overrides per scenario
# -------------------------------------------------------------------------
local extra_env=()
extra_env+=("CLAUDE_PLUGIN_DATA=${plugin_data_dir}")
unset THINK_SHORT_DEFAULT 2>/dev/null || true
if [[ "$scenario_num" == "09" ]]; then
extra_env+=("CLAUDE_DEBUG=1")
fi
# -------------------------------------------------------------------------
# Invoke claude
# -------------------------------------------------------------------------
local exit_code=0
if [[ -z "$SCENARIO_PROMPT" ]]; then
echo " WARNING: no prompt found in ${md_file}" >&2
fi
set +e
(
cd "$workdir"
env "${extra_env[@]}" \
claude \
--plugin-dir "${BREWTOOLS_DIR}" \
--print "${SCENARIO_PROMPT}" \
--dangerously-skip-permissions \
--output-format json \
> "${out_json}" 2>"${workdir}/stderr.txt"
)
exit_code=$?
set -e
# -------------------------------------------------------------------------
# Scenario 09 second pass (without CLAUDE_DEBUG)
# -------------------------------------------------------------------------
if [[ "$scenario_num" == "09" ]]; then
local log_file_2="${workdir}/.claude/brewtools.log"
# Remove log so second pass creates a fresh one
rm -f "$log_file_2"
local exit_code_2=0
set +e
(
cd "$workdir"
env "CLAUDE_PLUGIN_DATA=${plugin_data_dir}" \
claude \
--plugin-dir "${BREWTOOLS_DIR}" \
--print "say hi" \
--dangerously-skip-permissions \
--output-format json \
>> "${workdir}/out2.json" 2>>"${workdir}/stderr2.txt"
)
exit_code_2=$?
set -e
# The second-pass assert (LOG_NOT_CONTAINS) uses the fresh log
fi
# -------------------------------------------------------------------------
# Evaluate asserts
# -------------------------------------------------------------------------
local scenario_pass=1
local any_skip=0
for assert_entry in "${ASSERTS[@]}"; do
local ak="${assert_entry%%:*}"
local av="${assert_entry#*:}"
# Trim leading space
av="${av# }"
local result=0
evaluate_assert "$ak" "$av" "$workdir" "$out_json" "$exit_code" "$ALLOW_SKIP" "$plugin_data_dir" || result=$?
if [[ "$result" -eq 2 ]]; then
any_skip=1
elif [[ "$result" -ne 0 ]]; then
scenario_pass=0
fi
done
# -------------------------------------------------------------------------
# Scenario 09: also check second-pass asserts (LOG_NOT_CONTAINS lines)
# The second pass expects profile preview absent — evaluated separately
# -------------------------------------------------------------------------
if [[ "$scenario_num" == "09" ]]; then
local log_file_2="${workdir}/.claude/brewtools.log"
local result_2=0
evaluate_assert "LOG_NOT_CONTAINS" "think-short: profile preview =" \
"$workdir" "${workdir}/out2.json" "$exit_code_2" "0" "$plugin_data_dir" || result_2=$?
if [[ "$result_2" -ne 0 ]]; then
scenario_pass=0
fi
fi
# -------------------------------------------------------------------------
# Report
# -------------------------------------------------------------------------
if [[ "$scenario_pass" -eq 0 ]]; then
fail "${scenario_name} (exit=${exit_code})"
FAILED=$((FAILED + 1))
FAILED_NAMES+=("$scenario_name")
elif [[ "$any_skip" -eq 1 && "$ALLOW_SKIP" -eq 1 ]]; then
skip "${scenario_name} (hook did not fire — marked ALLOW_SKIP_ON_NO_TRIGGER)"
SKIPPED=$((SKIPPED + 1))
else
pass "${scenario_name}"
PASSED=$((PASSED + 1))
fi
}
# ---------------------------------------------------------------------------
# Scenario list (ordered)
# ---------------------------------------------------------------------------
SCENARIO_FILES=()
while IFS= read -r -d '' f; do
SCENARIO_FILES+=("$f")
done < <(find "${SCENARIOS_DIR}" -maxdepth 1 \( -name '[0-9][0-9]-*.md' -o -name '[0-9][0-9][a-z]-*.md' \) -print0 | sort -z)
if [[ "${#SCENARIO_FILES[@]}" -eq 0 ]]; then
echo "No scenario .md files found in ${SCENARIOS_DIR}" >&2
exit 1
fi
# ---------------------------------------------------------------------------
# Pre-flight checks (skip in dry-run)
# ---------------------------------------------------------------------------
if [[ "$DRY_RUN" -eq 0 ]]; then
for cmd in claude jq node; do
if ! command -v "$cmd" &>/dev/null; then
echo "Required tool not found: $cmd" >&2
exit 1
fi
done
mkdir -p "${RESULTS_DIR}"
fi
# ---------------------------------------------------------------------------
# Main loop
# ---------------------------------------------------------------------------
echo ""
echo -e "${BOLD}=== think-short E2E test runner ===${RESET}"
echo "Brewtools dir : ${BREWTOOLS_DIR}"
echo "Results dir : ${RESULTS_DIR}"
echo "Dry-run : ${DRY_RUN}"
echo ""
for md_file in "${SCENARIO_FILES[@]}"; do
num="$(basename "$md_file" | cut -d'-' -f1)"
# Apply filter if provided
if [[ "${#FILTER_SCENARIOS[@]}" -gt 0 ]]; then
local_match=0
for f in "${FILTER_SCENARIOS[@]}"; do
[[ "$f" == "$num" ]] && local_match=1 && break
done
if [[ "$local_match" -eq 0 ]]; then
continue
fi
fi
run_scenario "$md_file"
done
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
echo ""
echo -e "${BOLD}=== Summary ===${RESET}"
printf "Total: %d | " "$TOTAL"
printf "${GREEN}Passed: %d${RESET} | " "$PASSED"
printf "${RED}Failed: %d${RESET} | " "$FAILED"
printf "${YELLOW}Skipped: %d${RESET}\n" "$SKIPPED"
if [[ "${#FAILED_NAMES[@]}" -gt 0 ]]; then
echo ""
echo -e "${RED}Failed scenarios:${RESET}"
for name in "${FAILED_NAMES[@]}"; do
echo " - ${name}"
done
fi
echo ""
if [[ "$DRY_RUN" -eq 1 ]]; then
echo -e "${CYAN}Dry-run complete — no claude invocations made.${RESET}"
exit 0
fi
if [[ "$FAILED" -gt 0 ]]; then
exit 1
fi
exit 0
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "brewui",
"version": "3.7.3",
"version": "3.7.4",
"description": "Brewui - UI/visual/creative tools for Claude Code: AI image generation, design-to-code conversion",
"author": {
"name": "Maksim Kochetkov",
@@ -0,0 +1,178 @@
---
title: "Think Short"
description: "Toggle terse-output mode — inject brevity directives into main conversation and sub-agent prompts. Cuts preamble/filler/AI-artifacts in output-heavy workflows."
order: 3122
---
import { Badge, Callout, Card, CardGrid, Steps, UpdateNotice } from '../../../../components/mdx';
# Think Short — terse mode toggle
<Badge variant="default" text="sonnet" /> <Badge variant="accent" text="session+task" />
<Callout type="caution">
**Output-token bloat kills long workflows.** Preamble ("Let me…"), filler phrases ("Great question!"), unsolicited alternatives, and chain-of-thought verbalization accumulate fast across many sub-agent turns. In a brewcode run with 10+ agents, this can waste thousands of tokens per phase and obscure signal with noise.
</Callout>
<Callout type="tip">
**Three profiles, two injection points.** `light` / `medium` / `aggressive` profiles are injected once per session via SessionStart (cached, no repeat cost) and prepended fresh to every sub-agent prompt via PreToolUse:Task. Agents on the blacklist (`debate`, `docs-writer`, `architect`) are skipped automatically — they need full expressive output.
</Callout>
## Quick reference
| Command | Argument | Action |
|---------|----------|--------|
| `/brewtools:think-short on` | `[--scope global\|project]` | Enable terse mode, default scope = project |
| `/brewtools:think-short off` | — | Disable terse mode |
| `/brewtools:think-short profile <name>` | `light \| medium \| aggressive` | Set active profile |
| `/brewtools:think-short status` | — | Print effective state, sources, last 10 log lines |
| `/brewtools:think-short blacklist add <agent>` | `<agent>` name | Add agent to injection skip-list (e.g. `docs-writer`) |
| `/brewtools:think-short blacklist remove <agent>` | `<agent>` name | Remove agent from injection skip-list |
Default scope for all mutations: `project`. Pass `--scope=global` to write globally.
## When to use
- Output-heavy brewcode runs with many sub-agent spawns — reduces noise per phase
- Long single sessions where preamble accumulates across compacts
- Cutting verbose boilerplate from agents that over-explain before acting
- Enforcing ASCII-only output and no smart-quotes in structured data pipelines
## Examples
**Enable with default profile (medium):**
```
/brewtools:think-short on
```
**Switch to aggressive:**
```
/brewtools:think-short profile aggressive
```
**Natural language — RU combo (resolves to `on` + `profile aggressive`):**
```
включись максимально
```
**Natural language — level shorthand:**
```
уровень 3
агрессивный
макс
level 2
```
**Check current state:**
```
/brewtools:think-short status
```
Expected status output:
```
think-short: ENABLED (source: project-state)
profile: medium (source: project-state)
blacklist: [debate, docs-writer, architect]
state files:
project: .claude/brewtools/think-short.json (exists, updated 2026-04-20T12:34:56Z)
global: ~/.claude/plugins/data/brewtools-claude-brewcode/think-short.json (missing)
plugin.json defaults: enabled=false, profile=medium
env override: THINK_SHORT_DEFAULT=(unset)
recent log:
think-short: NL-prompt "включись максимально" → resolved as on + profile aggressive
```
**Disable:**
```
выключи
/brewtools:think-short off
```
## Profiles
| Profile | Directives | Approx tokens | Typical use |
|---------|-----------|---------------|-------------|
| `light` | Be terse. Results first, reasoning only if asked. | ~10 tokens | Light reduction, keeps reasoning visible |
| `medium` | Be terse. No preamble, no filler. Skip AI phrasings. Direct answers. No sycophancy, no unsolicited alternatives. | ~35 tokens | Balanced — recommended default |
| `aggressive` | Be terse. ASCII only. No preamble, closing fluff, sycophancy, restatement, unsolicited alternatives, "as an AI" framing. Results first. Prefer Edit over Write. | ~60 tokens | Maximum suppression — long automated runs |
## How it works
<Steps>
<li>
<div>
<strong>SessionStart — full profile injection</strong>
<p>When a new session starts, the SessionStart hook reads the effective state. If enabled, it injects the full profile text into <code>additionalContext</code>. This happens once per session — the profile is cached by the model context, so no repeat token cost on subsequent turns.</p>
</div>
</li>
<li>
<div>
<strong>PreToolUse:Task — per-agent prepend</strong>
<p>Before each sub-agent spawn (Task tool), the PreToolUse hook prepends the first 2 lines of the active profile to the agent's prompt. This ensures fresh injection even for agents spawned mid-session after context shifts. Agents in the blacklist are skipped entirely.</p>
</div>
</li>
<li>
<div>
<strong>Blacklist filter</strong>
<p>Agents named <code>debate</code>, <code>docs-writer</code>, and <code>architect</code> are excluded from injection by default. These agents need full expressive output — injecting brevity directives breaks their output quality. The blacklist is configurable via <code>blacklist add/remove</code>.</p>
</div>
</li>
<li>
<div>
<strong>State merge — fallback chain</strong>
<p>Effective state is computed by merging all sources in priority order. The skill reads and writes through <code>helpers/state.mjs</code> and <code>helpers/safe-write.mjs</code> (atomic write, O_NOFOLLOW, mode 0600). Project-scope state wins over global; env override wins over all.</p>
</div>
</li>
</Steps>
## State files
| Scope | Path |
|-------|------|
| Global | `$CLAUDE_PLUGIN_DATA/think-short.json` (fallback: `~/.claude/plugins/data/brewtools-claude-brewcode/think-short.json`) |
| Project | `<cwd>/.claude/brewtools/think-short.json` |
Project scope overrides global field-by-field — `enabled`, `profile`, and `blacklist` can each come from different scopes.
## State schema
```json
{
"version": 1,
"enabled": false,
"profile": "medium",
"blacklist": ["debate", "docs-writer", "architect"],
"updated_at": "2026-04-20T12:00:00.000Z"
}
```
## Fallback chain
1. `THINK_SHORT_DEFAULT` env var (`on` / `off` / profile name) — highest priority
2. Project state: `<cwd>/.claude/brewtools/think-short.json`
3. Global state: `$CLAUDE_PLUGIN_DATA/think-short.json`
4. `plugin.json` config (`config.think_short.default_enabled`, `config.think_short.default_profile`)
5. Hardcoded defaults: `enabled=false`, `profile=medium`, `blacklist=[debate, docs-writer, architect]`
## Injection timeline
SessionStart profile injection fires once and is cached in the session context — no repeat cost. PreToolUse:Task injection fires fresh on every sub-agent spawn, so agents created later in a long session always receive the current profile. If you toggle or change profile mid-session, the new state applies to the next sub-agent spawn immediately; the main-conversation context retains the earlier SessionStart injection until the next session.
## Logging
<Callout type="note">
All state mutations and NL-prompt resolutions are logged at INFO level to `.claude/brewtools.log`. Filter with `grep 'think-short'` to see only think-short events. The `status` command tails the last 10 matching lines automatically.
</Callout>
<CardGrid>
<Card title="Text Optimize" icon="settings" href="/brewtools/skills/text-optimize/">
Complementary token-reduction skill — rewrites prompts and docs for LLM efficiency rather than controlling output verbosity at runtime.
</Card>
<Card title="Brewtools overview" icon="document" href="/brewtools/overview/">
All brewtools skills and agents in one place.
</Card>
<Card title="GitHub source" icon="link" href="https://github.com/kochetkov-ma/claude-brewcode/tree/main/brewtools/skills/think-short">
Source code — skill, hooks integration, profile texts, and state helpers.
</Card>
</CardGrid>
<UpdateNotice />
+1
View File
@@ -83,6 +83,7 @@ export const navigation: NavSection[] = [
{ title: 'ssh', slug: 'brewtools/skills/ssh' },
{ title: 'text-human', slug: 'brewtools/skills/text-human' },
{ title: 'text-optimize', slug: 'brewtools/skills/text-optimize' },
{ title: 'think-short', slug: 'brewtools/skills/think-short' },
],
},
{