@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 new
+134
@@ -0,0 +1,134 @@
1 +---
2 +name: run-sigit
3 +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.
4 +---
5 +
6 +# Run sigit
7 +
8 +`sigit` is a single Rust binary that picks its mode at startup from whether stdin
9 +is a TTY:
10 +
11 +- **ACP mode** (stdin not a TTY): newline-delimited JSON-RPC 2.0 over stdio — the
12 + Agent Client Protocol surface that Zed / VS Code drive. This is the primary
13 + programmatic handle. Drive it with **`.claude/skills/run-sigit/driver.mjs`**.
14 +- **Interactive TUI** (stdin is a TTY): a full-screen ratatui chat, Unix-only.
15 + Drive it under tmux with **`.claude/skills/run-sigit/tui-smoke.sh`**.
16 +- **CLI subcommands**: `sigit login | logout | whoami`, handled before the split.
17 +
18 +Both drivers avoid on-device inference: `initialize`, `session/new`, and slash
19 +commands (`/whoami`, `/help`, `/status`) answer **without** loading a multi-GB
20 +GGUF model, so they work on a clean machine with nothing cached and no network.
21 +
22 +Paths below are relative to the repo root (`<unit>/`).
23 +
24 +## Prerequisites
25 +
26 +- Rust toolchain (pinned in `rust-toolchain.toml`); `cargo` on PATH.
27 +- Node ≥ 18 for the ACP driver (`driver.mjs`).
28 +- `tmux` for the TUI smoke test only: `brew install tmux` (macOS) /
29 + `apt-get install -y tmux` (Linux).
30 +
31 +## Build
32 +
33 +```bash
34 +cargo build # debug binary at target/debug/sigit
35 +```
36 +
37 +First build is slow (it compiles `onde` / mistralrs); incremental rebuilds are
38 +sub-second. Use `cargo build --release` for `target/release/sigit` if you want
39 +realistic inference speed — the drivers default to the debug binary.
40 +
41 +## Run (agent path)
42 +
43 +### ACP server — `driver.mjs`
44 +
45 +Spawns the binary in ACP mode, runs `initialize` → `session/new` →
46 +`session/prompt /whoami`, prints every frame, exits 0 on success:
47 +
48 +```bash
49 +node .claude/skills/run-sigit/driver.mjs
50 +# SIGIT_BIN=target/release/sigit node .claude/skills/run-sigit/driver.mjs
51 +```
52 +
53 +Expected tail:
54 +
55 +```
56 +<-- notify session/update "Signed in to siGit Code Cloud as demo@sigit.si."
57 + "stopReason": "end_turn"
58 +OK — ACP handshake, session, and /whoami round-tripped.
59 +```
60 +
61 +The `/whoami` reply arrives as an `agent_message_chunk` notification — the same
62 +streaming surface a real prompt fans out across many chunks. To drive real
63 +streamed inference, send a `session/prompt` with ordinary text instead of a
64 +slash command (needs a cached local model or a signed-in cloud tier).
65 +
66 +### Interactive TUI — `tui-smoke.sh`
67 +
68 +Launches the TUI under tmux, types `/help`, writes the rendered screen to
69 +`$TMPDIR/sigit-tui.txt` (the "screenshot" for a terminal app), then quits:
70 +
71 +```bash
72 +.claude/skills/run-sigit/tui-smoke.sh
73 +cat "${TMPDIR:-/tmp}/sigit-tui.txt" # view the captured screen
74 +```
75 +
76 +To poke it by hand, the same tmux moves the script automates:
77 +
78 +```bash
79 +tmux new-session -d -s sigit -x 120 -y 35
80 +tmux send-keys -t sigit './target/debug/sigit' Enter
81 +sleep 6
82 +tmux capture-pane -t sigit -p # read the screen
83 +tmux send-keys -t sigit '/help' Enter
84 +tmux send-keys -t sigit C-c # Ctrl+C quits
85 +tmux kill-session -t sigit
86 +```
87 +
88 +### CLI smoke
89 +
90 +```bash
91 +./target/debug/sigit whoami # prints the signed-in account, exit 0
92 +```
93 +
94 +## Run (human path)
95 +
96 +```bash
97 +cargo run # stdin is your TTY → launches the TUI
98 +```
99 +
100 +A full-screen chat opens; type a message or `/help`, Ctrl+C to quit. Useless
101 +headless or with stdin piped — that path falls through to ACP mode instead.
102 +
103 +## Gotchas
104 +
105 +- **The ACP server never exits on stdin EOF.** `printf '…' | sigit | head` hangs:
106 + the process stays alive holding stdout open, so `head` blocks waiting for bytes
107 + that only stop when you kill it. You must read the response frame and then
108 + `kill` the child — that's the whole reason `driver.mjs` exists instead of a
109 + one-line pipe.
110 +- **Piping stdin forces ACP mode.** Any non-TTY stdin (a pipe, `</dev/null`)
111 + routes to the JSON-RPC server, not the TUI. The TUI needs a real PTY, hence
112 + tmux.
113 +- **Handshake is intentionally model-free.** `initialize` / `session/new` defer
114 + GGUF loading to the first real prompt, so they're fast and need no network. A
115 + text `session/prompt` to an on-device model triggers a ~1–2 GB download on
116 + first use.
117 +- **The default model depends on sign-in state.** On a signed-in machine the
118 + picker shows a cloud tier (e.g. `onde-cloud (onde-fast)`); logged out it
119 + defaults to an on-device model. `sigit whoami` shows which.
120 +- **Logs go to different places per mode.** ACP mode → stderr (the driver prefixes
121 + them `[sigit]`). TUI mode redirects all stdout/stderr to `$TMPDIR/sigit.log` so
122 + the ratatui surface stays clean — tail that file to debug the TUI.
123 +- **macOS model cache is shared with the desktop app**, under
124 + `~/Library/Group Containers/group.com.ondeinference.apps/models/`; other
125 + platforms use `~/.cache/huggingface/`.
126 +
127 +## Troubleshooting
128 +
129 +- `binary not found: target/debug/sigit` → run `cargo build` first.
130 +- Driver hangs / times out on `initialize` → you're likely running a stale binary
131 + or one that crashed at startup; check the `[sigit]` stderr lines it echoes.
132 +- `tmux not installed` from `tui-smoke.sh` → `brew install tmux`.
133 +- TUI capture is blank → increase the `sleep` before `capture-pane`; the banner
134 + and (lazy) model selection take a few seconds on a cold start.
.claude/skills/run-sigit/driver.mjs new
+126
@@ -0,0 +1,126 @@
1 +#!/usr/bin/env node
2 +// ACP driver for the `sigit` binary.
3 +//
4 +// `sigit` runs as an Agent Client Protocol server when stdin is NOT a TTY
5 +// (newline-delimited JSON-RPC 2.0 over stdio — the same surface Zed / VS Code
6 +// drive). This script spawns the binary in that mode, runs a scripted handshake,
7 +// prints every request/response/notification, and exits non-zero on failure.
8 +//
9 +// It deliberately avoids triggering on-device inference: `initialize`,
10 +// `session/new`, and slash commands like `/whoami` and `/status` answer without
11 +// loading a multi-GB GGUF model, so the driver works on a clean machine with no
12 +// model cached and no network.
13 +//
14 +// Usage:
15 +// node driver.mjs [path-to-binary] # default: target/debug/sigit
16 +// SIGIT_BIN=target/release/sigit node driver.mjs
17 +//
18 +// Exit code 0 = every step got a well-formed JSON-RPC result.
19 +
20 +import { spawn } from "node:child_process";
21 +import { createInterface } from "node:readline";
22 +import { existsSync } from "node:fs";
23 +
24 +const bin = process.argv[2] || process.env.SIGIT_BIN || "target/debug/sigit";
25 +if (!existsSync(bin)) {
26 + console.error(`binary not found: ${bin} — run \`cargo build\` first`);
27 + process.exit(2);
28 +}
29 +
30 +// Force ACP mode regardless of how the driver itself was launched: pipe stdin so
31 +// the child's stdin is not a TTY.
32 +const child = spawn(bin, [], { stdio: ["pipe", "pipe", "pipe"] });
33 +
34 +// Surface the agent's own logs (it writes them to stderr in ACP mode).
35 +createInterface({ input: child.stderr }).on("line", (l) =>
36 + console.error(`[sigit] ${l}`),
37 +);
38 +
39 +const pending = new Map(); // id -> {resolve, method}
40 +const notifications = [];
41 +let nextId = 1;
42 +let failed = false;
43 +
44 +createInterface({ input: child.stdout }).on("line", (line) => {
45 + line = line.trim();
46 + if (!line) return;
47 + let msg;
48 + try {
49 + msg = JSON.parse(line);
50 + } catch {
51 + console.error(`<-- (non-JSON) ${line}`);
52 + return;
53 + }
54 + if (msg.id !== undefined && (msg.result !== undefined || msg.error)) {
55 + const waiter = pending.get(msg.id);
56 + console.log(`<-- response #${msg.id} (${waiter?.method ?? "?"})`);
57 + console.log(JSON.stringify(msg.result ?? msg.error, null, 2));
58 + if (msg.error) failed = true;
59 + waiter?.resolve(msg);
60 + pending.delete(msg.id);
61 + } else if (msg.method) {
62 + // A notification or a server->client request. We only observe these.
63 + notifications.push(msg);
64 + const update = msg.params?.update;
65 + let detail = "";
66 + if (update?.sessionUpdate === "agent_message_chunk") {
67 + // The streamed assistant text — one chunk per AgentMessageChunk. With the
68 + // streaming backend a real prompt produces many of these.
69 + detail = ` ${JSON.stringify(update.content?.text ?? update.content)}`;
70 + } else if (update?.sessionUpdate) {
71 + detail = ` (${update.sessionUpdate})`;
72 + }
73 + console.log(`<-- notify ${msg.method}${detail}`);
74 + }
75 +});
76 +
77 +function send(method, params) {
78 + const id = nextId++;
79 + const req = { jsonrpc: "2.0", id, method, params };
80 + console.log(`--> request #${id} ${method}`);
81 + child.stdin.write(JSON.stringify(req) + "\n");
82 + return new Promise((resolve, reject) => {
83 + pending.set(id, { resolve, method });
84 + setTimeout(() => {
85 + if (pending.has(id)) {
86 + pending.delete(id);
87 + reject(new Error(`timeout waiting for ${method} (#${id})`));
88 + }
89 + }, 20_000);
90 + });
91 +}
92 +
93 +async function main() {
94 + // 1. Handshake.
95 + await send("initialize", {
96 + protocolVersion: 1,
97 + clientCapabilities: {},
98 + });
99 +
100 + // 2. Open a session rooted at the repo. `cwd` must be absolute.
101 + const sessionRes = await send("session/new", {
102 + cwd: process.cwd(),
103 + mcpServers: [],
104 + });
105 + const sessionId = sessionRes.result?.sessionId;
106 + if (!sessionId) throw new Error("session/new returned no sessionId");
107 +
108 + // 3. Drive a no-inference slash command through the prompt surface. `/whoami`
109 + // reports the signed-in account; it never touches the model.
110 + await send("session/prompt", {
111 + sessionId,
112 + prompt: [{ type: "text", text: "/whoami" }],
113 + });
114 +
115 + console.log("\nOK — ACP handshake, session, and /whoami round-tripped.");
116 +}
117 +
118 +main()
119 + .catch((err) => {
120 + console.error(`FAILED: ${err.message}`);
121 + failed = true;
122 + })
123 + .finally(() => {
124 + child.kill("SIGTERM");
125 + setTimeout(() => process.exit(failed ? 1 : 0), 150);
126 + });
.claude/skills/run-sigit/tui-smoke.sh new
+41
@@ -0,0 +1,41 @@
1 +#!/usr/bin/env bash
2 +# Drive the interactive ratatui TUI under tmux: launch it, type /help, dump the
3 +# rendered screen to a file ("screenshot" for a terminal app), then quit cleanly.
4 +#
5 +# The TUI only starts when stdin is a real TTY, so it must run inside a terminal
6 +# multiplexer — tmux gives us one plus `capture-pane` to read what it drew.
7 +# Requires tmux (`brew install tmux`). Unix only (the TUI is #[cfg(unix)]).
8 +#
9 +# Usage: .claude/skills/run-sigit/tui-smoke.sh [path-to-binary]
10 +set -euo pipefail
11 +
12 +BIN="${1:-${SIGIT_BIN:-target/debug/sigit}}"
13 +SESSION="sigit-smoke-$$"
14 +OUT="${TMPDIR:-/tmp}/sigit-tui.txt"
15 +
16 +if [[ ! -x "$BIN" ]]; then
17 + echo "binary not found/executable: $BIN — run \`cargo build\` first" >&2
18 + exit 2
19 +fi
20 +command -v tmux >/dev/null || { echo "tmux not installed (brew install tmux)" >&2; exit 2; }
21 +
22 +cleanup() { tmux kill-session -t "$SESSION" 2>/dev/null || true; }
23 +trap cleanup EXIT
24 +
25 +tmux new-session -d -s "$SESSION" -x 120 -y 35
26 +tmux send-keys -t "$SESSION" "$BIN" Enter
27 +sleep 6 # banner + (lazy) model selection
28 +tmux send-keys -t "$SESSION" '/help' Enter
29 +sleep 2
30 +tmux capture-pane -t "$SESSION" -p > "$OUT"
31 +tmux send-keys -t "$SESSION" C-c # Ctrl+C quits
32 +sleep 1
33 +
34 +echo "Captured TUI screen -> $OUT"
35 +if grep -q '/whoami' "$OUT"; then
36 + echo "OK — TUI launched and /help rendered."
37 +else
38 + echo "FAILED — /help output not found in capture:" >&2
39 + sed '/^$/d' "$OUT" | head -40 >&2
40 + exit 1
41 +fi