Skip to content
Development
Skill

/writing-user-outputs

CLI output formatting standards for worktrunk. Load before editing any code that calls warning_message, hint_message, error_message, info_message, eprintln, or println, or that produces strings the user will see (CLI help, progress UI, snapshot text). Documents ANSI color

From plugin
worktrunk
6.4k7 skills3 hooks
Install
$ npx -y skills add max-sixty/worktrunk --skill writing-user-outputs --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/writing-user-outputs

Context preview

The summary Claude sees to decide when to auto-load this skill.

CLI output formatting standards for worktrunk. Load before editing any code that calls warning_message, hint_message, error_message, info_message, eprintln, or println, or that produces strings the user will see (CLI help, progress UI, snapshot text). Documents ANSI color

SKILL.md

writing-user-outputs.SKILL.md
name: writing-user-outputs
description: CLI output formatting standards for worktrunk. Load before editing any code that calls warning_message, hint_message, error_message, info_message, eprintln, or println, or that produces strings the user will see (CLI help, progress UI, snapshot text). Documents ANSI color nesting rules, message patterns, and output system architecture.
metadata:
  internal: true

Output System Architecture

Shell Integration

Worktrunk uses split file-based directive passing for shell integration:

1. Shell wrapper creates two temp files via `mktemp` (cd and exec) 2. Shell wrapper sets `WORKTRUNK_DIRECTIVE_CD_FILE` and `WORKTRUNK_DIRECTIVE_EXEC_FILE` 3. wt writes a raw path to the CD file; shell commands to the EXEC file (for `--execute`) 4. Shell wrapper reads the CD file with `cd -- "$(< file)"` (no shell parsing) 5. Shell wrapper sources the EXEC file if non-empty

When neither directive env var is set (direct binary call), commands execute directly and shell integration hints are shown.

Output Functions

The output system handles shell integration automatically. Just call output functions — they do the right thing regardless of whether shell integration is active.

// NEVER DO THIS - don't check mode in command code
if is_shell_integration_active() {
    // different behavior
}

// ALWAYS DO THIS - just call output functions
eprintln!("{}", success_message("Created worktree"));
output::change_directory(&path)?;  // Writes to directive file if set, else no-op

**Printing output:**

Use `eprintln!` and `println!` from `worktrunk::styling` (re-exported from `anstream` for automatic color support and TTY detection):

use worktrunk::styling::{eprintln, println, stderr};

// Status messages to stderr
eprintln!("{}", success_message("Created worktree"));

// Primary output to stdout (tables, shell code, pipeable)
println!("{}", table_output);

// Flush before interactive prompts
stderr().flush()?;

Which `println!` is in scope decides whether a closed pipe panics: std's panics on the `BrokenPipe` write error, anstream's drops it. `wt … | head` closes the pipe, so command code imports the `worktrunk::styling` one and no `std::println!` is left in `src/`.

**Output whose ANSI is already decided** declares that once at the top of the command with `worktrunk::styling::ColorChoice::Always.write_global()` and then prints through the same anstream macros — the statusline a shell prompt or Claude Code renders, and the `--help-page` document whose escapes the docs pipeline turns into HTML (`--plain` and `--help-md` declare `Never` the same way). Neither consumer is ever a tty, so without the declaration anstream would strip their color every time — and the test suite would not catch it, because it forces color with `CLICOLOR_FORCE=1`; `test_color_follows_the_consumer` pins the unforced behavior. Declare `Always` only when the pipe is a courier rather than the destination; anything a person reads directly stays on plain anstream, which is what strips color on a pipe and honors `NO_COLOR`.

**`--format=json` answers** go through `crate::output::print_json`, never a hand-rolled `println!("{}", serde_json::to_string_pretty(&v)?)`. It serializes pretty with one trailing newline and prints through anstream, so no `--format=json` surface panics when its consumer stops reading. Before that, thirty call sites had open-coded those two lines, and whether any one of them panicked under `| head -3` came down to which `println!` its module happened to import. `wt switch --format=json` is the one non-caller: it emits its single result as one compact line (still through anstream's `println!`), because that is what a shell loop reads.

**Shell integration functions** (`src/output/global.rs`):

| Function | Purpose | |----------|---------| | `change_directory(path)` | Shell cd after wt exits (writes to directive file if set) | | `execute(command)` | Shell command after wt exits | | `terminate_output()` | Reset ANSI state on stderr | | `is_shell_integration_active()` | Check if directive file set (rarely needed) | | `pre_hook_display_path(path)` | Compute display path for pre-hooks | | `post_hook_display_path(path)` | Compute display path for post-hooks |

**Message formatting functions** (`worktrunk::styling`):

| Function | Symbol | Color | |----------|--------|-------| | `success_message()` | ✓ | green | | `progress_message()` | ◎ | cyan | | `info_message()` | ○ | symbol dim, text plain | | `warning_message()` | ▲ | yellow | | `hint_message()` | ↳ | dim | | `error_message()` | ✗ | red | | `prompt_message()` | ❯ | cyan |

**Section headings** (`worktrunk::styling`):

use worktrunk::styling::format_heading;

// Plain heading
format_heading("BINARIES", None)  // => "BINARIES" (cyan)

// Heading with suffix
format_heading("USER CONFIG", Some("@ ~/.config/wt.toml"))
// => "USER CONFIG @ ~/.config/wt.toml" (title cyan, suffix plain)

stdout vs stderr

**Decision principle:** stdout carries the command's *answer*; stderr carries *narration* about producing it. The discriminating question is answer-vs-narration, not audience — `wt list` is "for the user" yet belongs on stdout because it *is* the answer. "Is this a message to the user?" doesn't discriminate, because nearly all output is.

  • **stdout** → the answer, in whatever format the user selected. Data (tables, JSON, shell code, an expanded template) and `--dry-run` previews both qualify: a preview is the whole answer when nothing mutates. Human-formatted output belongs here too. Color strips automatically on a pipe (anstream), so `wt list | grep` stays safe.
  • **stderr** → narration about doing it: progress, success/warning/error messages, hints, interactive prompts, and `-v`/`-vv` diagnostics.
  • **directive file** → shell commands executed after wt exits (cd, exec).

The same line can flip streams between modes. `wt config shell uninstall` deletes the file, so `✓ Removed … @ ~/.zshrc` only

Read more
Ships withworktrunk

Worktrunk is a CLI for Git worktree management, designed for parallel AI agent workflows

Get the whole plugin
Stats
6,363
Stars
232
Forks
Active
Maintenance
Rust
Language
4h ago
Last commit
9mo ago
Created

Repo: max-sixty/worktrunk