scsh 1.10.1

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
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
# Session browser daemon

`scsh` can run a small HTTP server on **localhost** (default port **7274** — `scsh` on a
numeric keypad) that collects events from every `scsh run` and renders them as an expandable
web UI. Terminal scrollback is painful for parallel container output; the session browser is
the easier way to read build logs, harness output, and per-skill results.

## Commands

```console
scsh daemon start      # persistent — runs until scsh daemon stop
scsh daemon stop       # stop the daemon
scsh daemon restart    # stop then start (persistent)
scsh daemon status     # exit 0 when the daemon is listening
```

During `scsh run`, if no persistent daemon is already running, `scsh` auto-starts an
**ephemeral** daemon. That daemon stays up while runs are active (with periodic pings) and
shuts down **five minutes** after the last client disconnects.

Every `scsh run` gets a session id of six random lowercase letters. The run prints its
clickable deep link URL twice — right after registering (watch live) and again as one of the
very last lines (so a coding agent relaying the tail of the output always surfaces it). It is
reachable while a daemon is listening on that port; start `scsh daemon start` for durable
post-run browsing, or rely on persisted state after `scsh daemon restart`:

```text
http://127.0.0.1:7274/session/abcdef
```

Open it in a browser to see image builds and skills as collapsible sections, with timestamped
harness output and container names.

## What is collected

| Event | Source |
| --- | --- |
| Image build start / output / success / failure | Live board build rows |
| Skill clone / harness phases | Proc notes |
| Container start / stop | Named container around each skill |
| Every stdout/stderr line | Build tail + harness tee (`scsh-run.log` stream) |
| Terminal recording (`.cast`) | asciinema PTY recording of each harness (see below) |

## Terminal recordings (asciinema)

Every harness runs inside a real PTY recorded by asciinema (size from the `terminal:`
block in `.scsh.yml`, default **200×50**). **All five harnesses** run as the genuine
end-to-end interactive TUI — the same screen a human would see: claude, codex, and cursor,
plus opencode (`opencode --prompt`) and grok (`grok "<prompt>"`, its default Build TUI). The
`scsh-tui-record` script (baked into the base image) runs the harness in a tmux session,
records the attached screen, and — when the skill's result file appears — sends the quit
keys (`/exit`, double Ctrl-C) and ends the session.

There is **no screen-scraping**: every consent, trust, and login prompt is skipped ahead
of time by a flag or seeded config, so the recording is clean and a stuck harness surfaces
as a timeout (a real setup bug) rather than being auto-clicked. Per harness:

- **claude**`--permission-mode acceptEdits` (the `bypassPermissions` consent screen has
  no non-interactive escape); onboarding + workspace trust seeded into the forwarded
  `.claude.json`.
- **codex**`--dangerously-bypass-approvals-and-sandbox`; `trust_level = "trusted"`
  appended to the forwarded `config.toml`.
- **cursor**`--force`; its `~/.cursor/projects/<repo-slug>/.workspace-trusted` marker
  pre-created in-container (`--trust` is print-mode-only, and there is no config key).

Missing/invalid credentials fail fast with a clear "log in on the host" error before any
container starts — scsh never tries to drive a login screen.

Each recorded skill — and each image build recorded on the host — is shown as an **inline
player** in the session page (a build falls back to a text log only when `asciinema` is
missing from PATH). The player is scsh's own **scsh-cast-player** — a first-party,
clean-room component (`src/daemon/html/player/`, MIT like the rest of scsh; no third-party
code or license rides in the browser UI). It has:

- **Playback** — play/pause, timeline scrubbing, and native keyboard: **space** pause,
  **←/→** seek, **&lt;/&gt;** speed, **[/]** jump between chapters (click the player first
  to focus it).
- **Fullscreen** — fills the viewport, fitting the terminal both horizontally and vertically.
- **Chapters** — if a cast has been annotated (below), its chapters show as **markers on the
  timeline** (YouTube-style) plus clickable chapter chips, with a one-sentence **summary**
  above the player.
- **Link at time** — copies a deep link to the standalone player at the current timestamp
  (`/cast/{session}/{proc}/play#t=<seconds>`).
- **⬇ .cast** — downloads the raw asciicast v3. Works **mid-run**: the recording is NDJSON,
  so the daemon serves the bytes written so far (truncated to the last complete line).
- **⬇ .html** — downloads the recording as **one self-contained offline HTML player page**
  (the same rendering `scsh export-cast` does, chapters sidecar folded in when present),
  named `<cast stem>.html`. Hidden until the recording has at least one complete frame —
  the export endpoint 404s on a frameless cast.
- **⬇ session .html** — in the session-page header: downloads **the entire session as one
  self-contained offline page** (`scsh-session-<id>.html`) — a summary header plus every
  recording embedded as its own per-cast export page (annotated casts keep their summary
  and chapters; procs without a recording become note rows). Shown whenever any proc has a
  registered cast.

## In-progress recordings

A run that just started has a registered cast with no complete frames yet — the player
does not error on it. Instead:

- **Placeholder** — until the recording has at least one complete event line, the player
  box (inline embed and standalone page alike) shows a calm *"Recording in progress — no
  frames yet."* note. The moment frames exist, it upgrades to a real player in place.
- **Reload suggestion** — while a proc runs, the daemon probes its recording cheaply on
  the WebSocket tick (a stat plus a tail-parse from a cached offset — tolerant of a
  truncated trailing line) and pushes `cast_growth` messages: `{ "type": "cast_growth",
  "session", "proc", "duration", "running" }`. The player shows an unobtrusive
  *"Recording grew: +Ns (loaded Xs, available Ys)"* banner whose Reload button re-fetches
  the cast and re-creates the player at your current position. No client-side polling —
  the server pushes, and only while someone is subscribed.
- **Live mode** — a **● Live** toggle (visible only while the proc runs) follows the tail:
  each growth notification reloads the cast, seeks to where the previous load ended, and
  plays the newly appended output. When the proc finishes, a final `running: false` notice
  ends live mode cleanly — one last reload picks up the complete cast and the toggle is
  disabled.

All of it degrades gracefully without the WebSocket: pages still load, finished casts
play exactly as before, and the manual ↻ reload button keeps working.

## Cast chapters & summaries (cursor / Composer)

After a run, if the `cursor-agent` CLI and a cursor login are present on the host, `scsh`
annotates each new recording: it renders the cast to a compact timestamped transcript, asks
cursor-agent on the **Composer** model for a one-sentence summary plus 3–8 chapters, and
writes a `<cast>.chapters.json` sidecar next to the recording. The player loads it from
`GET /cast/{session}/{proc}/chapters` (returns `{}` when absent). Annotate on demand with:

```console
scsh annotate-cast ~/.scsh/recordings/<recording>.cast     # override model via SCSH_ANNOTATE_MODEL
```

## Artifact formats

**Recording — asciicast v3** (`*.cast`). The [asciicast v3](https://docs.asciinema.org/manual/asciicast/v3/)
format: a header object on the first line, then one JSON array per line (NDJSON) — an output
event is `[<interval-seconds:number>, "o", "<text:string>"]` (times are intervals from the
previous event, not absolute). Being line-delimited is what makes a
partial file valid mid-run.

```jsonc
{"version": 3, "term": {"cols": 200, "rows": 50, "type": "xterm-256color"}, "timestamp": 1783108212}
[0.12, "o", "[?25lstarting…\r\n"]
[1.22, "o", "done\r\n"]
```

**Annotation sidecar** (`<cast-stem>.chapters.json`). Written by the annotation pass, served
at `/cast/{session}/{proc}/chapters`. The player uses `summary` for the caption and each
chapter as a timeline marker / jump target:

```jsonc
{
  "summary": "One sentence describing what the session did.",  // string, required
  "chapters": [                                                 // ascending by t; first is 0; [] allowed
    { "t": 0,    "title": "Startup" },      // t: seconds in (number, may be fractional); title: short label
    { "t": 6.5,  "title": "Read the skill" },
    { "t": 9.2,  "title": "Write result JSON" }
  ]
}
```

Field names are `snake_case`; the Rust source of truth for the shape is `CastAnnotation` /
`Chapter` in `src/annotate.rs`. An absent sidecar is served as `{}` (no summary, no chapters).

## Where artifacts live

While the container runs, the cast is served straight from the run dir
(`<run_dir>/tmp/scsh-run.log.cast`, bind-mounted and growing live). When the skill ends,
`scsh run` copies each run's artifacts into **`$SCSH_HOME`** (default `~/.scsh`) — the same
durable home the daemon store uses — so a throwaway caller clone (e.g. `code-beautiful-review`
under `tmp/`) cannot wipe session-exportable recordings:

| Artifact | Path |
| --- | --- |
| Recording | `~/.scsh/recordings/<stem>.cast` |
| Annotation sidecar | `~/.scsh/recordings/<stem>.chapters.json` |
| Harness run log | `~/.scsh/logs/<stem>.log` |
| Verbose debug log | `~/.scsh/logs/<stem>.debug.log` (claude/grok) · `~/.scsh/logs/<stem>.last.log` (codex) |

The stem is `<skill>-<YYYYMMDD-HHMMSS>-utc-<nonce>`. The timestamp alone is not unique — every
skill in one `scsh run` shares it — so the random nonce keeps same-second runs from overwriting
each other. Logs are kept for **every** run (including failures, when they matter most).

`~/.scsh/recordings/` is **permanent**: it holds only skill-run recordings, scsh never deletes
from it, and nothing else writes there — recordings stay until you remove them yourself.
Image-**build** casts are scratch by comparison and live apart in `~/.scsh/casts/`; clean
`~/.scsh/casts/` and `~/.scsh/logs/` whenever you like without touching a single recording.

## Configuration

| Variable | Default | Meaning |
| --- | --- | --- |
| `SCSH_DAEMON_PORT` | `7274` | HTTP listen port (localhost only) |
| `SCSH_HOME` | `~/.scsh` | Dir for the persistent session store (`daemon-<port>.redb`), permanent `recordings/`, build `casts/`, and `logs/` |

## Where state lives

The daemon persists its session store in an embedded **redb** database at
**`~/.scsh/daemon-<port>.redb`** (override the `~/.scsh` dir with `SCSH_HOME`). Each session
is one row, so a mutation writes just that session — not a rewrite of the whole store. Only
the daemon opens the DB (redb allows one process at a time); the CLI reads the daemon's mode
from a tiny cross-process marker instead.

Runtime files — the PID lock and the mode marker (`daemon-<port>.pid`, `daemon-<port>.mode`)
and the prune queue — live under the **system temp dir** `$TMPDIR/scsh-daemon/`. Session
history survives a `daemon restart`; the daemon's own uptime/client state starts fresh.

## API (for scripts)

- `GET /` — HTML session index
- `GET /session/{id}` — HTML session detail
- `GET /cast/{session}/{proc}` — asciicast v3 recording (valid partial file mid-run);
  `?dl=1` for a download attachment
- `GET /cast/{session}/{proc}/play` — HTML player page (scrub, pause, `#t=…` deep links)
- `GET /cast/{session}/{proc}/export.html` — the recording rendered as one self-contained
  offline HTML player page (identical to `scsh export-cast` output; the chapters sidecar is
  folded in when present, and a malformed sidecar exports without chapters). Served as a
  download attachment named `<cast stem>.html`; 404 with an actionable body until the
  recording has at least one complete frame
- `GET /session/{id}/export.html` — the ENTIRE session as one self-contained offline HTML
  page: a summary header plus every recording embedded as its per-cast export page (iframe
  `srcdoc` composition; procs without a recording become note rows). Served as a download
  attachment named `scsh-session-{id}.html`; 404 with an actionable body when the session
  has no exportable recording yet
- `GET /assets/scsh-cast-player.{js,css}` — the first-party player assets
- `GET /api/v1/sessions` — JSON session id list
- `GET /api/v1/session/{id}` — JSON session detail
- `GET /api/v1/images` — JSON status of every scsh image (base + one per harness) on the
  detected runtime: exists, up-to-date (fingerprint match), created, size (created/size are
  `null` on Apple `container`, which has no inspect formatter)
- `POST /api/v1/images/build` — body `{"harnesses": [name…], "rebuild_base": bool, "force":
  bool}` (all optional; no harnesses = all). Spawns a detached `scsh build-images --session
  <id>`, pre-creates that session, and returns `{"ok":true,"session":id}` so the caller can
  deep-link it. One build at a time — a concurrent request gets 409. Stderr is captured and
  the session is reconciled on exit (same as `jobs/start`), so a build that dies before it
  registers becomes a failed session with the error — never a stranded "running" one. Each
  image build is recorded as an asciinema cast on the host (same ASCII-cinema player as
  skill runs) whenever `asciinema` is on PATH.
- `POST /api/v1/session/start`, `/register`, `/deregister`, `/ping`, `/proc/*`, `/container`
  — event ingestion (used by `scsh run`); `/proc/cast` registers a proc's recording path
- `POST /api/v1/session/stop` — body `{"session":"…"}`. Force-stop a stalled job from the
  session page: stop every still-named container, SIGTERM (then SIGKILL) the `scsh run`
  process when its PID is known, and mark incomplete procs failed with `force_stopped`.
  Idempotent on an already-ended session.
- `POST /api/v1/repos/open` — body `{"path": "…"}`. Validate the path is a git repo, report
  whether it is clean, discover the harness definitions available to it, and remember it as an
  open repo. `{"ok":true,"repo":…,"clean":bool,"dirty":[…],"defs":[…]}`, or `{"ok":false,"error":…}`
- `POST /api/v1/harness-defs` — body `{"repo": "…"}`. Re-discover an open repo's definitions
  (a refresh). `{"defs":[…]}`
- `POST /api/v1/jobs/start` — body `{"repo": "…", "def": "…", "params": {…}}`. Enforce one job
  per directory, validate the definition + params, then spawn a detached `scsh run --def <name>`
  in the repo with the params as environment and a pre-created session id.
  `{"ok":true,"session":id}`; a second job in the same repo (or a dirty tree) gets 409
- `POST /api/v1/repos/pick` — pop the host's native folder chooser (the daemon is local) and
  return the chosen path: `{"ok":true,"path":…}`, `{"ok":false,"cancelled":true}`, or
  `{"ok":false,"error":…}` on a headless host (type the path instead)
- `GET /api/v1/repos` — the opened repositories and any repos that have jobs, each with its
  jobs (sessions) grouped underneath

## Harness definitions

A **harness definition** is a parameterized, runnable job — the unit the "start a job from the
browser" flow (and `scsh run --def <name>`) runs. Definitions come from three places, later
sources shadowing earlier ones by name (repo > home > built-in):

- **built-in** (embedded in the binary, always available): `doctor` (report which agent images
  and credentials are present, then run a trivial end-to-end confirm task), `add` (an a+b math
  self-test), `research` (a trivial tool-calling demo), and `fruits` (a workflow demo, below).
- `~/.harness/<name>.yml` — the running user's personal definitions.
- `<repo>/.harness/<name>.yml` — definitions that ship with a repository.

A definition is **either** a flat one-shot task **or** a workflow. Note the YAML is
**block-form only**: the minimal reader has no inline flow collections, so write nested mappings
and lists as indented blocks (not `{ … }` / `[ … ]`), except `needs:` which is a comma-separated
scalar.

A flat `.harness/<name>.yml`:

```yaml
description: "Add two integers A and B and verify the sum."   # one line, shown in the list
params:                                                       # each forwards as an env var
  A:
    type: int
    default: "2"
    description: "First addend"
  B:
    type: int
    default: "3"
task: |                                                       # becomes .skills/<name>/SKILL.md
  Read A and B from the environment, compute A+B, write {"sum": …} to $SCSH_RESULT, and assert.
invocations:                                                  # the agent matrix (as in .scsh.yml)
  opencode-gpt:
    harness: opencode
    model: openai/gpt-5.4-mini-fast
  claude-sonnet:
    harness: claude
    model: sonnet
```

Param types are `string`, `int`, `bool`, and `enum` (with a comma-separated `choices:`). A param
with a `default:` is optional; without one it is required unless `required: false` is set. The
`task` body is materialized into the run clone (or the git-transport bare on Apple Container),
so the caller's working tree stays clean — `scsh run --def` requires a clean repo just like a
normal run.

## Workflow definitions (`steps:`)

A definition with `steps:` (instead of `task:`+`invocations:`) is a **workflow** — a DAG of
steps scsh runs in order, passing typed output from one step into the next. Each step is a
context-free unit: it names an `agent`, a `prompt`, typed `output` fields, and `inputs` bound
from run params (`params.NAME`) or an upstream step's output (`stepid.field`). scsh resolves the
wiring and hands each input to the step as a plain environment variable, and appends the I/O
contract (which env vars carry the inputs, and the exact JSON to write to `$SCSH_RESULT`) to the
prompt — so a step knows only about itself.

```yaml
description: "Categorize words, then sort each list."
params:
  WORDS:
    type: string
    required: true
steps:
  categorize:
    agent:
      harness: claude
      model: sonnet
    inputs:
      WORDS: params.WORDS         # bind an input from a run param
    prompt: |
      Split the WORDS input into fruits and vegetables (comma-separated).
    output:                       # the typed result this step must write
      fruits:
        type: string
      vegetables:
        type: string
  sort_fruits:                    # sort_fruits and sort_vegetables run in parallel
    needs: categorize             # DAG edge (comma-separated for several)
    agent:
      harness: claude
      model: sonnet
    inputs:
      LIST: categorize.fruits     # bind an input from an upstream output field
    prompt: |
      Sort the LIST input alphabetically.
    output:
      sorted:
        type: string
```

- **`needs:`** — the DAG edges; a step runs once every step it needs has run (or been skipped).
- **`when:`** — an optional gate: a block map of `reference: value` (or `reference: { gte: 3 }`)
  entries, ALL of which must hold (AND) for the step to run. Ops: `eq ne lt lte gt gte in`.
  Disjunction is expressed as separate steps, so there is no OR combinator and no expression
  language. A false gate — or a skipped dependency — skips the step.
- **`output:`** — validated after the step: a missing or mistyped field fails the step (and any
  branch that needs it). Only these fields are visible to downstream `inputs`/`when`.

Every `inputs:`/`when:` reference must resolve to a declared param or an upstream step's declared
output field, and any referenced step must be in `needs:` — checked when the definition is
parsed, so a workflow that could branch on a value no step produces is rejected up front.

**Session scratch.** A workflow's per-step result files live under a session directory
`<scratch>/scsh/<session>/`, where `<scratch>` is `.harness/tmp` when that is gitignored (some
repos prefer it) else `tmp`. scsh refuses to run unless the scratch it uses is gitignored, so a
run never dirties the tracked tree.

## Start a job from the browser

The session index page is organized into tabs: **Jobs** (every run), **Directories** (opened
repos and their jobs), **Start a job**, and **Containers** (the images panel).

Under **Start a job**: type or paste a path (or click **Pick…** for the native folder chooser)
and **Open**. The daemon validates the repo with the *same* checks the run makes — it must be a
git repo that is **committed, clean, and has a gitignored scratch dir** (`tmp/` or `.harness/tmp`)
— and reports `runnable` plus any **blockers**. If it is not runnable, the blockers are shown and
**Start** stays disabled, so a doomed job is never started. Otherwise, pick a definition (its
agent routes and a workflow badge are shown), fill its param form, and **Start job** — which
posts `/api/v1/jobs/start` and deep-links to the spawned session, the same live board a console
run gets, because the job *is* an ordinary `scsh run --def`. The daemon runs **at most one job
per directory** at a time.

**No hidden jobs, no silent failures.** A started job's session is bound to its process: the
daemon captures the spawned run's output and, when the process exits, reconciles the session — a
run that finished normally is left alone, but one that died before it ever registered becomes a
**failed** session showing the captured error, never a stranded "running" one.

Two built-in definitions make good demos: **`doctor`** (no params — confirms the agent images
are built and each agent's credentials proxy through, then runs a trivial end-to-end task) and
**`fruits`** (the workflow demo — give it `WORDS` like `apple, carrot, pear, onion` and watch
`categorize` fan out into `sort_fruits` and `sort_vegetables` running in parallel).

## Images panel

The session index page ends with an **images** table: one row per scsh image (`scsh-base`
plus one per harness), with its status — **up to date** (fingerprint matches this scsh
build's embedded Dockerfile), **stale** (exists but fingerprint differs, e.g. after an scsh
upgrade), or **missing**. Select rows and press **Build selected** (or **Build all**);
optional toggles force-rebuild the base image (`--no-cache`) or rebuild images that are
already up to date. The buttons call `POST /api/v1/images/build` and navigate straight to
the spawned `scsh build-images` session, where each image build streams as a proc row —
the same view a run's build rows get. Builds are TUI-first: each image build runs under
a host `asciinema` PTY so Docker BuildKit / Apple `container` show their native progress,
and the session page embeds the cast player (identical to a skill recording).

## Assumptions

- **Assumed:** Port 7274 is acceptable as the default (`scsh` keypad mnemonic); override
  with `SCSH_DAEMON_PORT`.
- **Assumed:** Localhost-only binding is sufficient — no auth layer on the HTTP server.
- **Assumed:** Ephemeral idle timeout is five minutes with no connected `scsh run` clients.
- **Assumed:** Session ids are six lowercase `[a-z]` letters, matching Apple-container
  nonce style.
- **Assumed:** The daemon is best-effort — if it cannot start, `scsh run` still proceeds
  without the browser URL.

## Resetting the store

The daemon retains up to 200 sessions (each proc keeping up to 5000 output lines) in the
redb store. Because it writes only the sessions that changed — not the whole store each tick
— the store stays small and event POSTs don't stall (this replaced the earlier scheme that
re-serialized one growing JSON file and could reach tens of megabytes). To wipe session
history, stop the daemon and delete its DB:

```console
scsh daemon stop
rm ~/.scsh/daemon-${SCSH_DAEMON_PORT:-7274}.redb
scsh daemon start
```

This clears session history only; `.cast` recordings live under `~/.scsh/recordings/`
(override with `SCSH_HOME`) and are unaffected.

## Demo

```console
./scripts/daemon-demo.sh
```

Or manually:

```console
cargo build --release
./target/release/scsh daemon start
./target/release/scsh daemon status
# open http://127.0.0.1:7274/ after a scsh run
./target/release/scsh daemon stop
```

## Tests

```console
cargo test
```

Integration tests cover `daemon start` / `status` / `restart` / `stop` on localhost. Unit tests cover
the event model, JSON roundtrip, session id format, the harness-definition schema/discovery, and
the open-repo / start-job / one-job-per-directory endpoints. [`DAEMON-JOBS.md`](DAEMON-JOBS.md) is
a followable harness for the "open a repo & start a job from the browser" path.

## Manual verification (`scsh run` → browser)

Automated tests do not drive a full attended `scsh run` with browser attach. From the
**`scsh` repo root** after `cargo build`, capture the binary you just built:

```console
export SCSH_BIN="$PWD/target/debug/scsh"
```

The steps below use `$SCSH_BIN` so they work after `cd` into a scratch directory.

1. `$SCSH_BIN daemon stop` (clean slate) then `$SCSH_BIN daemon start`.
2. In a **fresh scratch directory**, scaffold a demo project: `$SCSH_BIN init-demo-project`
   (or use any git repo that already has a `.scsh.yml` with a short profile). Then run
   `$SCSH_BIN run` in that directory and note the session URL printed on stderr
   (or open `http://127.0.0.1:7274/`).
3. Confirm the browser shows the session, proc rows appear as skills run, harness
   output streams into the proc panel, and proc status updates to ✓/✗ on finish.
4. When the run ends, confirm the session moves to “ended” on the index page.
5. `$SCSH_BIN daemon restart` — daemon comes back and `GET /` still serves the index page.
6. `$SCSH_BIN daemon stop` — daemon exits and the port is closed.

For ephemeral mode, skip step 1: a short `$SCSH_BIN run` alone should spawn the
daemon, attach, and shut it down after the run disconnects and the idle timeout elapses.
If idle shutdown does not run, use `$SCSH_BIN daemon stop` as cleanup.

7. Remove the scratch directory and any daemon artifacts under the system temp dir, for example:

   ```console
   rm -rf "$SCRATCH_DIR"
   rm -f  ~/.scsh/daemon-${SCSH_DAEMON_PORT:-7274}.redb
   rm -f "$TMPDIR/scsh-daemon/daemon-${SCSH_DAEMON_PORT:-7274}.pid" \
         "$TMPDIR/scsh-daemon/daemon-${SCSH_DAEMON_PORT:-7274}.mode"
   ```