@setoelkahfi / sigit / commits / 4b8e7fd

chore(skills): add run-sigit skill to build and drive the binary

ACP driver (driver.mjs), TUI tmux smoke test (tui-smoke.sh), and SKILL.md documenting how to build, launch, and drive sigit without triggering on-device inference. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

paydii committed Jun 27, 2026 at 08:38 UTC 4b8e7fdb42fcb67b36f97db368cb0967b2be32c9
3 files changed +301
.claude/skills/run-sigit/SKILL.md
+134
new file mode 100644 index 0000000..a681c09 --- /dev/null +++ b/.claude/skills/run-sigit/SKILL.md @@ -0,0 +1,134 @@ +--- +name: run-sigit +description: Build, launch, and drive the sigit AI coding agent — run the ACP server, screenshot the interactive TUI, smoke-test the CLI. Use when asked to run sigit, start the agent, screenshot the chat UI, or verify a change to the binary. +--- + +# Run sigit + +`sigit` is a single Rust binary that picks its mode at startup from whether stdin +is a TTY: + +- **ACP mode** (stdin not a TTY): newline-delimited JSON-RPC 2.0 over stdio — the + Agent Client Protocol surface that Zed / VS Code drive. This is the primary + programmatic handle. Drive it with **`.claude/skills/run-sigit/driver.mjs`**. +- **Interactive TUI** (stdin is a TTY): a full-screen ratatui chat, Unix-only. + Drive it under tmux with **`.claude/skills/run-sigit/tui-smoke.sh`**. +- **CLI subcommands**: `sigit login | logout | whoami`, handled before the split. + +Both drivers avoid on-device inference: `initialize`, `session/new`, and slash +commands (`/whoami`, `/help`, `/status`) answer **without** loading a multi-GB +GGUF model, so they work on a clean machine with nothing cached and no network. + +Paths below are relative to the repo root (`<unit>/`). + +## Prerequisites + +- Rust toolchain (pinned in `rust-toolchain.toml`); `cargo` on PATH. +- Node ≥ 18 for the ACP driver (`driver.mjs`). +- `tmux` for the TUI smoke test only: `brew install tmux` (macOS) / + `apt-get install -y tmux` (Linux). + +## Build + +```bash +cargo build # debug binary at target/debug/sigit +``` + +First build is slow (it compiles `onde` / mistralrs); incremental rebuilds are +sub-second. Use `cargo build --release` for `target/release/sigit` if you want +realistic inference speed — the drivers default to the debug binary. + +## Run (agent path) + +### ACP server — `driver.mjs` + +Spawns the binary in ACP mode, runs `initialize` → `session/new` → +`session/prompt /whoami`, prints every frame, exits 0 on success: + +```bash +node .claude/skills/run-sigit/driver.mjs +# SIGIT_BIN=target/release/sigit node .claude/skills/run-sigit/driver.mjs +``` + +Expected tail: + +``` +<-- notify session/update "Signed in to siGit Code Cloud as demo@sigit.si." + "stopReason": "end_turn" +OK — ACP handshake, session, and /whoami round-tripped. +``` + +The `/whoami` reply arrives as an `agent_message_chunk` notification — the same +streaming surface a real prompt fans out across many chunks. To drive real +streamed inference, send a `session/prompt` with ordinary text instead of a +slash command (needs a cached local model or a signed-in cloud tier). + +### Interactive TUI — `tui-smoke.sh` + +Launches the TUI under tmux, types `/help`, writes the rendered screen to +`$TMPDIR/sigit-tui.txt` (the "screenshot" for a terminal app), then quits: + +```bash +.claude/skills/run-sigit/tui-smoke.sh +cat "${TMPDIR:-/tmp}/sigit-tui.txt" # view the captured screen +``` + +To poke it by hand, the same tmux moves the script automates: + +```bash +tmux new-session -d -s sigit -x 120 -y 35 +tmux send-keys -t sigit './target/debug/sigit' Enter +sleep 6 +tmux capture-pane -t sigit -p # read the screen +tmux send-keys -t sigit '/help' Enter +tmux send-keys -t sigit C-c # Ctrl+C quits +tmux kill-session -t sigit +``` + +### CLI smoke + +```bash +./target/debug/sigit whoami # prints the signed-in account, exit 0 +``` + +## Run (human path) + +```bash +cargo run # stdin is your TTY → launches the TUI +``` + +A full-screen chat opens; type a message or `/help`, Ctrl+C to quit. Useless +headless or with stdin piped — that path falls through to ACP mode instead. + +## Gotchas + +- **The ACP server never exits on stdin EOF.** `printf '…' | sigit | head` hangs: + the process stays alive holding stdout open, so `head` blocks waiting for bytes + that only stop when you kill it. You must read the response frame and then + `kill` the child — that's the whole reason `driver.mjs` exists instead of a + one-line pipe. +- **Piping stdin forces ACP mode.** Any non-TTY stdin (a pipe, `</dev/null`) + routes to the JSON-RPC server, not the TUI. The TUI needs a real PTY, hence + tmux. +- **Handshake is intentionally model-free.** `initialize` / `session/new` defer + GGUF loading to the first real prompt, so they're fast and need no network. A + text `session/prompt` to an on-device model triggers a ~1–2 GB download on + first use. +- **The default model depends on sign-in state.** On a signed-in machine the + picker shows a cloud tier (e.g. `onde-cloud (onde-fast)`); logged out it + defaults to an on-device model. `sigit whoami` shows which. +- **Logs go to different places per mode.** ACP mode → stderr (the driver prefixes + them `[sigit]`). TUI mode redirects all stdout/stderr to `$TMPDIR/sigit.log` so + the ratatui surface stays clean — tail that file to debug the TUI. +- **macOS model cache is shared with the desktop app**, under + `~/Library/Group Containers/group.com.ondeinference.apps/models/`; other + platforms use `~/.cache/huggingface/`. + +## Troubleshooting + +- `binary not found: target/debug/sigit` → run `cargo build` first. +- Driver hangs / times out on `initialize` → you're likely running a stale binary + or one that crashed at startup; check the `[sigit]` stderr lines it echoes. +- `tmux not installed` from `tui-smoke.sh` → `brew install tmux`. +- TUI capture is blank → increase the `sleep` before `capture-pane`; the banner + and (lazy) model selection take a few seconds on a cold start.
.claude/skills/run-sigit/driver.mjs
+126
new file mode 100755 index 0000000..88320ab --- /dev/null +++ b/.claude/skills/run-sigit/driver.mjs @@ -0,0 +1,126 @@ +#!/usr/bin/env node +// ACP driver for the `sigit` binary. +// +// `sigit` runs as an Agent Client Protocol server when stdin is NOT a TTY +// (newline-delimited JSON-RPC 2.0 over stdio — the same surface Zed / VS Code +// drive). This script spawns the binary in that mode, runs a scripted handshake, +// prints every request/response/notification, and exits non-zero on failure. +// +// It deliberately avoids triggering on-device inference: `initialize`, +// `session/new`, and slash commands like `/whoami` and `/status` answer without +// loading a multi-GB GGUF model, so the driver works on a clean machine with no +// model cached and no network. +// +// Usage: +// node driver.mjs [path-to-binary] # default: target/debug/sigit +// SIGIT_BIN=target/release/sigit node driver.mjs +// +// Exit code 0 = every step got a well-formed JSON-RPC result. + +import { spawn } from "node:child_process"; +import { createInterface } from "node:readline"; +import { existsSync } from "node:fs"; + +const bin = process.argv[2] || process.env.SIGIT_BIN || "target/debug/sigit"; +if (!existsSync(bin)) { + console.error(`binary not found: ${bin} — run \`cargo build\` first`); + process.exit(2); +} + +// Force ACP mode regardless of how the driver itself was launched: pipe stdin so +// the child's stdin is not a TTY. +const child = spawn(bin, [], { stdio: ["pipe", "pipe", "pipe"] }); + +// Surface the agent's own logs (it writes them to stderr in ACP mode). +createInterface({ input: child.stderr }).on("line", (l) => + console.error(`[sigit] ${l}`), +); + +const pending = new Map(); // id -> {resolve, method} +const notifications = []; +let nextId = 1; +let failed = false; + +createInterface({ input: child.stdout }).on("line", (line) => { + line = line.trim(); + if (!line) return; + let msg; + try { + msg = JSON.parse(line); + } catch { + console.error(`<-- (non-JSON) ${line}`); + return; + } + if (msg.id !== undefined && (msg.result !== undefined || msg.error)) { + const waiter = pending.get(msg.id); + console.log(`<-- response #${msg.id} (${waiter?.method ?? "?"})`); + console.log(JSON.stringify(msg.result ?? msg.error, null, 2)); + if (msg.error) failed = true; + waiter?.resolve(msg); + pending.delete(msg.id); + } else if (msg.method) { + // A notification or a server->client request. We only observe these. + notifications.push(msg); + const update = msg.params?.update; + let detail = ""; + if (update?.sessionUpdate === "agent_message_chunk") { + // The streamed assistant text — one chunk per AgentMessageChunk. With the + // streaming backend a real prompt produces many of these. + detail = ` ${JSON.stringify(update.content?.text ?? update.content)}`; + } else if (update?.sessionUpdate) { + detail = ` (${update.sessionUpdate})`; + } + console.log(`<-- notify ${msg.method}${detail}`); + } +}); + +function send(method, params) { + const id = nextId++; + const req = { jsonrpc: "2.0", id, method, params }; + console.log(`--> request #${id} ${method}`); + child.stdin.write(JSON.stringify(req) + "\n"); + return new Promise((resolve, reject) => { + pending.set(id, { resolve, method }); + setTimeout(() => { + if (pending.has(id)) { + pending.delete(id); + reject(new Error(`timeout waiting for ${method} (#${id})`)); + } + }, 20_000); + }); +} + +async function main() { + // 1. Handshake. + await send("initialize", { + protocolVersion: 1, + clientCapabilities: {}, + }); + + // 2. Open a session rooted at the repo. `cwd` must be absolute. + const sessionRes = await send("session/new", { + cwd: process.cwd(), + mcpServers: [], + }); + const sessionId = sessionRes.result?.sessionId; + if (!sessionId) throw new Error("session/new returned no sessionId"); + + // 3. Drive a no-inference slash command through the prompt surface. `/whoami` + // reports the signed-in account; it never touches the model. + await send("session/prompt", { + sessionId, + prompt: [{ type: "text", text: "/whoami" }], + }); + + console.log("\nOK — ACP handshake, session, and /whoami round-tripped."); +} + +main() + .catch((err) => { + console.error(`FAILED: ${err.message}`); + failed = true; + }) + .finally(() => { + child.kill("SIGTERM"); + setTimeout(() => process.exit(failed ? 1 : 0), 150); + });
.claude/skills/run-sigit/tui-smoke.sh
+41
new file mode 100755 index 0000000..d0b46c8 --- /dev/null +++ b/.claude/skills/run-sigit/tui-smoke.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Drive the interactive ratatui TUI under tmux: launch it, type /help, dump the +# rendered screen to a file ("screenshot" for a terminal app), then quit cleanly. +# +# The TUI only starts when stdin is a real TTY, so it must run inside a terminal +# multiplexer — tmux gives us one plus `capture-pane` to read what it drew. +# Requires tmux (`brew install tmux`). Unix only (the TUI is #[cfg(unix)]). +# +# Usage: .claude/skills/run-sigit/tui-smoke.sh [path-to-binary] +set -euo pipefail + +BIN="${1:-${SIGIT_BIN:-target/debug/sigit}}" +SESSION="sigit-smoke-$$" +OUT="${TMPDIR:-/tmp}/sigit-tui.txt" + +if [[ ! -x "$BIN" ]]; then + echo "binary not found/executable: $BIN — run \`cargo build\` first" >&2 + exit 2 +fi +command -v tmux >/dev/null || { echo "tmux not installed (brew install tmux)" >&2; exit 2; } + +cleanup() { tmux kill-session -t "$SESSION" 2>/dev/null || true; } +trap cleanup EXIT + +tmux new-session -d -s "$SESSION" -x 120 -y 35 +tmux send-keys -t "$SESSION" "$BIN" Enter +sleep 6 # banner + (lazy) model selection +tmux send-keys -t "$SESSION" '/help' Enter +sleep 2 +tmux capture-pane -t "$SESSION" -p > "$OUT" +tmux send-keys -t "$SESSION" C-c # Ctrl+C quits +sleep 1 + +echo "Captured TUI screen -> $OUT" +if grep -q '/whoami' "$OUT"; then + echo "OK — TUI launched and /help rendered." +else + echo "FAILED — /help output not found in capture:" >&2 + sed '/^$/d' "$OUT" | head -40 >&2 + exit 1 +fi