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