scsh 1.9.2

Scoped Skills Helper — preflight a git repo and run its scoped skills in ephemeral containers.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
# Contributing to `scsh`

`scsh` (**Scoped Skills Helper**) is a single, self-contained Rust binary that
preflight-checks a git repository, then builds one **in-memory Dockerfile** and
runs the repo's *scoped skills* — in parallel, each in its own ephemeral container
under its configured harness. This document is
the shared playbook for working in this repository: how it's laid out, the house
style, the conventions every commit so far has followed, and the one piece of
terminology that trips everyone up (see [the `tmp/` rule](#the-tmp-rule) — read
it first).

If you only remember three things:

1. **`tmp/` always means the repo subdirectory, never the system temp dir.**
2. **Pure logic stays separate from side effects, and everything is tested.**
3. **The root crate keeps its logic dependency-free (crates: `crossterm`+`console` for
   the UI, `signal-hook` for catching SIGINT/SIGTERM safely); only commit when asked.**

Agents working in this repo must also:

- **Never pipe agent-run commands through `| tail`** (or `| head`, or similar truncators).
  Run `cargo test`, `cargo build`, `scsh run`, and everything else **without** truncating
  stdout/stderr — the human wants to see the full output. Use `tee` to a file if you need
  a log *and* live output; do not substitute `tail` for watching a run.
- **Push warning-free code.** `cargo build`, `cargo build --release`, and `cargo test` must
  complete with **zero compiler warnings** in the code you commit (release is what
  `cargo install --path …` builds); fix or allow-list deliberately, never ship new
  `dead_code` / `unused` noise.
- **Use reasonable test timeouts.** Unit tests need no external deps — cap agent-run
  `cargo test` at **~30 seconds** unless you're running the full integration suite (then
  a few minutes is fine, but still bounded).

---

## The `tmp/` rule

> **Read this once and never forget it.** In this repository, whenever anyone
> writes or says **`tmp/`** they mean the **`tmp/` subdirectory of this repo**
> (`<repo-root>/tmp/`). We are **never** talking about the operating system's
> system-wide temp directory. These are two different places and conflating them
> causes real bugs.

There are genuinely two distinct things, and they are easy to mix up:

| Notation | What it is | Who uses it |
| --- | --- | --- |
| **`tmp/`** | The **`tmp/` subdirectory of this repository** (`<repo>/tmp/`). | Where `scsh` copies a skill's collected `result` back into your repo. **Gitignored.** |
| "the system temp dir" | The OS scratch area (often `/tmp` on Linux/macOS). | Where `scsh` creates a per-run clone dir, `scsh-YYYYMMDD-HHMMSS-utc-run-<skill>`, and builds the image. |

### Why this is confusing (and why it's still correct)

The repository's `.gitignore` ignores the repo's `tmp/` with a single line:

```gitignore
# scsh uses the system temp dir for container build scratch; never track a local /tmp.
/tmp
```

That line **looks like** an absolute system path, but it is not. In gitignore
syntax a **leading slash anchors the pattern to the repository root**, so `/tmp`
matches **`<repo>/tmp/`** — the repo subdirectory — and has nothing to do with
the OS `/tmp`. Git never ignores files outside the work tree anyway.

So:

- A skill's `result:` (e.g. `tmp/ab_result.json`) is written **into the repo's
  `tmp/`**, where it can't be accidentally committed because that directory is
  gitignored.
- `scsh`'s per-run clone and build scratch live in **the system temp dir**, a
  separate place that git neither sees nor tracks.

### It's verified, and it must stay verified

This isn't theoretical — it's enforced and checked:

- **A real `scsh` run refuses to proceed** unless the repo's `tmp/` is gitignored.
  Just before the container steps, `scsh` runs `git check-ignore` and stops with an
  actionable hint if the guard fails. (`scsh list` never runs, so it skips the guard.)
- **A real run also refuses unless the working tree is clean.** Each skill runs on
  a clone of *committed* state, so any uncommitted change (staged, unstaged, or
  untracked) would be absent from the container; `scsh` lists the offending paths and
  says to commit or stash them. (`scsh list` skips this too.)
- You can confirm it at any time:

  ```console
  $ git check-ignore -v tmp/
  .gitignore:4:/tmp	tmp/

  $ git ls-files | grep -E '(^|/)tmp/'    # nothing tracked under any tmp/
  $
  ```

**When writing docs, comments, or commit messages:** say "the system temp dir"
(not a bare `tmp/`) when you mean the OS scratch area, and reserve **`tmp/`** for
the repo subdirectory. Any path a skill writes *back into the repo* belongs under
`tmp/` precisely because it is gitignored.

---

## Repository layout

This repo holds a few kinds of thing, deliberately kept apart:

```
.
├── Cargo.toml, build.rs, src/, tests/, README.md, rustfmt.toml  # ← the scsh crate (repo root)
├── .gitignore                                                   # /target, /tmp (Cargo.lock IS committed)
├── DEMO.md                                                     # the guided, agent-followed demo
├── .skills/                                                     # canonical agent skills (source of truth)
│   ├── README.md
│   ├── add/ · multiply/                                         # example skills (the init-demo project)
│   ├── scsh-harness-demo-and-selftest/                         # bundled: follows DEMO.md to demo + self-test
└── .claude/skills → ../.skills                                 # symlinked host discovery path
```

### The root crate

The **primary `scsh` binary lives at the repository root** (`src/main.rs`,
`src/config.rs`, `src/runtime.rs`, `src/ui/`, `tests/cli.rs`). The root crate is
the product; everything else supports it.

### `.skills/` — agent skills

`.skills/` is the **single source of truth** for repo skills. Each skill is a
folder containing `SKILL.md` (YAML frontmatter + markdown body) plus optional
`scripts/`, `references/`, `assets/`. The tool-specific discovery paths
(`.claude/skills`, `.cursor/skills`, `.opencode/skills`, `.agents/skills`, …) are
**symlinks** to `.skills/`, so one edit updates every host. See
[`.skills/README.md`](.skills/README.md) for the full table. Rules:

- **Author in `.skills/<name>/`** — never in the symlinked tool paths.
- The **folder name must equal the `name` in the frontmatter.**
- The example skills illustrate the env-spec conventions and are what
  `scsh init-demo-project` scaffolds: `add` sums `A`+`B` (defaults `2`,`3`,
  injected by `scsh`), and `multiply` multiplies `X`·`Y` with **no defaults**  it lives in the `multiply` profile and `scsh` refuses it if either `X` or `Y`
  is unset. `scsh-harness-demo-and-selftest` is the agent-followed walkthrough of
  [`DEMO.md`]DEMO.md, and is the one skill `scsh installskills` bundles.

- **Prefer a shipped script over harness-authored code.** When a skill needs a
  deterministic computation or a fixed multi-step operation, write a small script (e.g.
  Python via `#!/usr/bin/env python3`) under the skill's `scripts/` and have its `SKILL.md`
  tell the harness to **run** it — don't ask the harness to write Python or bash on the fly.
  A shipped script is reviewable, testable, and saves the model from re-deriving (and maybe
  getting wrong) the same logic each run. The `add`/`multiply` examples do this
  (`scripts/add.py`, `scripts/multiply.py`).

## Development environment

- **Rust toolchain** (`cargo`) — the root crate targets `edition = "2021"` and its
  dependencies are `crossterm` + `console` (the interactive live board) and `signal-hook`
  (to catch SIGINT/SIGTERM safely — std has no signal API); all of its own logic is
  standard-library only, so the binary stays self-contained.
- **`git`** on `PATH` — required by `scsh` itself and by the integration tests.
- **A container runtime** for real runs and for integration-test preflight:
  Apple `container``docker``podman` on macOS; `docker``podman`
  elsewhere. Override with `SCSH_RUNTIME=<docker|podman|container>`.
- **Network** only for a *real* container run (it pulls the base image and
  installs opencode). Building, `scsh list`, and the whole test suite
  need no network.

---

## Build, demo, and test

```sh
cargo build --release          # binary at target/release/scsh
cargo test                      # unit + integration tests
cargo fmt                       # format per rustfmt.toml (run before committing)
```

To exercise the tool end to end, follow [`DEMO.md`](DEMO.md) — hand it to an agent
from an empty directory and it builds and runs a tiny `scsh` project (see below).

### Formatting

Formatting is governed by [`rustfmt.toml`](rustfmt.toml): **2-space indent**, no
hard tabs, `max_width = 120`, `use_small_heuristics = "Max"`, compressed fn
params. Run `cargo fmt` before you commit — diffs are expected to be
already-formatted.

### Markdown: always backtick `scsh`

In **every** Markdown file in this repo, the tool name **`scsh` must be written in
backticks** (inline code: `` `scsh` ``) — never bare. It is a command, not an English
word, and the monospace makes that obvious at a glance. The same goes for its
subcommands and config in prose (`` `scsh run` ``, `` `.scsh.yml` ``, the skill names
`` `add` ``, …). The only exception is inside fenced code blocks, where everything
is already monospace. New docs and edits must keep this consistent.

### Tests

- **Unit tests** live inline in `src/config.rs`, `src/runtime.rs`, `src/main.rs`,
  `src/json.rs`, `src/sha256.rs`, `src/daemon/` (model, JSON I/O, client), and the `src/ui/`
  modules, and cover the pure logic: the
  YAML-subset parser, schema validation, runtime-detection ordering, `which`, Dockerfile
  generation, shell quoting, the smart elapsed clock, output-line cleanup, build-command
  detection, the engine start-command advice, commit integration (rebase / fallback-branch
  / run-twice), SHA-256 vectors, the result cache (key determinism/sensitivity,
  store/lookup/restore), and the live board's model (layout, scrolling, expand/collapse). They
  need nothing but `cargo` (and `git`, for the
  commit-integration and cache-key tests).
- **Integration tests** (`tests/cli.rs`) drive the compiled binary through the
  whole flow. They require `git` and a runtime on `PATH`, but **must never use
  the network**: every case stops at `scsh list` (or an earlier guard),
  so no image is pulled and no container is built.
- **Always report the passing test count in your commit body** — every
  substantive commit so far does (e.g. *"122 tests pass (unit + integration)"*).

### Test timeouts (agents)

When an agent runs tests, use **reasonable timeouts** — don't let a hung command
block the session indefinitely:

- **Unit tests** (pure logic, no container/network): **~30 seconds is enough**.
  Example: `cargo test <filter>` with a **30s** wall-clock cap. If unit tests
  exceed that, something is wrong — investigate, don't raise the timeout.
- **Full suite** (`cargo test`, unit + integration): allow more (e.g. **2–5
  minutes**) because integration tests spawn the binary and probe the runtime —
  but still cap it; a stuck test is a bug.
- **Real `scsh run` / review fleet / demo steps:** scale to the work (minutes are
  OK), but always run in the foreground with full output visible (see below).

Never run `cargo test` with no timeout at all in an agent session.

Daemon and other localhost HTTP tests must bind **`127.0.0.1:0`** (or pick an ephemeral
port the same way) — **never hard-code port `7274`** (the production default). Tests
should not fight a developer's running session browser.

### Watching long runs (tests, `scsh run`, review fleet)

When you wait for something that can take minutes — `cargo test`, a real
`scsh run`, the `code-review` fleet, a demo step — **keep output visible on the
terminal**. Do not hide progress behind a pipe or a file-only redirect:

- **Do not** pipe agent-run commands through **`tail`** (or `head`, `sed` line
  limits, etc.) — e.g. `cargo test 2>&1 | tail -20`, `scsh run … | tail -20`.
  Truncation hides failures, strips context, and defeats the point of running the
  command; **`tail` in a pipeline is never acceptable for agent-driven runs** in
  this repo.
- **Do not** redirect only to a file (e.g. `scsh run … > run.out 2>&1`) unless
  you also have a way to watch it (`tail -f run.out` in another pane, or prefer
  `tee` below).
- **Do** run in the **foreground** when you can — `scsh` is designed to show a
  live, collapsible board on a TTY.
- **Do** use **`tee`** when you also want a log file:
  `scsh run code-review 2>&1 | tee tmp/my-run/run.out`
- **Background only when necessary:** `nohup scsh run … >> tmp/my-run/run.out
  2>&1 &` then `disown`, record the PID, and monitor with `tail -f` on that
  file. A bare `&` in a short-lived shell (or an agent session that exits) can
  leave the run half-started with no completion line and no result JSON.

Same rule for agents following [`DEMO.md`](DEMO.md) or
[`code-beautiful-review`](.skills/code-beautiful-review/SKILL.md): never
substitute `| tail` (or any output truncator) for watching the run — not for
`scsh`, not for `cargo test`, not for anything else you execute on the user's behalf.

### Compiler warnings

Code we push must be **warning-free** in **dev and release** builds. Before committing,
run at least:

```sh
cargo build --release
cargo test
```

Both must report **no compiler warnings** from our code (not “we'll fix it later”).
`cargo install --path .` uses the release profile — if release warns, the installed
binary was built from a dirty tree. If a warning is intentional and unavoidable,
suppress it locally with `#[allow(...)]` and a one-line comment saying why — never
leave stray `dead_code`, `unused`, or `unused_mut` warnings in commits.

### Demo

[`DEMO.md`](DEMO.md) is the authoritative, English-language **demo**: an agent (or a
careful human) is handed the file **from an empty directory** and follows it to build a
tiny `scsh` project from scratch and run it — `init-demo-project` scaffolds and commits
`add`/`multiply`, `add` runs by default (defaults and forwarded values), `multiply` runs
under its profile with `X`/`Y`, and `scsh` **refuses** `multiply` when they're unset. The
happy path does a **real run** (container + model); only the refusal and `scsh list` are
network-free, so the demo still teaches even without a runtime. The
[`scsh-harness-demo-and-selftest`](.skills/scsh-harness-demo-and-selftest/SKILL.md) skill is
what an agent invokes to follow it. Don't assert the demo's real-run results in CI — keep
programmatic proof in `cargo test`; the demo is the human-facing, end-to-end story.

---

## Code conventions

These are the design rules the codebase already lives by — match them so new code
reads like it belongs.

- **Logic stays dependency-free in the root crate.** Its crates are
  `crossterm` + `console` (pure-Rust, the live UI) and `signal-hook` — a deliberate,
  called-out dependency for one thing: catching SIGINT/SIGTERM *safely* (std has
  no signal API; `signal-hook` wraps the OS bits in a safe API). scsh also isolates
  each child in its own process group via the safe `Command::process_group`.
  Everything else — including the `.scsh.yml` config (a small purpose-built parser, *not*
  a general YAML library), the JSON reader, and SHA-256 — is standard-library only.
  Reach for another crate only as a deliberate decision to call out in the commit,
  never a default; prefer std.
- **Separate pure logic from side effects.** `config.rs` (parse/validate) and
  `runtime.rs` (runtime detection, Dockerfile/command generation) are **pure and
  exhaustively unit-tested**; process spawning for git, the container runtime, and the
  session-browser daemon lives in `main.rs` and `src/daemon/`. This split is what lets the
  suite be thorough without mocking a container engine — preserve it. New side-effecting
  code goes in those modules; new logic goes in a pure, testable function.
- **Every failure is actionable.** Preflight and guard failures print exactly
  what's wrong (``) and a concrete fix (``). Schema validation reports **all**
  problems at once, not just the first. Hold any new error path to the same bar.
- **Strict, all-at-once validation.** Unknown keys, a missing `skills` block, wrong
  types, empty values, a malformed env spec, a result path that escapes the repo — all
  rejected, all listed together.
- **The skill never touches your working tree.** A run operates on a throwaway
  clone in the system temp dir; the only thing written back into your real repo
  is the collected `result`, and only into the gitignored `tmp/` (existing files
  are backed up to `<name>.bak.YYYYMMDD-HHMMSS-utc`, never clobbered). Don't add
  code paths that write elsewhere into the user's repo.
- **Least privilege.** The container runs as a non-root `agent` user whose
  UID/GID match the host user's, so files it writes in the mount are owned by you.
- **Match the surrounding style.** Follow the naming, comment density, and idiom
  of the file you're editing. Keep the README and `--help` in sync with behavior
  changes (the existing commits always do).

For the full runtime/container design (clone strategy, in-memory Dockerfile, the
opencode install layer, `--userns=keep-id`, result collection), see
[`README.md`](README.md) — don't duplicate it here; update it there.

---

## Output style

`scsh`'s terminal output should be **complete but compact** — show everything that
matters, without one `✓` line per micro-step. The guiding rules:

- **Group related facts onto one line**, joined with ` · `. The repo's git state is
  one line, the backend and its build are one line, credentials are one line.
- **Stay quiet on success, loud on failure.** The preflight checks (git → repo →
  `.scsh.yml` → schema → runtime) print *nothing* individually when they pass; they
  collapse into a single summary line. A failing check still prints its own
  actionable `✗ <what's wrong>` / `→ <how to fix>`, with the literal command to type
  rendered **bold** (`bold()` in `main.rs`).
- **Drop redundant lines.** "/tmp scratch ready" added nothing over "/tmp ignored",
  so it's gone. Don't restate what an adjacent line already implies.
- **Name the backend explicitly** so it's obvious what's running the containers:
  `using docker`, `using podman`, or `using Apple Containers` (Apple's `container`
  runtime — the default on macOS). See `backend_name()`.
- **One line per skill**, carrying its result; the final line is the overall verdict.
  A failed skill adds its run-dir and log pointers (for inspection), nothing more.

A real `scsh` run therefore reads like:

```text
✓ git · repo ~/1 · clean · /tmp ignored
✓ using docker · build 0.6s
✓ opencode creds found (forwarded into each skill)
✓ opencode: add  29s  2 + 3 = 5
✓ add: brought in 1 commit (rebased onto prod)
✓ all 1 skill completed successfully
```

`scsh list` (no run-only guards) collapses to one summary line instead —
`✓ git · repo ~/1 · .scsh.yml valid (1 skill: add) · using docker` — then lists the
skills by profile (`--verbose` adds the Dockerfile + plan). Keep new output faithful to
this shape: a reader should
learn the repo state, the backend, the credentials, and each skill's outcome, and
little else.

---

## Commits, branches, and pull requests

### Commit messages

Follow the established style (read `git log` for the canonical examples):

- **Imperative, specific subject line***"Add runtime detection and the
  in-memory build/run plan"*, *"Wire up preflight, the real run, and init-demo"*.
  Trivial mechanical changes may use a terse subject and no body (*"fmt"*).
- **A body that explains what changed and why**, usually as bullets, and that
  **states the passing test count**.
- **Do not add `Co-Authored-By` trailers** — this repository does not use them.

### Branches

Commit on top of the mainline (`prod` here) rather than spinning up side branches.
There is no configured remote — this is a local-first repository, so once history is
shared it is never rewritten.

### When to commit

**Only commit when explicitly asked.** Default behavior is to leave changes in the
working tree, unstaged, for review. Never push or publish without an explicit
request — those are the universal safety boundaries (along with anything
destructive or irreversible), and they hold regardless of any other instruction.

### Definition of done (PR checklist)

- [ ] `cargo fmt` is clean.
- [ ] `cargo build --release` and `cargo test` pass with **zero compiler warnings**.
- [ ] `cargo test` passes; the commit body states the count.
- [ ] [`DEMO.md`](DEMO.md) still reflects how `scsh` behaves (it's the human-facing demo).
- [ ] New errors are actionable (`` what's wrong / `` how to fix).
- [ ] README / `--help` updated to match any behavior change.
- [ ] The repo's `tmp/` is still gitignored (`git check-ignore -v tmp/`), and no
      build output, clones, or results are tracked.
- [ ] The root crate adds no new deps beyond `crossterm`/`console` (UI) and
      `signal-hook` (signals) unless deliberately called out.

---

## A note on terminology, one more time

If you take away nothing else: **`tmp/` is the repo's own gitignored
subdirectory.** The system temp dir is a separate place we call "the system temp
dir." Keep them straight in code, comments, docs, and commit messages.