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