diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2759bf4..a4c1e43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy @@ -35,6 +35,8 @@ jobs: run: cargo test --workspace --features lash-cli/fff-zlob --locked - name: Check operator scripts run: python3 -m py_compile scripts/*.py + - name: Check documentation + run: python3 scripts/check_docs.py build: strategy: @@ -42,11 +44,19 @@ jobs: include: - os: ubuntu-latest target: x86_64-unknown-linux-gnu + linker: "" + packages: "" + - os: ubuntu-latest + target: aarch64-unknown-linux-gnu + linker: aarch64-linux-gnu-gcc + packages: gcc-aarch64-linux-gnu g++-aarch64-linux-gnu - os: macos-14 target: aarch64-apple-darwin + linker: "" + packages: "" runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} @@ -54,5 +64,10 @@ jobs: with: version: 0.15.2 - uses: Swatinem/rust-cache@v2 + - name: Install cross compiler + if: runner.os == 'Linux' && matrix.packages != '' + run: scripts/ci-install-apt-packages.sh ${{ matrix.packages }} - name: Build release binary + env: + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: ${{ matrix.linker }} run: cargo build --release --locked -p lash-cli --features fff-zlob --target ${{ matrix.target }} diff --git a/.github/workflows/docs-pages.yml b/.github/workflows/docs-pages.yml new file mode 100644 index 0000000..15c07c6 --- /dev/null +++ b/.github/workflows/docs-pages.yml @@ -0,0 +1,34 @@ +name: Docs Pages + +on: + push: + branches: [main] + paths: + - "docs/**" + - ".github/workflows/docs-pages.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: lash-cli-pages + cancel-in-progress: true + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/configure-pages@v6 + - uses: actions/upload-pages-artifact@v5 + with: + path: docs + - name: Deploy documentation + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a4b816..56e0a30 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,10 @@ permissions: contents: write actions: read +concurrency: + group: lash-cli-release-main + cancel-in-progress: false + env: CARGO_TERM_COLOR: always @@ -22,7 +26,7 @@ jobs: sha: ${{ steps.release.outputs.sha }} tag: ${{ steps.release.outputs.tag }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: ref: main fetch-depth: 0 @@ -73,7 +77,7 @@ jobs: packages: "" runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: ref: ${{ needs.prepare.outputs.tag }} - uses: dtolnay/rust-toolchain@stable @@ -95,7 +99,7 @@ jobs: run: cargo build --release --locked -p lash-cli --features fff-zlob --target ${{ matrix.target }} - name: Package run: scripts/package-release.sh ${{ matrix.target }} ${{ matrix.asset }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: ${{ matrix.asset }} path: dist/${{ matrix.asset }} @@ -104,12 +108,12 @@ jobs: needs: [prepare, build] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: ref: ${{ needs.prepare.outputs.tag }} fetch-depth: 0 fetch-tags: true - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: path: dist pattern: lash-*.tar.gz @@ -123,7 +127,7 @@ jobs: sha256sum lash-*.tar.gz > SHA256SUMS - name: Collect release notes run: python3 scripts/release_notes.py collect --end "${{ needs.prepare.outputs.tag }}" --out "${{ runner.temp }}/RELEASE_NOTES.md" - - uses: softprops/action-gh-release@v2 + - uses: softprops/action-gh-release@v3 with: tag_name: ${{ needs.prepare.outputs.tag }} body_path: ${{ runner.temp }}/RELEASE_NOTES.md diff --git a/README.md b/README.md index 5f6ad8e..c347cf2 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ The workspace owns the `lash` executable and its private TUI, export, file index, autoresearch, and test-harness crates. Lash itself remains an embeddable runtime; this repository is one complete host built on top of it. +Documentation: [CLI reference](https://samgalanakis.github.io/lash-cli/) · +[architecture](https://samgalanakis.github.io/lash-cli/architecture.html) · +[publishing](https://samgalanakis.github.io/lash-cli/publishing.html) + ## Build ```sh diff --git a/docs/architecture.html b/docs/architecture.html new file mode 100644 index 0000000..298e997 --- /dev/null +++ b/docs/architecture.html @@ -0,0 +1,70 @@ + + + + + + +lash CLI architecture + + + + + + + +
+

cli/architecture

+

lash-cli is a Host Application: it selects reusable Lash SDK crates and owns the executable's composition, configuration, presentation, operational policy, and releases.

+ +
+
+
+
+

Ownership boundary

+

The CLI depends on one reviewed Lash Git revision. Lash exposes runtime, protocol, provider, plugin, persistence, tracing, and language crates. The CLI turns those pieces into a terminal product.

+
+
+

lash-cli owns

Argument parsing, setup and stored configuration, provider materialization, concrete plugin wiring, execution-mode selection, TUI state, autonomous JSON/RPC surfaces, session navigation, export, update policy, installer assets, profiling, and binary releases.

+

Lash owns

The reusable runtime and facade, protocol implementations, provider factories, generic tools and plugins, stores, durable process machinery, Lashlang, trace contracts, and embedding documentation.

+
+
+
+

Workspace components

Every local crate is private to the terminal application unless another consumer justifies promoting a capability into the Lash SDK.

+
+

lash-cli

Binary bootstrap, configuration, providers, plugin composition, commands, session flows, autonomous modes, and terminal application state.

+

lash-tui

Terminal input, frame lifecycle, and rendering primitives.

+

lash-tui-extensions

Host-owned UI extension contracts and activity presentation.

+

lash-search-tools

FFF-backed local search and the optional Lashlang search surface.

+

lash-file-index

Ignore-aware file walking, filesystem notifications, and fuzzy file completion.

+

lash-export

Session-tree HTML and JSON export for CLI persistence and traces.

+

lash-autoresearch

CLI-owned applied workflow plugin and terminal extension.

+

harness + performance

The debug CLI harness and CLI performance helpers exercise product behavior without becoming runtime SDK contracts.

+
+
+
+

Composition flow

+
arguments + ~/.lash/config.json
+              │
+              ▼
+provider factories + plugin factories + store
+              │
+              ▼
+        LashCore / LashSession
+              │
+       ┌──────┴────────┐
+       ▼               ▼
+interactive TUI   --print / JSON / RPC
+

Execution modes such as standard and rlm are Lash protocol plugins. The CLI chooses and configures one for a session; it does not implement protocol semantics itself.

+
+
+

Dependency rule

+

lash-cli may depend on published or Git-pinned Lash SDK crates. Lash must not depend back on CLI crates. A second application that needs CLI-private code is evidence for extracting a focused SDK capability—not for making applications depend on one another.

+
+
+ + + diff --git a/docs/export-design.html b/docs/export-design.html new file mode 100644 index 0000000..fc7e016 --- /dev/null +++ b/docs/export-design.html @@ -0,0 +1,1665 @@ + + + + + + + +trace export · cross-session edges · lash CLI + + + + + + + + + + + +
+

trace/edges

+

A mockup of the html exporter's two-edge model: continue_as renders as an inline AgentFrame divider, and spawn_agent renders as a drill-in card. Click any node to follow the call tree. This page is a design exploration: everything below is static, hand-written sample data, not a render produced by lash-export. The continue_as inline AgentFrame-switch divider now ships in lash-export's multi-session tree view; the spawn_agent drill-in card shown here stays illustrative — the real exporter presents subagents as sibling views.

+
+ +
+ +
+
+ + + + + +
+ +
+
+ root session · s1·a47e +

Audit auth-related code paths and recent migrations

+

Root session of the run. Two parallel spawn_agent children gather inventory, then a continue_as AgentFrame switch carries the synthesis into frame s4·e51a.

+
+
+
views1·a47e · root
+
modelclaude-opus-4-7
+
turns8 (this view) + 4 AgentFrame (s4)
+
subagents2 — auth_files, db_migrations
+
frame→ s4·e51a · continue_as
+
tokens28.4k in · 3.1k out · 18% cache read
+
+
+ + + +
+
+ role + + + + +
+
+ edge + + +
+
+ +
+ + +
+ +
+
+ e0 + +
+
+
+ + Audit auth-related code paths and recent migrations. + +
+
+

Audit the codebase: list every file under src/auth/ that handles session tokens, summarise schema migrations since v3, and produce a single ordered list of the riskiest places to land a compliance fix.

+
+
+
+ +
+
+ e1 + +
+
+
+ + Plan: spawn two parallel explorers, then continue_as for the synthesis. + +
+
+
+ + + + +
+
+ e3 + +
+
+
+ + auth_files returned 4 paths. Now spawn db_migrations in parallel. + +
+
+
+ + + + +
+
+ e5 + +
+
+
+ + Both explorers returned. Context is at 78%. Invoke continue_as with the synthesis seed. + +
+
+
+ + +
+
+
+
+ ↪ continued as + Synthesise the audit into a ranked compliance plan. +
+
+ s1·a47e + + s4·e51a +
+
+ +
+

Take the auth-file inventory and the migration summary; produce a single ranked list of compliance touchpoints. Each item gets a one-line rationale and an effort estimate. Stop when the list is < 12 items.

+ +
+ seed · 4 entries +
+ problemprojected + auth_filesglobal · 4 paths + migration_summaryglobal · 1.4k + findingsglobal · 12 entries +
+
+ +
+ parent ctx78% + seed2.8 kb + turns8 + reasoncontinue_as +
+
+
+ + + +
+
+ e6 + +
+
+
+ + Synthesise the audit into a ranked compliance plan. + +
+
+

Take the auth-file inventory and the migration summary; produce a single ranked list of compliance touchpoints…

+
+
+
+ +
+
+ e7 + +
+
+
+ + Cross-reference auth_files against migration_summary; rank by exposure, then by effort. + +
+
+
+ +
+
+ e8 + +
+
+
+ + Ranked plan returned (8 items). + +
+
+

1. cookie_jar.rs: refresh-token rotation lands in the same write as session_secret; partition first. ~½ day
2. token_store.rs: v5 adds refresh_token_hash, but the read path still falls back to plaintext on cache miss. ~1 day
3. …

+
+
+
+ +
+
+
+ + +
+ +
+
+ subagent · s2·b9c1 · spawned from root +

auth_files — list session-token handlers

+

Subagent spawned from root at e2. Same shape as the root view: full transcript, full controls, full minimap. Use the breadcrumb above to return.

+
+
+
views2·b9c1 · subagent
+
capabilityexplore
+
parentroot → entered at e2
+
depth1
+
duration14.2s · 4.1k tokens
+
returned{ files: [4 paths] }
+
+
+ + + +
+ + +
+ +
+
+ e0 + +
+
+
+ + List files under src/auth/ that handle session tokens. + +
+
+

List files under src/auth/ that handle session tokens, with the read paths grouped by entry point.

+
+
+
+ +
+
+ e1 + +
+
+
+ + grep -r "session_token" src/auth → resolve, dedupe, finish. + +
+
+
+ +
+
+ e2 + +
+
+
+ + session_token in src/auth — 4 unique files + +
+
+
+ +
+
+ e3 + +
+
+
+ + { files: ["src/auth/session.rs", "…/middleware.rs", "…/cookie_jar.rs", "…/token_store.rs"] } + +
+
+
+ +
+
+
+ + +
+ +
+
+ subagent · s3·d23f · spawned from root +

db_migrations — summarise migrations since v3

+

Subagent that itself spawned a deeper child (migration_v5). Drill in further to see depth 2.

+
+
+
views3·d23f · subagent
+
capabilityexplore
+
parentroot → entered at e4
+
depth1 — has 1 child
+
duration22.7s · 7.8k tokens
+
returned{ summary: "v5 introduces session_secret …" }
+
+
+ + + +
+ + +
+ +
+
+ e0 + +
+
+
+ + Summarise migrations/ schema changes since v3. +
+
+
+ +
+
+ e1 + +
+
+
+ + read migrations/, group by version, fan out to a sub-explorer per migration. + +
+
+
+ + + +
+
+ e3 + +
+
+
+ + migration_v5 returned columns. Compose summary per version. + +
+
+
+ +
+
+ e4 + +
+
+
+ + { summary: "v5 introduces session_secret + refresh_token_hash …" } + +
+
+
+ +
+
+
+ + +
+ +
+
+ subagent · s3a·772b · depth 2 +

migration_v5 — risk-flagged columns

+

Grandchild, spawned from db_migrations, which itself was spawned from root. The breadcrumb traces the whole way back.

+
+
+
views3a·772b · subagent
+
capabilityexplore
+
parentdb_migrations → entered at e2
+
depth2
+
duration9.4s · 3.0k tokens
+
returned{ columns: ["session_secret", "refresh_token_hash"] }
+
+
+ + + +
+ + +
+ +
+
+ e0 + +
+
+
+ + Extract column names tagged risk:* from migrations/v5.sql. +
+
+
+ +
+
+ e1 + +
+
+
+ + read v5.sql; pluck columns whose comment matches /risk:.*/. + +
+
+
+ +
+
+ e2 + +
+
+
+ + { columns: ["session_secret", "refresh_token_hash"] } + +
+
+
+ +
+
+
+ +
+ + +
+
+
+

↪ AgentFrame · inline

+

continue_as is linear. The new frame's entries flow in the same view, behind a sodium-bracketed divider that names the seam: task, seed (projected vs global), parent context %, the new frame id. One scroll, one document.

+
+
+

▾ subagent · drill-in

+

Spawn_agent becomes a card you click into. The subagent gets its own full view: same hero, same controls, same minimap as the root. No nesting hacks, no width-cascade problems, no awkward squeeze.

+
+
+

breadcrumb · always visible

+

The top bar shows where you are: root > db_migrations > migration_v5. Click any segment to jump back. Browser back works (history.pushState). The lineage strip below shows the whole tree as a sibling-aware map.

+
+
+

same shape · everywhere

+

Every view is just a session render. The exporter already does that for one .db; we just iterate per descendant session, link them with the breadcrumb, and bake them all into one self-contained html file.

+
+
+
+
+ + + +
+ + + + + diff --git a/docs/export.html b/docs/export.html new file mode 100644 index 0000000..39a1296 --- /dev/null +++ b/docs/export.html @@ -0,0 +1,207 @@ + + + + + + + +lash CLI session exporter + + + + + + + +
+

html/exporter

+

The trace exporter renders one persisted lash session (plus any child sessions created by spawn_agent) into a single self-contained HTML document. The visual language matches the rest of the docs (warm-iron / fast-amber). The forensic affordance is two-edge-aware: continue_as AgentFrame switches are linear seams inside the same session view; subagents are drill-in cards into child-session views.

+
+ +
+
+
+

The two-edge model

+

continue_as and spawn_agent both create a visible boundary, but only spawn_agent creates another session. continue_as appends an AgentFrame record inside the same session; spawn_agent creates a child session with SessionRelation::Child. Flattening those under one shape is what made the previous exporter feel wrong, so the renderer mirrors the runtime distinction.

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
EdgeToolCardinalityLifecycleVisual treatment
AgentFrameSwitchcontinue_as1 → 1 terminalCurrent frame ends; the new frame takes over with seed and task.Inline divider. The frame's entries continue in the same session view, behind a sodium-bracketed seam that names the boundary.
Subagentspawn_agent1 → N blockingParent waits for the child to finish, then resumes.Drill-in card. Click into the subagent's own full view (same hero, same controls). Breadcrumb tracks the path back.
+
+
+ +
+
+

Inline AgentFrame switch seam

+

Continue_as is linear, so the natural reading is one transcript that crosses an AgentFrame boundary inside the same session. The seam is a full-width band with sodium hairlines top and bottom: the task in display type, the seed broken into projected vs global entries, and a hairline-divided stat strip. Those four pieces of state show the reader what was carried forward and what was left behind.

+
+
+
+
+
+
+ ↪ continued as + Synthesise the audit into a ranked compliance plan. +
+
+ s1·a47e + + s4·e51a +
+
+
+

Take the auth-file inventory and the migration summary; produce a single ranked list of compliance touchpoints. Each item gets a one-line rationale and an effort estimate. Stop when the list is < 12 items.

+
+ seed · 4 entries +
+ problemprojected + auth_filesglobal · 4 paths + migration_summaryglobal · 1.4k + findingsglobal · 12 entries +
+
+
+ parent ctx78% + seed2.8 kb + turns8 + reasoncontinue_as +
+
+
+
+
+ +
+
+

Subagent drill-in card

+

Spawn_agent is blocking but its result is a separate unit of work. Inlining the child transcript inside the parent's tool entry would force a width-cascade and lose the subagent's full hero / controls / minimap. Instead, the exporter gives the subagent its own full view reached by click-through navigation. The card below shows the affordance; in the shipped exporter the lineage strip and breadcrumb carry the [data-go] targets. The lichen accent keeps the child visually subordinate to the sodium AgentFrame switch seam.

+
+
+
+
+
+
+ ▾ subagent · spawn_agent + db_migrations · explore +

Summarise migrations/ schema changes since v3. Flag any change that touches identity, session, or token columns.

+
+ ok + turns5 + spawned1 · migration_v5 + tokens7.8k +
+
+
+
+ s3·d23f + 22.7s +
+
+
+
+ + + +
+
+

Live demo

+

An end-to-end mockup with three views (rootdb_migrationsmigration_v5) backed by synthetic but realistic content. Click any drill-in card or breadcrumb step to navigate. Browser back works through history.pushState. Resize the iframe to see responsive behavior.

+
+ +
+ open + export-design.html ↗ + · + scroll inside the frame to see the inlined AgentFrame switch divider +
+
+ +
+
+

Implementation map

+

The renderer is additive; single-session export still works as it always did. Child-session detection is automatic, and AgentFrame dividers come from the owning session's tool-control records.

+
+
    +
  1. +

    Tree discovery

    +

    Given a root .db path and a trace JSONL, tree::load_tree_from_paths scans the sessions directory, keeps every child store reachable through parent_session_id chains, and partitions LlmPromptSnapshots by context.session_id. Child sessions are classified from persisted SessionRelation::Child metadata and anchored back to the parent's spawn_agent tool call when possible. continue_as no longer participates in tree discovery because its AgentFrame records live inside the same session store.

    +
  2. +
  3. +

    View topology

    +

    A view is one persisted session: the root or a spawned subagent. html::tree::write_view writes one <section class="view"> per session: hero, lineage strip, minimap spine, and transcript. continue_as AgentFrame switches stay inline in that session's transcript (they never split the view), and spawned subagents are reachable through [data-go] navigation in the lineage strip and breadcrumb that targets child-session views.

    +
  4. +
  5. +

    View switching

    +

    Each view is rendered up-front and toggled by a tiny IIFE that reads window.__lashTraceTree (a serialized list of {id, label, sid, parent}, one entry per view head (root and each subagent), not one per session; id and parent are view IDs, sid is the head session's short id). Click handlers on [data-go] elements (drill-in cards, breadcrumb segments, lineage pills) call history.pushState and swap the active .view. The browser back button does the right thing through popstate.

    +
  6. +
  7. +

    Visual discipline

    +

    Sodium is reserved for the active edge: AgentFrame switch seams, the breadcrumb's current step, the user's intent. Lichen marks the subagent affordance: drill rails, drill arrows, child status pills. Filter chips read as quiet chalk by default and gain a sodium strikethrough only when the user has actively filtered something off, so the controls strip never floods the page with accent. Ash structural lines do everything else.

    +
  8. +
+
+ +
+
+

Source map

+

Single self-contained HTML; tokens, fonts, CSS, and JS all inlined.

+
+
+

lash-export

Tree discovery and edge classification. Returns a LoadedSessionTree with one LoadedSessionNode per discovered session; each carries its own chronological projection, partitioned LLM prompts, and the list of subagent edges anchored by parent call_id.

+

lash-export

The public render_tree(tree, format) is the multi-view entry point; the writers in the html::tree submodule (write_crumb_bar, write_view, write_view_hero, write_lineage, and render_tree_data_script) own the multi-view elements, while render_node_entries reuses the existing message/tool/RLM-step renderers unchanged.

+

lash-export

Tokens are appended at the bottom under "multi-view: breadcrumb, lineage, drill, AgentFrame switch". The base trace stylesheet stays intact for single-session exports; multi-view rules only activate when the page contains .view sections.

+

lash-export

The view-switcher IIFE at the bottom reads window.__lashTraceTree, wires [data-go] click handlers, renders the breadcrumb, and listens for popstate. It exits cleanly if the tree global is absent (single-session export).

+
+
+ +
+
read on ·
+
+
+
+
+ + diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..66e322f --- /dev/null +++ b/docs/index.html @@ -0,0 +1,327 @@ + + + + + + + +lash CLI reference + + + + + + + +
+

using/cli

+

The lash binary runs an interactive terminal UI by default, autonomous single-shot modes under --print, a JSONL event stream under --mode json, an stdin/stdout RPC loop under --mode rpc, and administrative flows like --export and --reset. This repository owns the executable and its user-visible contract.

+ +
+ +
+
+
+

First Run

+

If ~/.lash/config.json does not exist, lash launches the setup wizard. The wizard provisions an active provider, collects credentials, and writes the config file before dropping you into the TUI.

+
+
$ lash
+# Setup wizard:
+# 1. Choose a provider (Anthropic, OpenAI, OpenAI-compatible, Codex, Google OAuth)
+# 2. Paste API key (or run OAuth / device-code flow)
+# 3. Optionally paste a Tavily API key for the built-in web-search tool
+# 4. Drop into the interactive TUI
+

Re-run the wizard at any time with lash --provider. Use /provider from inside a session to switch providers without restarting.

+
+ +
+
+

Slash Commands

+

Type a / at the start of the prompt to invoke a command. Tab-completion is available. Skill names registered in ~/.lash/skills/ or .agents/lash/skills/ appear as additional /<skill> commands.

+
+
+ + + + + + + + + + + + + + + + + + + + + +
CommandAliasesUsageDescription
/clear/new/clearReset conversation
/compact/compact [focus instructions]Open a compaction Agent Frame seeded by a summary
/controls/controlsShow keyboard shortcuts
/fork/forkOpen a forked session in a new terminal
/tree/treeBrowse and switch branches in the current session
/version/versionShow lash-cli and lash-sansio versions
/info/status/infoShow current session/runtime info
/model/model [name]Show or switch the LLM model
/variant/variant [name]Show or switch the model variant (e.g. high, max)
/mode/mode [name]Show the session's execution mode (standard / rlm); it is locked for the session, so changing it requires a new session started with --execution-mode
/provider/login/providerSwitch, add, or re-authenticate providers
/logout/logoutRemove stored credentials for active provider
/retry/retryReplay the previous turn payload
/resume/continue/resume [id-or-name]Search sessions or resume by id/name (interactive picker if no arg)
/skills/skillsBrowse loaded skills
/help/?/helpOpen the commands and shortcuts overlay
/exit/quit/exitQuit
+
+
+ +
+
+

Keyboard Shortcuts

+

The TUI uses a small, consistent set of shortcuts. The full list is also available in-session via /controls.

+
+
+
+

History navigation

+
+
PageUp / PageDown
Scroll history or the active document overlay.
+
/
Cycle the input-draft history when not navigating a suggestion popup.
+
+
+
+

Prompt editing

+
+
Enter
Submit immediately when idle; while a turn is active, inject at the earliest safe runtime boundary.
+
Tab
Accept an autocomplete suggestion when the popup is open; with a draft, submit immediately when idle or queue a next-turn draft while active.
+
Ctrl+U
Delete draft text from the cursor to the start of the line.
+
Ctrl+K
Delete draft text from the cursor to the end of the line.
+
Ctrl+← / Ctrl+→
Move the cursor by word.
+
Home / End
Jump to the start / end of the prompt.
+
+
+
+

Selection & clipboard

+
+
Ctrl+Shift+C
Copy the current selection or the last assistant response. Configure via LASH_COPY_BINDING if the default conflicts with your terminal.
+
Ctrl+C
Close a popup or overlay, cancel the active turn, clear a non-empty draft, or quit from an idle empty prompt.
+
Esc
Close overlays, pickers, and modal flows.
+
+
+
+

Command palette

+
+
Ctrl+P
Open the command palette settings overlay: a searchable list of Settings actions (theme, model, variant, provider, execution mode, logout) and Session actions (new, resume, tree, fork), each annotated with the equivalent slash command.
+
+
+
+

Pickers & multi-select

+
+
/ or k / j
Move selection.
+
Space
Toggle a multi-select option when supported.
+
Tab / BackTab
Add / remove a note on a selection when supported.
+
+
+
+

Background process dock

+
+
Tab / Shift+Tab
With an empty prompt, cycle focus through the dock of background processes. The dock lists only processes granted to the current session.
+
Enter
Open an overview of the focused process.
+
Delete
Cancel the focused process.
+
+

A running process shows running; a terminal one shows its outcome — success, error, cancelled, or abandoned. abandoned (ADR 0019) means the owner was lost without recording an outcome and renders in the failure style, like error. A detached command (shell.start with detach: true) never appears in the dock: it returns immediately with status: "detached" and is owned by the host/OS, not tracked as a session process.

+
+
+
+ +
+
+

CLI Flags

+

Run lash --help for the full list. The most common flags, grouped by purpose:

+
+
+
+

Provider & model

+
+
--provider
Re-run the provider setup wizard.
+
--model <name>
Override the default model for this session.
+
--variant <name>
Override the model-native variant (e.g. high, max, xhigh).
+
--api-key <key>
One-shot API key for OpenAI or OpenAI-compatible providers.
+
--base-url <url>
Base URL for the LLM API (OpenAI-compatible).
+
--tavily-api-key <key> (env TAVILY_API_KEY)
Tavily key for the bundled web-search tool.
+
+
+
+

Execution

+
+
--execution-mode <mode>
standard (default) or rlm.
+
-c, --context-approach <approach>
For standard mode: rolling_history or observational_memory.
+
--rlm-var name=<json>
Repeatable. Project a read-only bound variable into the RLM autonomous turn.
+
--rlm-vars-file <path>
Same as above but load the projections from a JSON object file.
+
--rlm-termination <mode>
Choose natural (default) or require an explicit structured finish with finish-required.
+
+
+
+

Session

+
+
--resume <id_or_name>
Resume by session id, name, or legacy .db filename.
+
--resume-prompt <prompt>
Send a prompt right after resume.
+
+
+
+

Output modes

+
+
-p, --print <prompt>
Autonomous: run the prompt once, write the answer to stdout, exit. See Print mode below.
+
--mode <interactive|json|rpc>
Select the host surface. interactive is the TUI default; json requires --print and writes JSONL events to stdout; rpc reads LF-delimited requests from stdin and writes LF-delimited records to stdout.
+
--await-background-work
In autonomous mode, wait for plugin background work before exiting.
+
--turn-usage-json <path>
In autonomous mode, write per-turn and cumulative usage JSON to a file.
+
+
+
+

Debug & tracing

+
+
--debug
Detailed lifecycle/debug logs plus per-session LLM traces.
+
--trace-level <standard|extended>
Trace detail. Default standard.
+
--debug-ui-trace trace.json
Record the live TUI session as a replayable UI trace plus a final snapshot.
+
--debug-ui-trace-interval-ms <ms>
When recording a UI trace, also capture numbered checkpoint snapshots at the given interval.
+
+
+
+

Observational memory

+
+
--om-observation-message-tokens <tokens>
Observe recent raw history after this many new message tokens.
+
--om-observation-buffer-tokens <tokens>
Keep this much message-token slack before observation pressure starts.
+
--om-observation-block-after-tokens <tokens>
Block new turns once unobserved message tokens pass this threshold.
+
--om-observation-max-tokens-per-batch <tokens>
Cap the raw-history slice sent to one observer pass.
+
--om-previous-observer-tokens <tokens>
Budget previous observer notes included in a fresh observation pass.
+
--om-reflection-observation-tokens <tokens>
Target observation-token volume before reflection becomes useful.
+
--om-reflection-buffer-activation-percent <percent>
Start reflection when the reflection buffer reaches this percentage.
+
--om-reflection-block-after-tokens <tokens>
Block new turns once pending reflection material passes this threshold.
+
+
+
+

Admin

+
+
--reset
Delete ~/.lash/ and ~/.cache/lash/ and exit.
+
--info
Print current config / runtime info and exit.
+
--check-update / --update
Check / install the latest release.
+
--export <db>
Export a persisted session database. Combine with --export-format html|json, --export-trace <jsonl>, and --export-out <path>.
+
+
+
+
+ + + +
+
+

JSON & RPC Modes

+

--mode json and --mode rpc are machine-facing CLI surfaces. They do not enter the TUI and they reserve stdout for LF-delimited JSON records. The stream is versioned: the opening turn_start record (and the RPC ready record) carries protocol_version: 1, so a consumer can pin the wire shape before parsing the rest.

+
+
$ lash --mode json --print "Summarise this repository."
+{"type":"turn_start","protocol_version":1,"stream_id":1}
+{"type":"event","stream_id":1,"activity":{"id":"a1","correlation_id":"c1","type":"model_request_started","protocol_iteration":0}}
+{"type":"event","stream_id":1,"activity":{"id":"a2","correlation_id":"call-3","type":"tool_call_completed","call_id":"call-3","name":"exec_command","args":{"...":"..."},"output":{"...":"..."},"duration_ms":41}}
+{"type":"turn_finish","stream_id":1,"ok":true,"cancelled":false,"assistant_text":"...","outcome":{...},"usage":{...},"children_usage":[],"errors":[],"execution":{...},"tool_calls":[...]}
+

Each event wraps one TurnActivity: an id, a correlation_id, and a flattened TurnEvent tagged by type. Tool events report the runtime tool name (exec_command), not the shell.exec surface path; see the Lash SDK's event vocabulary and stability contract.

+

RPC mode starts a reusable session and reads one JSON object per line from stdin. It opens with a ready record ({"type":"ready","protocol_version":1,"protocol":"lash.rpc.v1","methods":["prompt","ping","shutdown"]}). A prompt request streams turn_start, event, and turn_finish records tagged with the request id, then writes a final response.

+
{"id":1,"method":"prompt","params":{"prompt":"List the top-level crates."}}
+{"id":2,"method":"shutdown"}
+
+ +
+
+

Config File

+

Configuration lives at ~/.lash/config.json (overridable via the LASH_HOME environment variable). The schema is defined by LashConfig in lash-cli and uses serde(deny_unknown_fields), so unknown top-level fields are rejected to catch typos.

+
+
{
+  "active_provider": "openai",
+  "providers": {
+    "openai": { "type": "openai", "api_key": "sk-..." },
+    "anthropic": {
+      "type": "anthropic",
+      "api_key": "sk-ant-...",
+      "options": {
+        "cache_retention": "long",
+        "max_output_tokens": 16384
+      }
+    }
+  },
+  "auxiliary_secrets": {
+    "tavily_api_key": "tvly-..."
+  },
+  "mcp_servers": {
+    "docs": {
+      "transport": "stdio",
+      "command": "uvx",
+      "args": ["mcp-server-docs"]
+    }
+  },
+  "agent_models": {},
+  "model_defaults": {
+    "openai": { "model": "openai/gpt-5.5", "variant": "medium" }
+  }
+}
+
+ + + + + + + + + + +
FieldTypePurpose
active_providerstringKey of the currently active provider; must exist in providers.
providersmapPer-provider credentials and metadata, keyed by provider kind. Shared provider tuning lives under options, including cache_retention (none | short | long) and max_output_tokens.
auxiliary_secretsobjectSecrets for non-LLM services. Currently tavily_api_key.
mcp_serversmapMCP server configs by name. Discriminated by transport: stdio, streamable_http, or sse.
agent_modelsmapUser-overridable model names per subagent capability.
model_defaultsmapFresh-session model + variant default per provider kind. Session resumes ignore this and use the persisted head.
+
+
+ +
+
+

Environment Variables

+

The CLI reads a small set of environment variables; most are overrides for paths or behavior the config file doesn't cover.

+
+
+ + + + + + + + + + + + + + + + + +
VariablePurpose
LASH_HOMEOverride the root data directory (defaults to ~/.lash/).
OPENAI_API_KEY / OPENAI_COMPATIBLE_API_KEYProvider API keys used during setup or one-shot sessions when no stored key is configured.
OPENROUTER_API_KEYOpenRouter API key accepted as an OpenAI-compatible shortcut when --base-url points at https://openrouter.ai/api/v1.
TAVILY_API_KEYTavily API key for the bundled web search tool. Alternative to --tavily-api-key and the config file.
LASH_GOOGLE_CLIENT_ID / LASH_GOOGLE_CLIENT_SECRETOverride the OAuth client credentials used by the Google / Code Assist provider.
CODE_ASSIST_ENDPOINT / CODE_ASSIST_API_VERSIONOverride the Google Code Assist API endpoint and version for provider integration testing.
LASH_COPY_BINDINGOverride the copy keystroke: ctrl-shift-c (default) or ctrl-y. Ctrl+C stays reserved for cancel semantics, so ctrl-c falls back to the default.
LASH_LOGtracing-subscriber log filter (e.g. debug, warn, lash=trace).
LASH_NO_OSC52Disable OSC 52 clipboard escape sequences.
LASH_INSTALL_DIROverride the install dir used by --update.
LASH_REPOOverride the GitHub repo used by --check-update / --update.
LASH_TOKIO_STACK_BYTESOverride the worker-thread stack size used by the CLI Tokio runtime.
GOOGLE_CLOUD_PROJECT[_ID]Google Cloud project ID for the Gemini / Code Assist provider.
+
+
+ +
+
+

Resume & Fork

+

Every session has a UUID and a generated human-readable name. --resume <id_or_name> accepts either, plus the legacy .db filename for old sessions. Without an argument, /resume opens an interactive picker showing the 50 most recent sessions sorted by recency. The picker hides zero-turn sessions when any non-empty session exists; direct /resume <id-or-name> may still target any session.

+
+

/fork branches the current session at the current message and opens the fork in a new terminal window. Lash resolves the launcher from the running executable and falls back to finding lash on $PATH if that binary isn't launchable. The launched fork runs lash --resume <new-session-id> in the new terminal.

+
+
+ + + + diff --git a/docs/publishing.html b/docs/publishing.html new file mode 100644 index 0000000..37d366d --- /dev/null +++ b/docs/publishing.html @@ -0,0 +1,45 @@ + + + + + + +publishing lash CLI + + + + + + + +
+

cli/publishing

+

CLI releases are manual, reproducible GitHub releases cut only from a green commit on main. They are independent from Lash SDK releases.

+ +
+
+
+

Release contract

+
    +
  1. Merge a focused pull request into main after required CI passes.
  2. +
  3. Ensure the releasable commit range contains a Release-Notes: commit section.
  4. +
  5. Dispatch the repository's Release workflow. Leave release_sha blank for current main, or provide a green ancestor commit.
  6. +
  7. The workflow validates ancestry and successful push CI, creates the next tag if needed, stamps the binary version, builds every supported target, and publishes the release.
  8. +
+

Do not create release tags or upload artifacts by hand.

+
+
+

Published assets

+
+

Binaries

lash-linux-x86_64.tar.gz, lash-linux-aarch64.tar.gz, and lash-macos-aarch64.tar.gz.

+

Integrity + install

SHA256SUMS covers the binary archives. install_lash.sh is rendered for the exact release tag and installs the matching asset.

+
+
+
+

Version relationship

+

The CLI records one exact Lash Git revision in its root Cargo.toml. Updating that revision is an explicit compatibility change with its own checks. CLI and SDK version numbers do not need to advance together.

+
+
+ + + diff --git a/docs/styles.css b/docs/styles.css new file mode 100644 index 0000000..930a1f3 --- /dev/null +++ b/docs/styles.css @@ -0,0 +1,61 @@ +:root { + color-scheme: dark light; + --bg: #141412; + --panel: #1e1e1a; + --panel-2: #27271f; + --text: #f3ead8; + --muted: #b7ad99; + --line: #474333; + --accent: #e9b44c; + --code: #0d0d0c; + --bg-elev: var(--panel); + --bg-faint: var(--panel-2); + --ink: var(--text); + --ink-mid: var(--muted); + --ink-dim: var(--muted); + --ink-faint: var(--muted); + --rule: var(--line); + --rule-strong: var(--line); + --font-display: "Big Shoulders Display", sans-serif; + --font-body: Spectral, Georgia, serif; + --font-mono: "Chivo Mono", monospace; + --page-max: 1120px; + font-family: Spectral, Georgia, serif; +} + +* { box-sizing: border-box; } +html { scroll-behavior: smooth; } +body { margin: 0; background: var(--bg); color: var(--text); line-height: 1.65; } +a { color: var(--accent); } +.skip-link { position: absolute; left: -9999px; } +.skip-link:focus { left: 1rem; top: 1rem; z-index: 2; } +.opener, .body, footer { width: min(1120px, calc(100% - 2rem)); margin-inline: auto; } +.opener { padding: 5rem 0 2.5rem; border-bottom: 1px solid var(--line); } +.opener__title { margin: 0; font: 800 clamp(3.4rem, 10vw, 7.5rem)/.82 "Big Shoulders Display", sans-serif; letter-spacing: -.035em; text-transform: uppercase; } +.slash { color: var(--accent); } +.opener__lede { max-width: 78ch; color: var(--muted); font-size: 1.15rem; } +.doc-links { display: flex; flex-wrap: wrap; gap: .75rem 1.25rem; font: 500 .85rem/1.4 "Chivo Mono", monospace; text-transform: uppercase; } +.body { padding: 1rem 0 5rem; } +.section { padding: 2.5rem 0; border-bottom: 1px solid var(--line); } +.section-header { max-width: 78ch; } +h2 { margin: 0 0 .5rem; font: 700 clamp(2rem, 5vw, 3.2rem)/1 "Big Shoulders Display", sans-serif; text-transform: uppercase; } +h3 { font-family: "Big Shoulders Display", sans-serif; font-size: 1.45rem; } +.two-col, .def-list-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); gap: 1rem; margin-top: 1.25rem; } +.panel { padding: 1rem 1.15rem; background: var(--panel); border: 1px solid var(--line); border-radius: .35rem; } +.panel h3:first-child { margin-top: 0; } +.table { overflow-x: auto; border: 1px solid var(--line); border-radius: .35rem; } +table { width: 100%; border-collapse: collapse; font-size: .95rem; } +th, td { padding: .7rem .8rem; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; } +th { background: var(--panel-2); font: 500 .75rem/1.3 "Chivo Mono", monospace; text-transform: uppercase; } +code, kbd, pre { font-family: "Chivo Mono", ui-monospace, monospace; } +code, kbd { padding: .1rem .3rem; background: var(--code); border: 1px solid var(--line); border-radius: .2rem; font-size: .82em; } +pre { overflow-x: auto; padding: 1rem; background: var(--code); border: 1px solid var(--line); border-radius: .35rem; } +pre code { padding: 0; border: 0; background: none; } +.def-list dt { margin-top: .75rem; font-weight: 600; } +.def-list dd { margin-left: 0; color: var(--muted); } +.micro-label { font: 500 .72rem/1.3 "Chivo Mono", monospace; color: var(--muted); text-transform: uppercase; } +footer { padding: 2rem 0 4rem; color: var(--muted); } + +@media (prefers-color-scheme: light) { + :root { --bg: #f5efe0; --panel: #fffaf0; --panel-2: #ebe1cc; --text: #25231e; --muted: #615b4f; --line: #cfc2a8; --accent: #8d5500; --code: #eee5d4; } +} diff --git a/scripts/check_docs.py b/scripts/check_docs.py new file mode 100644 index 0000000..cffdeff --- /dev/null +++ b/scripts/check_docs.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Validate local links in the dependency-free CLI documentation site.""" + +from __future__ import annotations + +from html.parser import HTMLParser +from pathlib import Path +from urllib.parse import urlsplit + + +ROOT = Path(__file__).resolve().parents[1] +DOCS = ROOT / "docs" + + +class LinkParser(HTMLParser): + def __init__(self) -> None: + super().__init__() + self.links: list[str] = [] + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + attr = "href" if tag in {"a", "link"} else "src" if tag == "script" else None + if attr is None: + return + values = dict(attrs) + if values.get(attr): + self.links.append(values[attr] or "") + + +def target_for(page: Path, link: str) -> Path | None: + parsed = urlsplit(link) + if parsed.scheme or parsed.netloc or link.startswith("#"): + return None + raw_path = parsed.path + if not raw_path: + return None + target = (page.parent / raw_path).resolve() + if raw_path.endswith("/"): + target /= "index.html" + return target + + +def main() -> None: + failures: list[str] = [] + pages = sorted(DOCS.rglob("*.html")) + if not pages: + raise SystemExit("no documentation pages found") + for page in pages: + parser = LinkParser() + parser.feed(page.read_text(encoding="utf-8")) + for link in parser.links: + target = target_for(page, link) + if target is not None and not target.exists(): + failures.append(f"{page.relative_to(ROOT)}: missing {link}") + if failures: + raise SystemExit("\n".join(failures)) + print(f"validated {len(pages)} documentation pages") + + +if __name__ == "__main__": + main()