Draws each tool call as one compact 'tool_call: Tool(input) - meta' line and hides its result block

A harness-agnostic agent skill stack.
Skills follow the Agent Skills format (SKILL.md + optional scripts/references/assets). One source of truth under skills/. Works with Cursor, Claude Code, Codex, and any other agent that discovers skills the standard way.
Inspired by stacks like pstack, but not locked to a single product.
npx skills add binbandit/bin-stack
Pick specific skills:
npx skills add binbandit/bin-stack --skill file-pr --skill babysit-pr --skill arena --skill bro
git clone https://github.com/binbandit/bin-stack.git ~/Developer/ai/bin-stack
cd ~/Developer/ai/bin-stack
./scripts/link-skills.sh
That links each skill directory into the harness roots you already have (~/.agents/skills, ~/.cursor/skills, ~/.claude/skills, ~/.codex/skills, …). Edit here; every harness sees the same files.
| Skill | When to use |
|---|---|
file-pr | File or update a concise pull request |
babysit-pr | Monitor a PR through review bots and CI |
arena | Run competing attempts, then graft the strongest ideas into one result |
bro | Restate the last message in plain language |
verify | Prove a behavior change on the user surface with revision-bound evidence |
review | Filter adversarial findings by reachable failure and concrete evidence |
Claude Code mods: plugins of function hooks that change how Claude Code looks and behaves. They are Claude Code only.
| Mod | What it does |
|---|---|
glamour-dark | Draws replies as rich markdown in glamour's dark style, one sentence per line |
marathon-footer | Restyles the hints after the mode label under the prompt to match the SLOPOTHON banner: ⏵⏵ auto mode on · ⇧⇥ CYCLE // ← 1 AGENT |
redact-secrets | Swaps secrets for placeholders before Claude reads them or the session logs them; Write and Edit put the real values back |
tool-lines | Draws each tool call as one compact line and hides its result block |
ascii-spinner | Swaps the working line (verb, timer, tokens, effort, tip) for a one-row WEAVEworm rethreading synthsilk, in Sekiguchi colors |
slopothon | Paints a SLOPOTHON lockup over the Claude logo in Marathon's design language: block wordmark, a dot-matrix card naming the model and branch, and a livery strip with a tagline and barcode per session. Fullscreen ("tui": "fullscreen") only; it starts each session with a /slopothon row and the logo shows for ~130ms first. Type /slopothon to redraw it inline |
Install every mod at user scope:
./scripts/install-mods.sh
The script adds mods/ as the bin-stack marketplace and installs each mod from it. Claude Code reads the mods in place from this clone, so an edit here applies on the next session or /reload-plugins. Re-run it after adding a mod.
skills/
file-pr/SKILL.md
babysit-pr/SKILL.md
arena/SKILL.md
bro/SKILL.md
verify/SKILL.md
review/SKILL.md
mods/
.claude-plugin/marketplace.json
glamour-dark/
marathon-footer/
redact-secrets/
tool-lines/
ascii-spinner/
slopothon/
scripts/
link-skills.sh # optional: symlink into local harness skill roots
install-mods.sh # install the Claude Code mods
Canonical skills live in skills/. Harness-specific paths are adapters (symlinks or CLI installs), not forks of the content.
skills/<name>/SKILL.md with required name and description frontmatter.name matching the directory. Lowercase, hyphens only.references/; keep SKILL.md short enough to load on activation.See agentskills.io/specification.
MIT
The verify and review skills adapt selected pstack guidance by Lauren Tan at revision 9f451cf875ad1239912762f67741e8e5ba6ac0f1. Each skill bundles its provenance and the original MIT notice in LICENSE-pstack so attribution travels with individual skill installs.
hooks/register.ts 144 lines1import type { Register } from 'claude-code'
2
3const ENGINE_ROW_INDENT = 4
4const DOT_COLUMN_WIDTH = 2
5const MIN_TEXT_WIDTH = 20
6const MIN_LINE_CHARS = 120
7const MAX_LINE_ROWS = 3
8const MAX_SUBJECT_CHARS = 35
9const UNMEASURED_TERMINAL_COLUMNS = 120
10
11type ToolRow = {
12 tool: string
13 input: unknown
14 output?: unknown
15 isRunning: boolean
16 isErrored: boolean
17 isInterrupted: boolean
18}
19
20const SUBJECT_KEYS_BY_PRIORITY = [
21 'command',
22 'file_path',
23 'notebook_path',
24 'pattern',
25 'url',
26 'query',
27 'skill',
28 'description',
29 'path',
30 'action',
31 'prompt',
32]
33
34const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null
35
36const asOneLine = (text: string) => text.replace(/\s+/g, ' ').trim()
37
38function subjectOf(input: unknown): string {
39 if (!isRecord(input)) return ''
40 const subjectKey = SUBJECT_KEYS_BY_PRIORITY.find(key => typeof input[key] === 'string' && input[key] !== '')
41 if (subjectKey !== undefined) return asOneLine(String(input[subjectKey]))
42 return Object.keys(input).length === 0 ? '' : asOneLine(JSON.stringify(input))
43}
44
45const resultField = (row: ToolRow, name: string): unknown => (isRecord(row.output) ? row.output[name] : undefined)
46
47const printedOutput = (row: ToolRow) => `${resultField(row, 'stdout') ?? ''}\n${resultField(row, 'stderr') ?? ''}`
48
49type MetaProbe = (row: ToolRow) => string | undefined
50
51const META_PROBES: MetaProbe[] = [
52 row => (row.isInterrupted ? 'interrupted' : undefined),
53 row => (row.isErrored && !row.isInterrupted ? 'error' : undefined),
54 row => (resultField(row, 'backgroundTaskId') !== undefined ? 'background' : undefined),
55 row => (resultField(row, 'timedOutAfterMs') !== undefined ? 'timed out' : undefined),
56 row => (/Shell cwd was reset/.test(printedOutput(row)) ? 'shell cwd was reset' : undefined),
57 row => {
58 const note = resultField(row, 'returnCodeInterpretation')
59 return typeof note === 'string' && note !== '' ? note : undefined
60 },
61]
62
63type ToolLine = { tool: string; subject: string; meta: string }
64
65function clip(text: string, room: number): string {
66 if (text.length <= room) return text
67 return room <= 1 ? '…' : text.slice(0, room - 1) + '…'
68}
69
70function lineFor(row: ToolRow, columns: number): ToolLine {
71 const textWidth = Math.max(columns - ENGINE_ROW_INDENT - DOT_COLUMN_WIDTH, MIN_TEXT_WIDTH)
72 const lineBudget = Math.min(Math.max(textWidth, MIN_LINE_CHARS), textWidth * MAX_LINE_ROWS)
73 const meta = META_PROBES.map(probe => probe(row))
74 .filter(note => note !== undefined)
75 .join(', ')
76 const frameChars = `tool_call: ${row.tool}()`.length + (meta === '' ? 0 : ` - ${meta}`.length)
77 const subjectRoom = Math.min(MAX_SUBJECT_CHARS, lineBudget - frameChars)
78 return { tool: row.tool, subject: clip(subjectOf(row.input), subjectRoom), meta }
79}
80
81const hasFailed = (row: ToolRow) => row.isErrored || row.isInterrupted
82
83const hasFinished = (row: ToolRow) => !row.isRunning && row.output !== undefined
84
85function dotColor(row: ToolRow): 'error' | 'success' | 'warning' {
86 if (hasFailed(row)) return 'error'
87 return hasFinished(row) ? 'success' : 'warning'
88}
89
90let isTranscriptExpanded = false
91
92const isCompact = (surface: string) => surface === 'terminal' && !isTranscriptExpanded
93
94export const register: Register = on => {
95 on('ui.render', { component: 'UserMessage', props: { origin: { kind: 'composer' } } }, async ($, e, next) => {
96 if (e.surface === 'terminal' && e.props.isExpanded !== isTranscriptExpanded) {
97 isTranscriptExpanded = e.props.isExpanded
98 $.ui.invalidate('ui.render')
99 }
100 if (!isCompact(e.surface)) return next(e)
101 const { Box } = $.ui.resolve(e)
102 return Box({ flexDirection: 'column', marginBottom: 1, children: [await next(e)] })
103 })
104
105 on('ui.render', { component: 'ToolUse' }, async ($, e, next) => {
106 if (!isCompact(e.surface)) return next(e)
107 const { Box, Text } = $.ui.resolve(e)
108 const line = lineFor(e.props, e.viewport?.columns ?? UNMEASURED_TERMINAL_COLUMNS)
109 const metaStyle = e.props.isErrored ? { color: 'error' } : { dimColor: true }
110 const dotColumn = Box({
111 minWidth: DOT_COLUMN_WIDTH,
112 flexShrink: 0,
113 children: [Text({ color: dotColor(e.props), children: ['●'] })],
114 })
115 const textColumn = Box({
116 flexShrink: 1,
117 children: [
118 Text({
119 wrap: 'wrap',
120 children: [
121 Text({ dimColor: true, children: ['tool_call: '] }),
122 Text({ bold: true, children: [line.tool] }),
123 `(${line.subject})`,
124 ...(line.meta === '' ? [] : [Text({ ...metaStyle, children: [` - ${line.meta}`] })]),
125 ],
126 }),
127 ],
128 })
129 return Box({ flexDirection: 'row', marginLeft: 2, children: [dotColumn, textColumn] })
130 })
131
132 on('ui.render', { component: 'ToolGroup' }, async ($, e, next) => {
133 if (!isCompact(e.surface)) return next(e)
134 const unfoldedGroup = { ...e.props, isExpanded: true }
135 return next({ ...e, props: unfoldedGroup })
136 })
137
138 on('ui.render', { component: 'ToolResult' }, async ($, e, next) => {
139 if (!isCompact(e.surface)) return next(e)
140 const { Box } = $.ui.resolve(e)
141 return Box({})
142 })
143}
144