Effort toggle experiment.

An opinionated collection of Claude Code plugins, primarily developed for internal team use. Public so colleagues and collaborators can install directly.
These plugins reflect specific workflows and tool choices -- they may not suit every setup. Feel free to fork and adapt.
/plugin marketplace add mareurs/claude-plugins
/plugin install sdd@claude-plugins
/plugin install codescout-companion@claude-plugins
/plugin install buddy@claude-plugins
| Plugin | Version | Description |
|---|---|---|
| sdd | 2.4.2 | Specification-Driven Development: governance, workflow commands, and enforcement hooks |
| codescout-companion | 1.20.20 | Companion plugin for codescout MCP server: injects tool guidance, redirects Read/Grep/Glob/Edit/Write to symbol-aware tools, auto-reindex + drift warnings, worktree shared-asset symlinking |
| statusline | 1.2.1 | Rich, color-coded terminal status line: model, context %, rate limits (incl. per-model weekly), git info, duration. Self-heals orphan statusLine settings when sibling plugins are uninstalled. |
| buddy | 0.11.13 | Himalayan-aesthetic bodhisattva companion: 12 specialist masters on demand, AI judge, focus tracking, statusline integration |
| Pi harness | 0.1.0 | Companion for pi: Codescout routing, skill-load tracker, recon badge, MCP status widget, and local read-only session snapshots |
| session-bridge | 0.1.0 | Cross-session MCP bridge: ask one Claude Code session a question from another, answered in its loaded context. Rust MCP server, bash SessionStart/Stop hooks. |
| legible | 0.2.0 | Output style legible:Explanatory STE: the Explanatory style (★ Insight blocks) plus about 80% of the ASD-STE100 writing rules, so prose is easier to read and check. |
| Plugin | Additional Requirements |
|---|---|
| sdd | None (core workflow). Serena MCP for /drift and /document. |
| codescout-companion | codescout MCP server configured in .mcp.json or globally. |
For pi users, this repo ships a harness adapter in harnesses/pi/. The harnesses/ directory is the home for non-Claude-Code integrations; future Codex, GitHub Copilot, and other harness adapters belong alongside Pi there.
Full setup involves three components that layer on top of each other:
Quick install (claude-plugins part):
pi install npm:pi-mcp-adapter # once — adds pi-mcp-adapter package
git clone https://github.com/mareurs/claude-plugins
cd claude-plugins/harnesses/pi
./install.sh
Then configure ~/.pi/agent/mcp.json with your codescout binary path and any API keys, and run /mcp reconnect codescout once in pi to warm the directTools cache.
What you get:
cs: reconnaissance [recon F2/W1], skills: debugging-yeti, pdf …, MCP: 2/2 codescout ● researcher ●codescout-companion/skills, buddy/skills, sdd/skills) wired into pi — no duplicationsymbols, read_file, edit_code, …) available as first-class pi tools (not proxied)/skill:research-web, /skill:research-subagent) ready with researcher-mcpSee harnesses/pi/README.md for the full step-by-step install, mcp.json examples, extension details, and skill reference.
pi supports an Anthropic-Messages-compatible proxy for any provider via ~/.pi/agent/models.json. Two patterns worth knowing:
Single upstream (Anthropic direct):
{
"providers": {
"anthropic": { "baseUrl": "http://localhost:8082" }
}
}
Multiple upstreams through one proxy (Anthropic + GitHub Copilot): if your proxy has a fixed upstream, use a per-request X-Proxy-Upstream header so the proxy can route to the right backend. The header is stripped before forwarding. GitHub Copilot models already speak the Anthropic Messages API, so the same proxy serves both.
{
"providers": {
"anthropic": { "baseUrl": "http://localhost:8082" },
"github-copilot": {
"baseUrl": "http://localhost:8082",
"headers": { "X-Proxy-Upstream": "https://api.individual.githubcopilot.com" }
}
}
}
See docs/pi-integration.md for the full setup: proxy implementation, local model providers, skill versioning, and the X-Proxy-Upstream contract.
A methodology where code follows specifications. Every feature starts with a clear definition of what before diving into how.
Commands: /specify, /plan, /review, /drift, /document, /bootstrap-docs, /sdd-init
Skills: sdd-flow (full lifecycle orchestration)
Hooks: spec-guard, review-guard, subagent-inject, session-start
See sdd/README.md for full documentation.
Companion plugin for codescout MCP server. Routes Claude to use codescout's symbol-aware tools instead of Read/Grep/Glob. Auto-detects codescout from .mcp.json, ~/.claude/.claude.json, ~/.claude/settings.json, or ~/.claude.json.
Hooks:
See codescout-companion/README.md for details and configuration.
The Pi harness integrates Codescout as Pi's primary code-intelligence layer. It owns three TypeScript extensions, the local session bridge, and an AGENTS.md routing guide.
Extensions:
codescout-companion.ts (harnesses/pi/) — widget below editor: skill-load tracker, recon badge, MCP server statussecret-guard.ts (harnesses/pi/) — fail-closed shell-egress guardsession-bridge.ts (harnesses/pi/) — local, read-only Pi session snapshotscodescout-mode.ts (codescout-companion/.pi/) — packaged routing adapter that routes Pi native read/edit/write and redundant source shell commands to Codescout when replacement tools are availableSkills wired into pi: reconnaissance, explore-project, 12 buddy specialists, sdd-flow, researcher-mcp, research-web, research-subagent, tracker-hygiene
MCP: codescout tools surface as first-class pi tools via directTools (no mcp__codescout__ prefix). researcher-mcp optional.
See harnesses/pi/README.md for full install instructions.
Add to your project's .claude/settings.json so all team members get the plugins automatically:
{
"extraKnownMarketplaces": {
"claude-plugins": {
"source": {
"source": "github",
"repo": "mareurs/claude-plugins"
}
}
},
"enabledPlugins": {
"sdd@claude-plugins": true,
"codescout-companion@claude-plugins": true,
"buddy@claude-plugins": true
}
}
When team members trust the repository folder, Claude Code automatically installs the marketplace and plugins.
Working on this repo? Install the local git hooks once per clone:
./scripts/install-hooks.sh
They are not synced by git (nothing is, in .git/hooks/), so a fresh clone has none. The pre-push shim carries force-push protection on main and a version-bump parity check that stops a hand-bumped version reaching main while the installed profiles still disagree about it. ./tests/run-all.sh warns when they are absent.
MIT
hooks/register.ts 233 lines1import type { Register } from 'claude-code'
2
3// effort-toggle: the mod of the effort-toggle experiment (spec 2026-10-17-effort-toggle-design.md, "The mod").
4//
5// One config file, `${$.plugin.root}/config.json`, written by `writeHookCopy` once per batch (not by the stage step),
6// picks the mode:
7// toggle registers the set_effort tool; the model's last valid choice is the effort of every later main step.
8// placebo registers the same tool and answers the same text, but rewrites no step (the tool does nothing).
9// random registers the same tool and answers the same text; the effort of the main steps follows a seeded
10// two-state chain, independent of what the model asks (randomLevels below is that chain).
11// schedule registers no tool; request number n asks schedule[min(n, length - 1)].
12// Levels are exactly 'low' and 'xhigh'. A "request" is a request of the MAIN loop, counted across the whole session
13// from 0 (turn.step's `index` is per turn and is not used). Subagent steps (agentId set) never advance the count and
14// get the current main level, which is none until the first main step has chosen one.
15//
16// Log: one JSON line per event at config.logPath, the whole file rewritten after each line because $.fs.write has no
17// append. A log failure never breaks a request or a tool answer. A missing or malformed config registers nothing,
18// rewrites nothing and leaves one `error` line at FALLBACK_LOG.
19// { type: 'step', request: number | null, index, agentId: string | null, turnId, before, after } index = the engine's
20// per-turn step index (it restarts every turn; `request` does not); before = the effort the step carried, after =
21// the effort the request carries (equal to before when not rewritten); null only if it had none
22// { type: 'call', request, agentId: string | null, level, reason, text } text = the result text the tool answered
23// { type: 'invalid', request, agentId: string | null, input } input without the reserved keys (tool, tool_use_id,
24// agentId, consent)
25// { type: 'error', message }
26//
27// This file is loaded by the engine AND imported by Node in mod.test.mjs, so it is erasable TypeScript only (type
28// annotations and `import type`: no enums, namespaces or parameter properties). State lives in `register`'s closure.
29
30export const TOOL_NAME = 'set_effort'
31export const FULL_TOOL_NAME = 'mcp__effort-toggle__set_effort'
32export const FALLBACK_LOG = '/run/state/effort-toggle/log.jsonl'
33export const TOOL_DESCRIPTION =
34 'You start at low reasoning effort. Call this tool to change your effort for your next steps. Reviewing and verifying your work need higher effort: switch to xhigh before you review a change or run your final checks. When you judge that low effort is enough for what comes next, switch back to low.'
35export const INPUT_SCHEMA = {
36 type: 'object',
37 properties: { level: { type: 'string', enum: ['low', 'xhigh'] }, reason: { type: 'string' } },
38 required: ['level', 'reason'],
39}
40const DENY_TEXT = 'set_effort needs level "low" or "xhigh" and a non-empty reason.'
41
42type Level = 'low' | 'xhigh'
43type RandomParams = { pUp: number; pDown: number; seed: number }
44type ModConfig =
45 | { mode: 'toggle' | 'placebo'; logPath: string }
46 | { mode: 'schedule'; schedule: Level[]; logPath: string }
47 | { mode: 'random'; random: RandomParams; logPath: string }
48
49export function resultText(level: Level): string {
50 return `Effort is now ${level}. It applies from your next step.`
51}
52
53// FNV-1a over the UTF-16 code units of `s`, as an unsigned 32-bit number.
54function fnv1a32(s: string): number {
55 let h = 0x811c9dc5
56 for (let i = 0; i < s.length; i += 1) {
57 h ^= s.charCodeAt(i)
58 h = Math.imul(h, 0x01000193)
59 }
60 return h >>> 0
61}
62
63// mulberry32: a 32-bit generator, a number in [0, 1) per call.
64function mulberry32(seed: number): () => number {
65 let a = seed | 0
66 return () => {
67 a = (a + 0x6d2b79f5) | 0
68 let t = Math.imul(a ^ (a >>> 15), 1 | a)
69 t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
70 return ((t ^ (t >>> 14)) >>> 0) / 4294967296
71 }
72}
73
74// The random mode's chain, one level per call: the first is 'low'; each later call draws one number u and switches
75// low -> xhigh when u < pUp, xhigh -> low when u < pDown. The generator is seeded with seed ^ fnv1a32(turnId).
76function levelChain(pUp: number, pDown: number, seed: number, turnId: string): () => Level {
77 const rand = mulberry32(seed ^ fnv1a32(turnId))
78 let cur: Level | null = null
79 return () => {
80 if (cur === null) {
81 cur = 'low'
82 } else {
83 const u = rand()
84 if (cur === 'low' && u < pUp) cur = 'xhigh'
85 else if (cur === 'xhigh' && u < pDown) cur = 'low'
86 }
87 return cur
88 }
89}
90
91// The levels of main requests 0 .. n - 1 of a random arm whose session began with turn `turnId`.
92export function randomLevels(pUp: number, pDown: number, seed: number, turnId: string, n: number): Level[] {
93 const next = levelChain(pUp, pDown, seed, turnId)
94 const out: Level[] = []
95 for (let i = 0; i < n; i += 1) out.push(next())
96 return out
97}
98
99function isLevel(x: unknown): x is Level {
100 return x === 'low' || x === 'xhigh'
101}
102
103function isProbability(x: unknown): x is number {
104 return typeof x === 'number' && Number.isFinite(x) && x >= 0 && x <= 1
105}
106
107function parseConfig(text: string): ModConfig {
108 let raw: any
109 try {
110 raw = JSON.parse(text)
111 } catch (err) {
112 throw new Error(`config.json is not JSON: ${err instanceof Error ? err.message : String(err)}`)
113 }
114 if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('config.json is not an object')
115 if (typeof raw.logPath !== 'string' || raw.logPath === '') throw new Error('config.json: logPath must be a non-empty string')
116 const logPath: string = raw.logPath
117 switch (raw.mode) {
118 case 'toggle':
119 case 'placebo':
120 return { mode: raw.mode, logPath }
121 case 'schedule': {
122 const s = raw.schedule
123 if (!Array.isArray(s) || s.length === 0 || !s.every(isLevel)) {
124 throw new Error('config.json: schedule must be a non-empty list of "low" and "xhigh"')
125 }
126 return { mode: 'schedule', schedule: s.slice(), logPath }
127 }
128 case 'random': {
129 const r = raw.random
130 if (r === null || typeof r !== 'object' || !isProbability(r.pUp) || !isProbability(r.pDown) || !Number.isSafeInteger(r.seed)) {
131 throw new Error('config.json: random needs pUp and pDown in [0, 1] and an integer seed')
132 }
133 return { mode: 'random', random: { pUp: r.pUp, pDown: r.pDown, seed: r.seed }, logPath }
134 }
135 default:
136 throw new Error(`config.json: unknown mode ${JSON.stringify(raw.mode)}`)
137 }
138}
139
140type FsWriter = { fs: { write: (path: string, text: string) => Promise<unknown> } }
141type Sink = { path: string; lines: string[]; writing: Promise<void> }
142
143// Appends a line and rewrites the file. Writes are serialised, and each writes every line so far, so a failed write is
144// repaired by the next one and parallel callers lose no line. Never rejects. A top-level function, because the engine
145// follows `$` only into functions declared at the top of this file.
146function appendLog($: FsWriter, sink: Sink, line: object): Promise<void> {
147 sink.lines.push(JSON.stringify(line))
148 sink.writing = sink.writing
149 .then(() => $.fs.write(sink.path, sink.lines.join('\n') + '\n'))
150 .then(
151 () => undefined,
152 () => undefined,
153 )
154 return sink.writing
155}
156
157export const register: Register = on => {
158 let cfg: ModConfig | null = null
159 const sink: Sink = { path: FALLBACK_LOG, lines: [], writing: Promise.resolve() }
160 let requests = 0 // main requests taken so far; the next main step is request number `requests`
161 let mainLevel: Level | null = null // the level of the main loop now; null until one is chosen
162 let chain: (() => Level) | null = null
163
164 on('session.start', async ($, e, next) => {
165 try {
166 let text: string
167 try {
168 text = await $.fs.read(`${$.plugin.root}/config.json`)
169 } catch (err) {
170 throw new Error(`cannot read config.json: ${err instanceof Error ? err.message : String(err)}`)
171 }
172 const parsed = parseConfig(text)
173 if (parsed.mode !== 'schedule') {
174 try {
175 await $.tool.register({ name: TOOL_NAME, description: TOOL_DESCRIPTION, inputSchema: INPUT_SCHEMA })
176 } catch (err) {
177 throw new Error(`tool.register failed: ${err instanceof Error ? err.message : String(err)}`)
178 }
179 }
180 cfg = parsed
181 sink.path = parsed.logPath
182 } catch (err) {
183 cfg = null
184 await appendLog($, sink, { type: 'error', message: err instanceof Error ? err.message : String(err) })
185 }
186 return next(e)
187 })
188
189 on('tool.call', { tool: FULL_TOOL_NAME }, async ($, e, next) => {
190 if (cfg === null) return next(e)
191 const { tool: _tool, tool_use_id: _id, agentId: rawAgent, consent: _consent, ...input } = e as Record<string, unknown>
192 const agentId = typeof rawAgent === 'string' ? rawAgent : null
193 const request = Math.max(requests - 1, 0) // the main request whose response made this call
194 const { level, reason } = input
195 if (!isLevel(level) || typeof reason !== 'string' || reason.trim() === '') {
196 await appendLog($, sink, { type: 'invalid', request, agentId, input })
197 return { deny: DENY_TEXT }
198 }
199 // Set before any await, so calls answered in one message apply in the order their hooks start: the last one wins.
200 if (cfg.mode === 'toggle') mainLevel = level
201 const text = resultText(level)
202 await appendLog($, sink, { type: 'call', request, agentId, level, reason, text })
203 return { result: text }
204 })
205
206 on('turn.step', async function* ($, e, next) {
207 if (cfg === null) return yield* next(e)
208 let request: number | null = null
209 if (typeof e.agentId !== 'string') {
210 request = requests
211 requests += 1
212 if (cfg.mode === 'schedule') {
213 mainLevel = cfg.schedule[Math.min(request, cfg.schedule.length - 1)]
214 } else if (cfg.mode === 'random') {
215 chain ??= levelChain(cfg.random.pUp, cfg.random.pDown, cfg.random.seed, e.turnId)
216 mainLevel = chain()
217 }
218 }
219 const rewrite = mainLevel !== null && mainLevel !== e.effort
220 const sent = rewrite ? { ...e, effort: mainLevel as Level } : e
221 await appendLog($, sink, {
222 type: 'step',
223 request,
224 index: e.index,
225 agentId: typeof e.agentId === 'string' ? e.agentId : null,
226 turnId: e.turnId,
227 before: e.effort ?? null,
228 after: sent.effort ?? null,
229 })
230 return yield* next(sent)
231 })
232}
233