# CLI procedure reference

Run `bun <skill-path>/scripts/soundfish.mjs <verb>` or the installed
`soundfish <verb>`; every verb prints its own usage with `--help`. Bun 1.3.14
or newer is required. Standard output carries only the result (a URL, Markdown
link, agent text, JSON, verification result, or a path); warnings and failures
go to standard error.

## Exit codes and output options

0 success, 2 usage, 3 invalid input, 4 conflict (a stale base digest or a
derived ID that already exists), 5 rejected by the protocol, 1 unexpected.
Every verb accepts `--format json` (one object with `url`, `fragment`, the
MusicCID or ArrangementCID, `digest`, and `canonicalBytes`), and `--base-url
<url>`. Address output also accepts `--fragment-only`, except `handoff`, which
requires a complete URL.

## Addresses and stdin

An `#s1` address is an editable loop; an `#a1` address is a song. Quote every
URL so the shell never sees the `#`. Only the read-only address commands
(`inspect`, `verify`, `handoff`, `analyze`, `lint`, `render`, `export-json`, and
`export-midi`) and the song verbs accept `-` in the URL position to read one
address from standard input; every loop operation takes its base URL as an
explicit argument. `compose -`, `encode -`, `fmt -`, and `import-json -` read
their document from standard input, and `add-track` or `replace-track` accept
`-` only in the track-input position.

## Inspect and verify

```sh
soundfish inspect '<url>'                       # S1 agent text (loops)
soundfish inspect '<url>' --format compose      # the authoring view
soundfish inspect '<url>' --annotate            # S1 with program-name comments
soundfish inspect '<url>' --format json         # lossless document; songs default to this
soundfish verify '<url>'                        # valid, kind, CID, digest, section count
```

Never claim verification after only inspecting decoded data.

## Deliver and recover

Save the final URL directly from the successful editing or creation command's
standard output as `final.url`. Keep this authority separate from the reply.
Inspect it to confirm the title, track types, requested changes, and preserved
tracks and lineage, then generate one complete verified Markdown link:

```sh
soundfish inspect - --format json < final.url
soundfish handoff - < final.url > reply.md
```

Keep the generated link line intact. Add only plain prose around it; the entire
reply must contain no code or HTML.
Check the actual reply against the saved authority before returning it verbatim:

```sh
soundfish handoff - --check reply.md < final.url
```

`handoff` works for loops and songs. `--check` requires exactly one standalone
Markdown link and verifies its complete canonical document against the source,
including titles, IDs, and lineage. Its origin, path, and query must match the
delivery base URL (`https://sound.fish/` by default). Compressed and uncompressed
spellings of the same canonical bytes are accepted; a matching MusicCID alone
is insufficient. This is a check for the generated link with surrounding plain
prose, not a general Markdown parser or repair command.
`--format json` returns a structured result. To read the reply from stdin, use
an explicit source URL: `soundfish handoff '<url>' --check - < reply.md`.
Source URL and reply cannot both use stdin.

Retain `final.url`, `reply.md`, and any lossless JSON export as recovery artifacts.
If a delivered link is truncated, regenerate it from `final.url`. If only a
lossless JSON export survives, use `soundfish import-json final.soundfish.json
> recovered.url`, inspect the recovered composition, then repeat the handoff
procedure. Never invent missing payload characters, append a guessed checksum,
or repair a fragment by hand. If no intact authority remains, request it again.
The check proves the saved reply matches the source; a chat or clipboard can
still alter it after checking. Share the retained lossless JSON if that surface
cannot carry the complete link.

## Diagnostics

```sh
soundfish analyze '<url>'                       # bytes, events, key, chords, density, registers
soundfish lint '<url>'                          # rubric findings with a fix verb; always exits 0
soundfish render --dry '<url>' out.wav          # stems and band statistics (repository checkout only)
soundfish lint '<url>' --render                 # lint plus the mix findings from a fresh render
```

`render --dry` layers the document's drum kits and instrument overrides above
the base bank exactly as the browser does; the published CLI reports that it
cannot render and stops.

## Operations on a loop

Every operation takes `--operation op_<name>` and prints one new URL. The same
ID against the same base reproduces the same URL; a changed base exits 4. Track
and event IDs are derived from the operation ID, never invented, and a
transform replaces its track under a new derived ID: with `--format json` the
result carries `tracks` (id, name, kind) so the next verb can name it.

```sh
soundfish add-track '<url>' track.soundfish --operation op_pad --from trk_keys
soundfish replace-track '<url>' trk_keys track.soundfish --operation op_revoice --from trk_keys
soundfish remove-track '<url>' trk_pad --operation op_drop_pad
soundfish resize '<url>' 8 --operation op_eight
soundfish set-tempo '<url>' 126 --operation op_tempo
soundfish set-title '<url>' 'Night drive' --operation op_title
soundfish set-effects '<url>' --reverb 1500 --delay 2000 --feedback 3000 --time 500 --operation op_fx
soundfish set-document-automation '<url>' delay-return --base 2000 --points "0:0:linear,32:4000" --operation op_ride
soundfish add-event '<url>' trk_bass --pitch 36 --start 0 --end 4 --velocity 96 --operation op_root
soundfish move-event '<url>' trk_bass evt_a1b2c3d4 --start 8 --pitch 43 --operation op_lift
soundfish remove-event '<url>' trk_bass evt_a1b2c3d4 --operation op_drop
soundfish set-kit '<url>' trk_drums 25 --operation op_808
soundfish set-pan '<url>' trk_keys --base 3500 --operation op_left
soundfish set-cutoff '<url>' trk_pad --base 4000 --points "0:4000:linear,48:9000" --operation op_open
soundfish set-swing '<url>' 24 --operation op_swing
soundfish set-meta '<url>' --key Am --genre house --operation op_meta
soundfish transpose '<url>' trk_bass -12 --operation op_low
soundfish fill '<url>' trk_drums --bar 4 --seed 3 --operation op_fill
soundfish duck '<url>' trk_bass --against trk_drums --operation op_duck
soundfish thin '<url>' trk_keys --to-bytes 19000 --operation op_thin
```

A track input is one-track S1 agent text; write it with `inspect --format
agent` as the model or let `compose` produce a whole loop and copy one track.
Repeat `--from <track-id>` for every current track the new material derives
from; omit it only for independent material. Transforms (`transpose`, `shift`,
`humanize`, `duplicate-bars`, `double`, `fill`, `variation`, `accent`, `thin`,
`duck`, `set-program`, `set-volume`, `set-send`, `rename`, `legato`) record
lineage from the transformed track automatically. `swing` is an alias of
`set-swing`, a document operation.

## Templates

```sh
soundfish templates list [--kind grooves|progressions|bass|songs]
soundfish templates apply progressions/jazz-ii-V-I --key F#m --bpm 132
soundfish templates apply bass/octave-bounce --key Am --compose > bass.compose
```

Templates ship in `templates/` beside `scripts/`; progressions and bass idioms
are written in scale degrees and resolve to `--key`.

## Songs

```sh
soundfish song build --title 'Night drive' --section 'Intro:<loop-url>x2' --section 'Drop:<loop-url>'
soundfish song sections '<song-url>'
soundfish song detach '<song-url>' 2
soundfish song apply '<song-url>' -- set-tempo 124 --operation op_tempo
```

Sections must share one tempo, one swing, and one track roster (same track
IDs, kinds, programs, and kits). Loop operations refuse a song address; use
`song apply` for every section or `song detach` for one.

## Export and import

```sh
soundfish export-json '<url>' out.soundfish.json    # lossless, loop or song
soundfish export-midi '<url>' out.mid               # interoperable projection
soundfish import-json out.soundfish.json
soundfish import-midi input.mid [--as-song]
soundfish encode document.soundfish [--strict]      # S1 agent text to URL
soundfish fmt document.soundfish                    # canonical S1 spelling
```

JSON preserves the canonical model; MIDI drops song routing, effects,
automation, identities, and lineage, so keep the URL or JSON as the authority.
MIDI import pads short tracks to the bar, maps controllers 7, 10, 91, and 93
at tick zero, and reports tempo maps, meter changes, pitch bend, aftertouch,
SysEx, and off-grid notes as rejections; state them rather than discarding
them.

## Local song import

Only download and transform media the user has the right to use.

```sh
soundfish song doctor
soundfish song import '<youtube-url>' --output <directory> [--from 0:30 --to 0:46]
soundfish song recut <run-directory> --loop-bars 4
```

The importer needs a user-managed StemDeck service (yt-dlp plus a separator)
and Basic Pitch in its own Python environment; `song doctor` checks them and
the CLI never installs or starts them. Each run writes stems, MIDI and CSV,
loop and song URL, JSON, and MIDI artifacts, and an analysis file. Separation
and transcription are estimates; audition and correct the MIDI.

StemDeck assigns job IDs server-side and has no client idempotency key. If a
submission response is lost before an ID arrives, do not retry blindly: keep
the `.soundfish-submission-*.failure.json` evidence, inspect the StemDeck
queue, and cancel any matching job before retrying.

## Data boundary

The CLI never uploads a composition or creates a server record. Do not send a
URL or its decoded contents to a mutation service. The provider disclosure
for hosted chats lives at https://sound.fish/docs/agent.
