Skip to content

Latest commit

 

History

History
643 lines (489 loc) · 22.7 KB

File metadata and controls

643 lines (489 loc) · 22.7 KB

mish Showcase: Side-by-Side Comparison

A detailed comparison of LLM tool calls with mish vs without mish (bare shell), using real commands run against the mish codebase (43,294 lines of Rust across 70 source files, 1,311 unit tests).

All output in this document was captured from a single live mish serve session on 2026-03-04. "Without mish" baselines were captured from the same machine in the same build state.


Table of Contents

  1. Test Setup
  2. What mish Exports
  3. Challenge 1: Build a Rust Project
  4. Challenge 2: Run 1,311 Unit Tests
  5. Challenge 3: Error Diagnosis
  6. Challenge 4: Watch-Pattern Filtering
  7. Challenge 5: Background Process Lifecycle
  8. Challenge 6: Structured Output Passthrough
  9. Metrics Summary
  10. Ambient State: The Process Table Digest

Test Setup

Environment:

  • macOS Darwin 24.6.0, Apple Silicon
  • Rust 1.85, cargo, zsh
  • mish v0.1.0 running as MCP server via mish serve
  • Claude Code connected over stdio MCP transport

The challenge: An LLM agent needs to build, test, and debug a 43K-line Rust project. Every tool call consumes tokens. How much context does mish save, and what intelligence does it add?

Methodology: Each challenge runs the same command through both:

  • Without mish: Claude Code's built-in Bash tool (raw shell execution)
  • With mish: The sh_run / sh_spawn / sh_interact MCP tools

We measure: lines returned, structured metadata, error diagnostics, and token efficiency.


What mish Exports

Every mish response is compact, symbol-prefixed text — not JSON. The format is designed for LLM consumption: unambiguous, ~5x fewer tokens than JSON, zero parsing overhead.

Output symbols

Symbol Meaning
+ Success
! Error
Narration / recommendation
Dangerous
~ Diagnostic / suggestion
- Removed / killed

sh_run response anatomy

+ exit:0 1.2s condense (29→16)           ← header: symbol exit elapsed category (total→shown)
   Compiling mish v0.1.0                  ← body: post-processed output (VTE-stripped, deduped, truncated)
warning: field `config` is never read
   Finished `dev` profile target(s)
~ file_exists src/main.rs (modified 2m)   ← enrichment: diagnostics on failure (~ kind message)
→ consider --message-format=json (quieter output)  ← recommendation: suggested flag for next run
[procs] server:running:30s watcher:running:15s     ← digest: ambient process state (MCP only)

The header packs exit code, wall-clock timing, command category, and compression ratio into one line. Body follows without indentation. Enrichment, recommendations, and process digest are appended when present.

Background process responses

sh_spawn:

+ spawned server pid:12345 session:main matched:1.7s
Server listening on port 3000
[procs] server:running:1.7s

sh_interact (status / read_tail / kill):

+ server status running pid:12345 30s session:main
+ server read_tail 5 lines running
- server killed

sh_session (list / create / close):

+ session list
  main /bin/zsh /Users/scott/projects/mish (2 procs)
+ session create test-session ready
+ session close test-session

sh_help: Text reference card (see end of document).


Challenge 1: Build a Rust Project

Command: cargo build

Without mish (Bash tool)

Tool call:  Bash({ command: "cargo build 2>&1" })

Response:   Plain text, 28 lines
   Compiling mish v0.1.0 (/Users/scottmeyer/projects/mish)
warning: field `config` is never read
  --> src/mcp/server.rs:71:5
   |
67 | pub struct McpServer {
   |            --------- field in this struct
...
71 |     config: Arc<MishConfig>,
   |     ^^^^^^
   |
   = note: `#[warn(dead_code)]` on by default

warning: field `shell_path` is never read
  --> src/session/shell.rs:89:5
   |
87 | pub struct ShellProcess {
   |            ------------ field in this struct
88 |     pty: PtyCapture,
89 |     shell_path: String,
   |     ^^^^^^^^^^

warning: function `get_output_tail` is never used
   --> src/tools/sh_spawn.rs:371:4
    |
371 | fn get_output_tail(table: &ProcessTable, alias: &str, max_lines: usize) -> String {
    |    ^^^^^^^^^^^^^^^

warning: `mish` (lib) generated 3 warnings
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.98s

What the LLM gets: Raw compiler output. No exit code (must parse text). No timing. No category. No suggestion for next time.

With mish (sh_run)

Tool call:  sh_run({ cmd: "cargo build 2>&1", timeout: 120 })

Response:   Compact text, 16 lines shown from 29 total
+ exit:0 1.2s condense (29→16)
   Compiling mish v0.1.0 (/Users/scottmeyer/projects/mish)
    Building [=======================> ] .../...: mish
warning: field `config` is never read (x3)
  --> src...:...:5 (x2)
   | (x3)
67 | pub struct McpServer {
   |            --------- field in this struct (x2)
...
71 |     config: Arc<MishConfig>,
   |     ^^^^^^
   = note: `#[warn(dead_code)]` on by default
 (x3)
87 | pub struct ShellProcess {
88 |     pty: PtyCapture,
89 |     shell_path: String,
   |     ^^^^^^^^^^
warning: function `get_output_tail` is never used → src...:... (x2)
→ consider --message-format=json (Consider adding --message-format=json for quieter output)

Comparison

Metric Without mish With mish
Lines returned 28 16
Exit code (must parse text) exit:0 in header
Timing (unknown) 1.2s in header
Category (unknown) condense in header
Compression ratio (unknown) (29→16) in header
Deduplication None Repeated warning structure collapsed with (x3) markers
Next-run suggestion None → consider --message-format=json
Process awareness None Empty (no [procs] line = nothing running)

Token savings: ~43% fewer output lines. But the real value is structured metadata in the header — the LLM knows exit code, timing, category, and compression ratio at a glance.


Challenge 2: Run 1,311 Unit Tests

Command: cargo test --lib

Without mish (Bash tool)

Tool call:  Bash({ command: "cargo test --lib 2>&1" })

Response:   Plain text, 1,342 lines
(last 5 lines shown — full output is 1,342 lines of individual test names)

test tools::sh_spawn::tests::spawn_alias_conflict_error ... ok
test tools::sh_spawn::tests::spawn_invalid_wait_for_regex ... ok

test result: ok. 1311 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 21.21s

What the LLM gets: 1,342 lines of test foo ... ok repeated 1,311 times. All consumed as context tokens. The only useful line is the last one.

With mish (sh_run)

Tool call:  sh_run({ cmd: "cargo test --lib 2>&1", timeout: 120 })

Response:   Compact text, 201 lines shown from 1,341 total
+ exit:0 21.9s condense (1341→201)
warning: field `config` is never read → src...:... (x675)
 (x4)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.06s
     Running unittests src/lib.rs (target/debug/deps/mish-9ff88bb7b4389a71)
test audit::logger::tests::command_record_log_level_is_info ... ok (x2)
test audit::logger::tests::command_none_omitted_from_json ... ok (x2)
test audit::logger::tests::disabled_logger_no_crash ... ok
...
... [262 lines truncated] ...
...
test tools::sh_spawn::tests::spawn_alias_conflict_error ... ok
test tools::sh_spawn::tests::spawn_wait_for_timeout ... ok
→ consider --message-format=json (Consider adding --message-format=json for quieter output)

Comparison

Metric Without mish With mish
Lines returned 1,342 201
Compression 1:1 6.7:1
Exit code (parse last line) exit:0 in header
Duration (parse last line) 21.9s in header
Deduplication None Repeated ... ok patterns collapsed with (xN)
Oreo truncation None [262 lines truncated] — head + tail preserved
Warnings Buried in 1,342 lines Promoted to top, deduplicated

Token savings: ~85% reduction (1,342 → 201 lines). The Oreo truncation keeps the first tests (to see the run start) and last tests (to see the final result), hiding the repetitive middle. Dedup collapses repeated ... ok patterns.


Challenge 3: Error Diagnosis

Command: cp /tmp/nonexistent_source.txt /var/log/readonly_dest/

Without mish (Bash tool)

Tool call:  Bash({ command: "cp /tmp/nonexistent_source.txt /var/log/readonly_dest/ 2>&1" })

Response:   1 line
cp: directory /var/log/readonly_dest does not exist

What the LLM gets: A single error message. To diagnose, it must now make follow-up tool calls:

  1. ls /tmp/nonexistent_source.txt — does source exist?
  2. ls -la /var/log/ — does dest parent exist?
  3. ls /var/log/readonly_dest/ — does dest exist?
  4. stat /var/log/ — what are the permissions?

That's 4 additional round trips before the LLM can reason about the failure.

With mish (sh_run)

Tool call:  sh_run({ cmd: "cp /tmp/nonexistent_source.txt /var/log/readonly_dest/" })

Response:   Compact text with enrichment
! exit:1 153ms narrate (1→1)
cp: directory /var/log/readonly_dest does not exist
~ source nonexistent_source.txt (not found) ✗
~ path /var/log ✓  /var/log/readonly_dest ✗
~ nearest /var/log contains: DiagnosticMessages/, apache2/, asl/, ...
~ nearest /tmp contains: (19 entries)
~ source /tmp/nonexistent_source.txt ✗
~ dest /var/log/readonly_dest/ ✗
~ dest_parent log (1.3 KB, rwxr-xr-x) ✓

Comparison

Metric Without mish With mish
Error message 1 line 1 line (same)
Source file status (unknown — needs follow-up) ~ source ... ✗ not found
Dest path walk (unknown — needs follow-up) ~ path /var/log ✓ /var/log/readonly_dest ✗
Dest parent permissions (unknown — needs follow-up) ~ dest_parent rwxr-xr-x
Nearby files (unknown — needs follow-up) ~ nearest lines (for typo detection)
Follow-up calls needed 4 0
Total round trips 5 1

The enrichment budget is <100ms (153ms total including command execution), read-only, non-speculative. mish pre-fetches exactly the diagnostics the LLM would ask for next — path walks, source/dest existence, permissions, nearest directory contents — and delivers them as ~ lines in the same response.


Challenge 4: Watch-Pattern Filtering

Command: cargo test --lib with watch="warning" and unmatched="drop"

This is the same 1,311-test run from Challenge 2, but now we only want compiler warnings.

Without mish

The LLM would need to:

  1. Run cargo test --lib 2>&1
  2. Receive all 1,342 lines
  3. Parse them in context to find warnings
  4. Or run cargo test --lib 2>&1 | grep warning — but pipes in Claude Code can cause zombie processes

With mish (sh_run + watch)

Tool call:  sh_run({
    cmd: "cargo test --lib 2>&1",
    timeout: 120,
    watch: "warning",
    unmatched: "drop"
})

Response:   2 lines from 1,341
+ exit:0 21.5s condense (1341→2)
warning: field `config` is never read → src...:... (x581)
→ consider --message-format=json (Consider adding --message-format=json for quieter output)

Comparison

Metric Without mish With mish + watch
Lines consumed 1,342 2
Compression 1:1 670:1
Precision Must scan all output Exact regex matches only
Round trips 1 (but wastes tokens) 1 (surgical)

Token savings: 99.9% reduction. The full test suite ran, but only the warning-matched lines were returned. Dedup collapsed 3 structurally similar warnings into one line with an (x581) marker.


Challenge 5: Background Process Lifecycle

An LLM needs to start a long-running process, confirm it's ready, check on it later, and clean it up. Without mish, this requires polling loops and PID tracking.

Without mish

# Step 1: Start background process
Bash({ command: "python3 -c \"...\" &; echo $!" })
# Response: "12345" (must parse PID)

# Step 2: Check if ready (poll loop)
Bash({ command: "..." })  # ← no good way to wait for output pattern

# Step 3: Check status
Bash({ command: "kill -0 12345 && echo running || echo stopped" })

# Step 4: Read output
Bash({ command: "..." })  # ← output was lost, not captured

# Step 5: Kill
Bash({ command: "kill 12345" })

Problems: Output is lost after the process backgrounds. No way to wait for a regex match. Must manually track PIDs. No structured state.

With mish

Step 1: Spawn with wait-for pattern

Tool call:  sh_spawn({
    alias: "watcher",
    cmd: "python3 -c \"import time; [print(f'tick {i}', flush=True) or time.sleep(0.5) for i in range(20)]\"",
    wait_for: "tick 3",
    timeout: 10
})
+ spawned watcher pid:61235 session:main matched:1.7s
tick 3
[procs] watcher:running:1.7s

The process started, and mish polled output until tick 3 appeared (1.7s), then returned immediately. The LLM knows the process is ready without polling.

Step 2: Check status

Tool call:  sh_interact({ alias: "watcher", action: "status" })
+ watcher status running pid:61235 1.7s session:main
[procs] watcher:running:1.7s

Step 3: Read recent output

Tool call:  sh_interact({ alias: "watcher", action: "read_tail", lines: 10 })
+ watcher read_tail 5 lines running
tick 0
tick 1
tick 2
tick 3
[procs] watcher:running:3.2s

Output is captured in a spool — it's never lost, even for background processes.

Step 4: Kill

Tool call:  sh_interact({ alias: "watcher", action: "kill" })
- watcher killed
[procs] watcher:killed:3.2s

Comparison

Capability Without mish With mish
Start + wait for ready Manual poll loop wait_for: "tick 3" — blocks until match
Ready confirmation Parse stdout (if captured) matched:1.7s in header
Time to ready Unknown matched:1.7s
Read output later Lost (backgrounded) Spool captures everything
Process state kill -0 hack running / killed in response
Named references Raw PIDs alias: "watcher"
Cleanup kill <pid> (must track) sh_interact({ alias, action: "kill" })
Ambient awareness None [procs] digest on every response

Challenge 6: Structured Output Passthrough

Command: git log --oneline -20

Without mish (Bash tool)

Tool call:  Bash({ command: "git log --oneline -20" })

Response:   20 lines, plain text
a65f671 docs: rewrite README to match landing page install-first flow
d831c27 fix: use macos-14 runner, add x86_64 musl target for Alpine/Docker
5d6c5be feat: squasher block refactor, grammar dialects, fixture tests, and repo cleanup
fbd81aa feat: OG social card, install script, release workflow, and landing page restructure
f0af768 feat: add ohitsmish.com landing page with GitHub Pages deployment
c29ab39 docs: add SHOWCASE.md with side-by-side mish vs bare shell comparison
fe9728e feat: add ansible/apt/brew/curl/gcc/go/rsync/rustc/ssh/systemctl grammars
4213ba9 feat: raw mode interactivity detection for condense handler (mish-p6.d6)
0af3fb9 feat: add kubectl + terraform TOML grammars with fixture tests
cbf4e7f feat: add jest + webpack TOML grammars with fixture tests
310186b test: verify grammar inheritance flows through classifier end-to-end
b615119 feat: stack trace compression via deferred-line mechanism
bbdabe5 feat: enrich truncation markers with error/warning counts from hidden region
bbbd391 feat: add binary output detection to squasher pipeline
ac3b9c7 feat: add sh_session audit action for MCP session log access
570d851 feat: add signal handling to condense event loop
af92231 feat: wire unified Pipeline into CLI condense handler
78a0ff5 feat: wire unified Pipeline into sh_run, close b1/b2/b3/c3/g4
bac2c8d feat: wire ImplicitDedup into classifier Tier 3 path
101ffe3 feat: extract narrate_output() and parse_structured() pipeline-callable functions

With mish (sh_run)

Tool call:  sh_run({ cmd: "git log --oneline -20" })
+ exit:0 243ms condense (21→20)
a65f671 (HEAD -> main, origin/main) docs: rewrite README to match landing page install-first flow
d831c27 (tag: v0.1.0) fix: use macos-14 runner, add x86_64 musl target for Alpine/Docker
5d6c5be feat: squasher block refactor, grammar dialects, fixture tests, and repo cleanup
fbd81aa feat: OG social card, install script, release workflow, and landing page restructure
f0af768 feat: add ohitsmish.com landing page with GitHub Pages deployment
c29ab39 docs: add SHOWCASE.md with side-by-side mish vs bare shell comparison
fe9728e feat: add ansible/apt/brew/curl/gcc/go/rsync/rustc/ssh/systemctl grammars
4213ba9 feat: raw mode interactivity detection for condense handler (mish-p6.d6)
0af3fb9 feat: add kubectl + terraform TOML grammars with fixture tests
cbf4e7f feat: add jest + webpack TOML grammars with fixture tests (x2)
310186b test: verify grammar inheritance flows through classifier end-to-end
b615119 feat: stack trace compression via deferred-line mechanism
bbdabe5 feat: enrich truncation markers with error/warning counts from hidden region
bbbd391 feat: add binary output detection to squasher pipeline
ac3b9c7 feat: add sh_session audit action for MCP session log access
570d851 feat: add signal handling to condense event loop
af92231 feat: wire unified Pipeline into CLI condense handler
78a0ff5 feat: wire unified Pipeline into sh_run, close b1/b2/b3/c3/g4
bac2c8d feat: wire ImplicitDedup into classifier Tier 3 path

Key difference: mish detected similar commit messages and applied dedup ((x2) marker on the jest + webpack commits). For structured/passthrough commands, mish preserves all content — it doesn't aggressively compress data that's already information-dense. But it still adds the compact header with exit code, timing, category, and line counts.


Metrics Summary

Token Efficiency Across All Challenges

Challenge Raw Lines mish Lines Compression Extra Intelligence
cargo build 28 16 1.8:1 exit, timing, category, recommendation
cargo test (1,311 tests) 1,342 201 6.7:1 exit, timing, Oreo truncation
cargo test + watch 1,342 2 670:1 surgical regex filtering
cp failure 1 1 1:1 7 enrichment diagnostics (saves 4 follow-ups)
git log 20 20 1:1 exit, timing, dedup

What mish Adds (Not Just Compression)

Feature Value
Compact header + exit:0 1.2s condense (29→16) — everything at a glance
Wall-clock timing Elapsed time in every header
Command classification Category in header (condense/narrate/passthrough/structured/interactive/dangerous)
Error enrichment ~ lines with pre-fetched diagnostics eliminate 3-5 follow-up calls per failure
Watch patterns Regex filtering at the source, not in the LLM's context
Deduplication Template-based grouping: Downloading pkg (x100)
Oreo truncation Head + tail preserved, hidden middle scanned for hazards
Hazard markers [262 lines truncated — N errors in hidden region]
Background process management Named aliases, output spools, wait-for-ready
Process table digest [procs] line on every response — ambient awareness
Recommendations → consider flag (reason) — suggested flags for quieter output

Ambient State: The Process Table Digest

Every mish MCP response appends a [procs] line when background processes exist. This gives the LLM ambient awareness of all active processes without polling.

When no processes are running: No [procs] line appears. Zero overhead.

When a background process is active:

[procs] watcher:running:1.7s

Multiple processes:

[procs] server:running:30s watcher:running:15s

After killing a process:

[procs] watcher:killed:3.2s

The digest is always present when non-empty, always free. The LLM never needs to ask "what's running?" — it already knows. This eliminates an entire class of status-polling tool calls.

Digest behavior:

  • Default mode: Changed — only processes modified since last response
  • Caps at 20 entries (keeps all non-terminal + 10 most recent terminal)
  • Terminal entries stubified after client sees them once
  • Sorted by priority: awaiting_inputrunning → terminal

The sh_help Reference Card

Available at any time for context recovery (e.g., after LLM context compaction):

Tool call:  sh_help({})
# mish reference card

## tools
  sh_run
    cmd*: string — Command to execute
    timeout: integer =300 — Seconds before kill
    watch: string — Regex or @preset to filter output
    unmatched: string =keep — Handle non-matching lines when watch is set (keep|drop)
  sh_spawn
    alias*: string — Unique name for this process
    cmd*: string — Command to execute
    wait_for: string — Regex to match before returning success
    timeout: integer =300 — Seconds to wait
  sh_interact
    alias*: string — Target process alias
    action*: string — Action: send | read_tail | signal | kill | status
    input: string — For send: string to write (include \n for enter)
    lines: integer =50 — For read_tail: number of lines
  sh_session
    action*: string — Action: list
  sh_help
    tool: string — Filter to a single tool (sh_run, sh_spawn, sh_interact, sh_session, sh_help)

## squasher max_lines:200 oreo:50/150 max_bytes:65536
## resources sessions:1/5 processes:0/20

Compact text reference card with live resource usage so the LLM knows capacity.


Conclusion

mish is not a shell replacement — it's a context-efficiency layer between the LLM and the shell. The LLM still runs the same commands. But instead of raw text that must be parsed, it gets:

  1. Compact headers on every response (exit code, timing, category, compression ratio)
  2. Compressed output for noisy commands (6.7x–670x reduction)
  3. Pre-fetched diagnostics on errors (~ lines eliminating 3-5 follow-up calls)
  4. Surgical filtering via watch patterns (see only what matters)
  5. Process lifecycle management with named handles and output spools
  6. Ambient state via the [procs] digest (zero-cost awareness)

The result: fewer tokens consumed, fewer round trips, and richer context for decision-making — all in a format that's ~5x more token-efficient than JSON.