diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dfbc769..8cced35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,7 @@ permissions: jobs: linux: name: Linux ${{ matrix.os }} / Python ${{ matrix.python }} + timeout-minutes: 60 strategy: fail-fast: false matrix: @@ -70,6 +71,7 @@ jobs: macos: name: macOS ${{ matrix.arch }} / Python 3.13 + timeout-minutes: 30 strategy: fail-fast: false matrix: @@ -153,6 +155,7 @@ jobs: quality: name: Quality, privacy, and public export + timeout-minutes: 60 runs-on: ubuntu-24.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -230,6 +233,8 @@ jobs: tests/test_projects_manifest.py \ tests/test_public_export.py tests/test_public_scan.py \ tests/test_release_artifact.py tests/support.py \ + tests/test_sandbox_sweep.py \ + tests/shard.py \ tests/test_readme_art.py tests/test_readme_keys.py \ tools/readme_art_lib.py tools/readme_keys.py ruff format --check tools/build-public-tree tools/build-release-artifact \ @@ -273,6 +278,15 @@ jobs: tools/public-scan . --git-history fi - name: Build and test exact public export + env: + # One process per module, four at a time. This suite is bounded by + # wall clock rather than by processors -- most of its expensive + # modules sit in a pty waiting for a picker to repaint -- so four + # workers finish the same 2,728 tests in a quarter of the time on + # this runner's four cores just as they do on thirty-two. Measured + # 2026-08-18: 1,588 s serial, 395 s here, floor 393 s because + # test_watchdog alone takes that long. + SESSION_KIT_TEST_WORKERS: "4" run: | tools/build-public-tree \ --commit "$GITHUB_SHA" \ @@ -301,12 +315,17 @@ jobs: env: COVERAGE_FILE: ${{ runner.temp }}/coverage run: | - coverage run --branch --source=lib \ - -m unittest discover -s tests -t . + # Every worker gets its own data file (--parallel-mode inside the + # driver), and `combine` merges them into COVERAGE_FILE before the + # threshold is applied -- so the number below is the whole suite's, + # exactly as it was when one process walked every module. + python tests/shard.py --workers 4 --coverage + coverage combine coverage report --skip-empty --fail-under=70 shpool-patch: name: Optional shpool patches apply, test, and build + timeout-minutes: 30 runs-on: ubuntu-24.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -336,6 +355,9 @@ jobs: "$GITHUB_WORKSPACE/shpool-patch/$patch" done cargo test --locked --manifest-path \ - "$RUNNER_TEMP/shpool/Cargo.toml" --workspace + "$RUNNER_TEMP/shpool/Cargo.toml" --workspace \ + || { echo "::warning::upstream shpool tests failed once; retrying"; \ + cargo test --locked --manifest-path \ + "$RUNNER_TEMP/shpool/Cargo.toml" --workspace; } cargo build --locked --release --manifest-path \ "$RUNNER_TEMP/shpool/Cargo.toml" --bin shpool diff --git a/CHANGELOG.md b/CHANGELOG.md index c9a523e..e322219 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,33 @@ All notable changes to Session Kit are documented here. -## [Unreleased] +## [0.4.3] - 2026-08-19 ### Fixed +- A pressed key could vanish when the machine was busy. Bash's timed `read` + abandons input it has already taken from the kernel if its timer fires in + the middle of the read, so a keypress landing on the timer's edge was + consumed and never delivered: a `q` could disappear and the Enter behind it + act alone, or a lone Enter be swallowed outright. No read that consumes the + terminal carries a timer any more. The picker sleeps on a private descriptor + that never has data, asks `read -t 0` whether real input is ready, and only + then reads it, untimed. The idle beat is measured on the wall clock rather + than counted in ticks, so load cannot stretch it, and keys queued ahead of a + Ctrl-C are answered in the order they were typed. A source-scan test fails + any future timed read of the terminal. +- Losing the publishing-lock fence race no longer crashes. Retiring the + legacy `inventory.lock` replaces it with a read-only fence file, and a + second process that had just checked the fence then opened the fenced + inode for writing and escaped with a raw `PermissionError`. That open now + re-verifies the fence and retries through the new lock generation, while a + genuine permission problem still surfaces. CI caught the interleaving once + under coverage instrumentation; a deterministic test now pins it. +- `sp account sync-rules` skips a symlinked rulebook by name and skips a + rulebook that is not UTF-8 text, instead of aborting the whole run on the + first and crashing with a traceback on the second. The skipped file and the + reason appear in the report. +- The `sp` help table had lost the line naming what `sp account verify` does. - Both pickers now agree with their own list about which sessions are waiting. The 2026-08-15 ruling that a finished provider turn is `needs you` however the vendor spells it lives in `labels.STATE_WORDS`, and the list column reads @@ -44,8 +67,23 @@ All notable changes to Session Kit are documented here. `Kill confirmation`. `docs/voice.md` allows neither word, and the second named a confirmation step that no longer exists. +### Changed + +- Two keys, named for what they do: inside a session, Ctrl-Q leaves it + running and Ctrl-D closes it; in the picker, Esc and Ctrl-D quit. Every + footer, hint, and page now uses that vocabulary, and `leave` no longer + appears where `quit` is meant. + ### Added +- `sp account sync-rules [--check]`: one shared rulebook, written by hand in + one place, is rendered into every enrolled profile's rulebook between + markers. `--check` reports drift without writing. The doctor reports a + profile whose rulebook has drifted from the shared file. +- The sandbox sweep derives its temp-directory prefixes from the test sources + themselves instead of a hand-kept list of six, so a new test's sandboxes are + swept without anyone remembering to register them, and it refuses by name to + remove `.git`, `.github`, `.gitignore`, and `.shellcheckrc`. - `CODE_OF_CONDUCT.md`, the Contributor Covenant 2.1, with reports routed through GitHub so one about the maintainer still reaches someone. - The README's pictures are generated from the running picker rather than @@ -64,8 +102,8 @@ All notable changes to Session Kit are documented here. - Short project directory names: a Claude session launched at the registered root of a project shortcut now stores transcripts and auto memory under the shortcut's alias (Claude Code 2.1.234's `CLAUDE_CODE_PROJECT_DIR_NAME`). - The export is proved per launch — launcher version, unambiguous registry, - valid alias, no directory conflict — and an existing munged directory is + The export is proved per launch, launcher version, unambiguous registry, + valid alias, no directory conflict, and an existing munged directory is renamed in one atomic move only while no other session of that profile runs inside the root. `session-kit doctor` gained a `project-dir-names` check; `SESSION_KIT_PROJECT_DIR_NAME=off` disables the feature. @@ -76,8 +114,8 @@ All notable changes to Session Kit are documented here. kit. Daemon selection now rules out any daemon owned by another account before it asks who holds the listener: their `/proc//fd` is unreadable, which the uniqueness rule read as "cannot establish", so - `daemon_generation` went null and every session became unprovable — - `sp new` produced an unresolved row and `sp go`/`sp close` refused. A + `daemon_generation` went null and every session became unprovable, +`sp new` produced an unresolved row and `sp go`/`sp close` refused. A listener under `/run/user/` is mode 0700, so another account's daemon was never a candidate. Census rows now carry the owning uid; a row without one is still treated as a candidate. @@ -86,12 +124,12 @@ All notable changes to Session Kit are documented here. the rename and undoes it if a raw same-profile launch appeared in the gap; a same-profile provider in the launcher's own ancestor chain counts as a live session (only shells and launch plumbing are excused); and the Claude - version proof is bound to the exact executable the shell then runs — an + version proof is bound to the exact executable the shell then runs, an armed export launches the proved realpath, never a re-resolved `claude`. An alias path occupied by a regular file or symlink now refuses instead of exporting an unusable name. - Enrolment writes `autoContinueAtUsageLimit: false` even when the source - Claude profile has no settings file at all — the default is a promise about + Claude profile has no settings file at all, the default is a promise about every managed profile, not a transform applied only when there was something to copy. - The project-dir-name helper reads the same projects registry every other @@ -104,7 +142,7 @@ All notable changes to Session Kit are documented here. - A self-name could deadlock the whole estate: its write-time revalidation ran a fresh collection while holding the name-store locks, and collecting can - re-acquire `config.lock` through a second descriptor — the process then + re-acquire `config.lock` through a second descriptor, the process then waits forever on its own lock while every collector, picker refresh and name attempt queues behind it (observed live for twenty minutes on 2026-08-17). Revalidation now re-reads only the process table; the one @@ -305,6 +343,7 @@ All notable changes to Session Kit are documented here. attach. - Added patch `0006`, which coalesces bursts of client resize events. +[0.4.3]: https://github.com/dob323/session-kit/releases/tag/v0.4.3 [0.4.2]: https://github.com/dob323/session-kit/releases/tag/v0.4.2 [0.4.1]: https://github.com/dob323/session-kit/releases/tag/v0.4.1 [0.4.0]: https://github.com/dob323/session-kit/releases/tag/v0.4.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bac9887..a24eefe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -115,7 +115,7 @@ installations on a persistent Mac account. bug again.** Tab is IFS whitespace, so a run of tabs collapses to one separator and every empty field between them disappears; the fields after it shift one place left and a record written perfectly is read as garbage. This -cost the project three launches in one night — every `sp new --model` without +cost the project three launches in one night, every `sp new --model` without `--launch-key` wrote an empty launch key, the generation fields shifted, and the session came up as a shell with no provider. @@ -128,7 +128,7 @@ line=${line//$'\t'/$'\034'} IFS=$'\034' read -r provider cwd model launch_key <<<"$line" ``` -The pattern sites are in `bashrc/shpool.bashrc` — the start record, the +The pattern sites are in `bashrc/shpool.bashrc`, the start record, the sidecar, and the launch record all read this way, each with a Python shape check above it that refuses a record containing `\034` in the first place. @@ -148,8 +148,8 @@ separate checklist: ## No personal names in shipped files -Everything the public export ships — code, comments, docstrings, tests, -identifiers, configuration, prompts, and docs — names roles, not people. Write +Everything the public export ships, code, comments, docstrings, tests, +identifiers, configuration, prompts, and docs, names roles, not people. Write "the operator" or "the maintainer"; never a person's name. This covers identifiers as much as prose: a keyword argument or JSON key with a name in it ships just as publicly as a comment does. @@ -164,7 +164,7 @@ release tree that contains a personal-name token, and it is the gate that `tools/build-public-tree` runs on every export. The scanner stores the blocked words only as one-way SHA-256 digests in `PRIVATE_TOKEN_DIGESTS`, so the scanner itself stays safe to publish, and it tests each underscore-separated -part of an identifier as well as the whole token — `operator_confirmed` is +part of an identifier as well as the whole token, `operator_confirmed` is fine, a name in that position is not. The rule reaches published history too, and history published before the rule @@ -174,7 +174,7 @@ and accepted, and `tools/public-scan --git-history --baseline` skips those exact objects. It skips nothing else: a credential in a listed blob still fails, the working-tree scan ignores the baseline entirely, and an edited file is a different object that fails again. Append to that list only after reading -the blob and only for something already published — a failure on a blob that +the blob and only for something already published, a failure on a blob that has not shipped means the tree still has to be fixed. Adding a new blocked word means adding its digest, not the word: diff --git a/README.md b/README.md index 4a2846f..bab4abd 100644 --- a/README.md +++ b/README.md @@ -4,62 +4,95 @@ [![Latest release](https://img.shields.io/github/v/release/dob323/session-kit)](https://github.com/dob323/session-kit/releases) [![License](https://img.shields.io/github/license/dob323/session-kit)](LICENSE) -**Many AI coding sessions. One place to see which one needs you.** +**Close the terminal windows, not the AI coding sessions. One number takes you back in.** -Session Kit is a local picker for Claude Code, Codex, and shell sessions. Type `kit` and every session you are running is in one list, with a stable number, a name, and a state word that says whether it is waiting on you or still working. The sessions run on the host, so closing the terminal or dropping SSH does not end them. +Every Claude Code and Codex session in one menu: named, numbered, colour-coded. Open, close, and jump to whichever needs you. + +The sessions keep running on the host whether or not you are watching them.

The Session Kit picker: seven sessions grouped into Ready and Open elsewhere, each row showing number, name, provider, account, model, state, and last activity

+

+ kit → see what needs you → type its number → back in the session. +

+

Install · The picker · + Safety model · + Accounts · Delegated work · How it works · - Safety model · Documentation

-> **Public beta.** `v0.4.2` supports Linux with systemd and macOS 14 or newer. Start on a single trusted Unix account where provider conversations can be recovered. Session Kit is not a security boundary against another process already running as the same Unix user. +> **Public beta, `v0.4.3`.** Linux with systemd, or macOS 14 and newer. I use Session Kit daily, and the beta moves quickly, so expect to update. Full detail in [Beta and support](#beta-and-support). -## What you get +## Why I built it -| Capability | What it means | -|---|---| -| **One list** | Managed Claude Code, Codex, and shell sessions in a single keyboard-first picker. | -| **States that mean something** | `question`, `needs you`, `working`, and `idle` have exact definitions and sort consistently. | -| **Stable session numbers** | Open, inspect, close, and find sessions without copying internal shpool IDs. | -| **Survives disconnects** | The session stays on the host when a terminal window closes or SSH drops. | -| **Guarded actions** | Every mutation rechecks exact live identity immediately before it runs, and refuses on any doubt. | -| **Recoverable conversations** | Closing a Claude Code or Codex session records the exact conversation for restore. | -| **Isolated working copies** | A delegated session gets its own git worktree on its own branch, and hands it back when it closes — kept instead, with the reason named, whenever giving it back could lose work. | -| **Account-aware** | Provider accounts are enrolled and shown per session. A conversation whose weekly quota runs out is carried to another enrolled account once, and you are told afterwards. | -| **Local only** | No hosted account, no analytics, no update beacon, no telemetry. | -| **Provider-aware display** | Names, account aliases, models, terminal titles, and per-session colors stay visible without becoming identity evidence. | +I run several Claude Code and Codex sessions at the same time. Keeping them alive turned out to be the easy part. Remembering which one was doing what, which one had finished, and which one had been sitting there waiting on me the whole time was not. I kept opening window after window just to find the session that had asked me something. + +Session Kit is the small home screen I wanted for that. I use it every day, on a local machine and over SSH to the host where the work actually runs. + +It is a passion project. There is no company behind it, no paid tier, and nothing to buy. It is MIT licensed, it collects nothing, and it talks to no server of mine. I published it because it fixed a problem I hit every single day, and if you hit the same one, it will probably fix yours. + +## What it does + +- **See every session in one list.** Claude Code and Codex together, on one screen. +- **Know what needs you.** `question`, `needs you`, `working`, and `idle` make the next session to check obvious. +- **Jump back in by number.** Every session keeps a stable number that does not move. +- **Survive terminal and SSH disconnects.** The session keeps running on the host. +- **Know which window is which.** Sessions name and colour themselves, and the name, number, and colour follow the session into its own window. +- **Use a different account per session.** The subscription belongs to the session, not to the terminal that launched it. +- **Get a closed conversation back.** Closing a session records the exact conversation for restore. +- **Keep delegated work apart.** A machine-origin session gets its own git worktree on its own branch, and hands it back when it closes. +- **Stay local.** No hosted account, no analytics, no telemetry, no update beacon. ## Is this for you? -It fits best if you run several AI coding sessions at once, work on a remote host over SSH, and want to know at a glance which session is blocked on you rather than opening each one to find out. +If you regularly have enough sessions open that you lose track of which one needs you, this was built for exactly that, and you will feel it on the first run. -Here is what it does **not** do: +If you usually keep one or two sessions going, you probably do not need it yet. -- **No diff review.** It will not show you what a session changed. That stays in git and your editor. -- **No cost or spend accounting.** It can read the provider's own quota percentages and act when one runs out, but it does not price or count tokens. -- **Not a multiplexer.** It runs on [shpool](https://github.com/shell-pool/shpool) and does not replace tmux, or try to. -- **Not a security boundary.** It protects you from acting on the wrong session, not from a hostile process running as you. +It works the same on a local workstation or on a remote host over SSH. -If what you actually need is reviewing the diff each agent produced, a different tool will serve you better. +## Simple on purpose + +The interface is deliberately small: open `kit`, see what needs you, press a number. + +More is going on underneath. Before Session Kit changes a live session, it re-proves that the session is still the exact provider conversation and the exact process it expects. If it cannot prove that, it refuses instead of guessing. + +The picker is a view of your sessions. It is never trusted as evidence of which live session an action should affect. That distinction is the whole design, and [Safety model](#safety-model) has it in full. ## Install Session Kit installs per user, on the Linux or macOS machine where the work runs. +### Installing with an AI assistant + +The shortest path, if Claude Code, Codex, or another terminal agent is already open: give it this. + +> Install Session Kit from `https://github.com/dob323/session-kit`. Use the latest release artifact, not a clone of `main`. Download the release archive with its `.sha256` and `.provenance.json` files, verify the checksum, extract it, and run `./install.sh --check` first. Fix only the remedies that preflight explicitly names. Then run `./install.sh`, `session-kit doctor`, `session-kit services enable`, and `session-kit doctor` again. Never bypass a refused step. Show me the final doctor output and finish by telling me to type `kit`. + +### Installing it yourself + +Artifacts are named by the exact commit they were built from, so there is no fixed download URL. This asks the release API which files belong to the current release, checks what arrived, and only then unpacks it. It needs nothing but Python 3 and `tar`, both of which the install needs anyway. + ```bash mkdir session-kit-download cd session-kit-download -gh release download --repo dob323/session-kit --pattern 'session-kit-*' +python3 - <<'PY' +import json, urllib.request +url = "https://api.github.com/repos/dob323/session-kit/releases/latest" +with urllib.request.urlopen(url) as response: + release = json.load(response) +for asset in release["assets"]: + urllib.request.urlretrieve(asset["browser_download_url"], asset["name"]) + print("downloaded", asset["name"]) +PY if command -v sha256sum >/dev/null; then sha256sum --check session-kit-*.sha256 @@ -78,24 +111,18 @@ session-kit services enable session-kit doctor ``` -`./install.sh --check` is read-only. Do not work around a refusal — Session Kit prints the reason and the remedy it expects. +`./install.sh --check` is read-only. Do not work around a refusal. Session Kit prints the reason and the remedy it expects. ### Requirements -- shpool `0.11.0` +- shpool `0.11.0`, the stock build. The optional patches in [`shpool-patch/`](shpool-patch/) are **not** needed to install or to start using this; that decision can wait until something makes you want them - Claude Code, Codex, or both - one trusted Unix account, with per-user service access **Linux** additionally needs a readable `/proc`, a systemd user manager, Bash 4+, and Python 3.10+. **macOS** additionally needs macOS 14+, an active desktop login for the per-user launchd GUI domain, Homebrew Bash 4+, and Python 3.11+. -For supported shpool paths, provider setup, manual asset download, project import, and activation, read [Install Session Kit](docs/install.md). - -### Installing with an AI assistant - -If Claude Code, Codex, or another terminal agent is doing the installation, give it this: - -> Install Session Kit from `https://github.com/dob323/session-kit`. Use the latest release artifact, not a clone of `main`. Download the release archive with its `.sha256` and `.provenance.json` files, verify the checksum, extract it, and run `./install.sh --check` first. Fix only the remedies that preflight explicitly names. Then run `./install.sh`, `session-kit doctor`, `session-kit services enable`, and `session-kit doctor` again. Never bypass a refused step. Show me the final doctor output and finish by telling me to type `kit`. +Prefer the GitHub CLI, installing without a network path to the API, or checking the provenance file by hand? [Install Session Kit](docs/install.md) has every route, plus supported shpool paths, provider setup, project import, and activation. ## First run @@ -108,7 +135,6 @@ New session defaults to Claude Code. A session can also be started directly: ```bash sp new claude sp new codex -sp new shell ``` Project aliases, provider choice, account selection, and configured models make those launches more specific. See [Projects](docs/projects.md) and [Use Session Kit](docs/usage.md). @@ -127,6 +153,8 @@ The state words are deliberately small and literal: | `idle` | A needs-you transcript has not moved for the configured idle window. | | `pending` | A launch that has not finished, or a value Session Kit cannot currently read. It is not a fifth state. | +One limit worth stating plainly: Claude Code reports a blocking prompt the moment it opens one, so `question` is exact. Codex does not expose that yet, so a Codex session reads `needs you` when its turn ends rather than the instant it asks you something. + Pressing `a` narrows the list to just those sessions, with how long each has waited. @@ -149,7 +177,69 @@ The home screen keeps the common actions one key away: A session that is open elsewhere defaults to **Move it here** after a fresh identity check. The earlier window returns to its picker, and the provider conversation is not duplicated. -Press `?` for the full key reference — filtering, ranges, grouping, forking, renaming, and `g` to jump to the next session that needs you are all there. See [Picker navigation](docs/picker-navigation.md) for the cursor-driven picker, mouse behavior, action panels, machine sessions, and closed-session restore. +Every session also names and colours itself. It takes a short title from its own first piece of work, keeps a number that does not move, and gets a colour no other live session has. All three follow the session into its own window: the tab title carries the name and number, and the session is tinted its colour inside Claude Code and Codex themselves. So the window you are typing in tells you which session it is, and the picker and the session never disagree. + +Press `?` for the full key reference: filtering, ranges, grouping, forking, renaming, and `g` to jump to the next session that needs you are all there. See [Picker navigation](docs/picker-navigation.md) for the cursor-driven picker, mouse behavior, action panels, machine sessions, and closed-session restore. + +## Safety model + +Session Kit deliberately separates **what you see** from **what it trusts**. + +

+ Four steps: a frozen snapshot, binding an action proof to the exact provider UUID and generation, rechecking live identity immediately before acting, then acting or refusing +

+ +1. **Provider UUID plus exact process generation is identity.** +2. Session number, title, directory, timestamps, and terminal output are display context. +3. Every mutation rechecks live identity immediately before it runs. +4. Missing, stale, partial, duplicated, or conflicting evidence fails closed. +5. A refusal changes nothing. + +Before a proof-bound action, Session Kit can bind and recheck the session manager, terminal generation, managed shell, provider process and ancestry, exact provider conversation UUID, and frozen snapshot generation. The proof is owner-only and short-lived. + +This is the part worth caring about: a picker one second out of date still cannot act on the wrong session, because the picker was never the evidence. + +It protects against stale or ambiguous picker state selecting a different session than the one you intended. It does **not** isolate mutually hostile processes running with your own Unix-user privileges. + +Read [Security and local data](docs/security-and-data.md) and [Architecture](docs/architecture.md) for the complete trust model. + +## What it does not do + +- **No diff review.** It will not show you what a session changed. That stays in git and your editor. +- **No cost or spend accounting.** It can read the provider's own quota percentages and tell you when one runs out, but it does not price or count tokens. +- **Not a multiplexer.** It runs on [shpool](https://github.com/shell-pool/shpool) and does not replace tmux, or try to. +- **Not a security boundary.** It protects you from acting on the wrong session, not from a hostile process running as you. + +### Why not tmux? + +tmux keeps processes alive, which is most of the way there, and it is already on your machine. What it cannot tell you is which of eleven panes is waiting on an answer. It has no notion of which conversation a pane holds, so nothing stops you acting on the pane next to the one you meant. + +Session Kit knows both. Every session carries the identity of its provider conversation, and that identity is re-proved against the live system before anything is changed. + +It runs on [shpool](https://github.com/shell-pool/shpool) rather than tmux, so there is one more thing to install first. That was the trade: shpool gives a clean session per shell without fighting multiplexer semantics, and that is what makes a session's identity provable at all. + +## Accounts and subscriptions + +One machine, several logins for the same provider, one per session. Enrol each account once and it becomes a choice at launch: + +```bash +sp account enroll claude work you@example.com +sp account list claude +``` + +Each enrolled account keeps its own provider configuration directory, so a session started on it authenticates as that account and no other. Three Claude Code subscriptions can be running in three sessions at the same moment, and the picker shows which account each session belongs to, the `personal` and `work` column in the screenshot above. + +This is the part most session tools do not do. A terminal multiplexer inherits whichever login the shell that started it happened to have, so every window shares one subscription. Here the account is a property of the session. + +Provider authentication stays in provider-owned storage throughout. Each session launches the provider's own binary against its own configuration directory. Session Kit does not ask for, copy, print, or log provider tokens, and it does not put a subscription token into any harness of its own. + +### Carrying a conversation to another account + +There is a mechanism that can move one idle conversation to another enrolled account when its weekly quota runs out. **It is off, and it stays off until you turn it on.** The watchdog runs in `report` mode by default and only says what it would do; automatic changes require setting `SESSION_KIT_WATCHDOG_MODE=repair` deliberately. + +Leave it off unless you have a reason. Owning several subscriptions and using each for your own work is ordinary use. Moving work between accounts *because a limit was reached* is a different shape, and it is the shape providers look for when they enforce against limit evasion, and the consequence lands on your account, not on this tool. `sp account-auto-switch ` shows you what would happen without doing it. + +Read [Configure Session Kit](docs/configuration.md) for enrolment, verification, and the carry-over rules in full. ## Delegated work @@ -159,7 +249,7 @@ A session started as machine-origin is given its own git worktree, on a branch t sp new claude --worktree ``` -Closing the session gives the copy back, and every close does it — `sp close`, the picker's `k`, `bye` or a clean provider exit, and the scheduled cleanup pass. The copy is kept instead, with the reason named out loud, whenever giving it back could lose work: uncommitted, staged or untracked files, ignored files that were created there, a commit not yet in the reference, somebody still working in the directory, or a check that could not run at all. +Closing the session gives the copy back, and every close does it: `sp close`, the picker's `k`, `bye` or a clean provider exit, and the scheduled cleanup pass. The copy is kept instead, with the reason named out loud, whenever giving it back could lose work: uncommitted, staged or untracked files, ignored files that were created there, a commit not yet in the reference, somebody still working in the directory, or a check that could not run at all. A directory that is not a git repository has nothing to isolate, and a person's own session is never moved out of the directory they chose. See [Use Session Kit](docs/usage.md) for the complete rules. @@ -179,26 +269,6 @@ Install Session Kit where the work actually runs. There is no Session Kit server to deploy. -## Safety model - -Session Kit deliberately separates **what you see** from **what it trusts**. - -

- Four steps: a frozen snapshot, binding an action proof to the exact provider UUID and generation, rechecking live identity immediately before acting, then acting or refusing -

- -1. **Provider UUID plus exact process generation is identity.** -2. Session number, title, directory, timestamps, and terminal output are display context. -3. Every mutation rechecks live identity immediately before it runs. -4. Missing, stale, partial, duplicated, or conflicting evidence fails closed. -5. A refusal changes nothing. - -Before a proof-bound action, Session Kit can bind and recheck the session manager, terminal generation, managed shell, provider process and ancestry, exact provider conversation UUID, and frozen snapshot generation. The proof is owner-only and short-lived. - -This protects against stale or ambiguous picker state selecting a different session than the one you intended. It does **not** isolate mutually hostile processes running with your own Unix-user privileges. - -Read [Security and local data](docs/security-and-data.md) and [Architecture](docs/architecture.md) for the complete trust model. - ## Claude Code and Codex Session Kit does not replace either provider. @@ -207,22 +277,8 @@ For **Claude Code**, it can supply the installed status line, session name and c For **Codex**, it leaves the Codex status bar under Codex control, and supplies per-launch terminal-title items and the session theme without editing `~/.codex/config.toml`. -Provider authentication stays in provider-owned storage. Session Kit does not ask for, copy, print, or log provider tokens. - Read [Claude Code and Codex integration](docs/provider-integration.md) for the exact contracts. -## Display - -Session Kit is developed and tested against Ghostty, but any truecolor terminal runs the key-driven picker. The cursor-driven picker has a reduced-color fallback. - -```bash -NO_COLOR=1 kit -``` - -`SESSION_KIT_NO_COLOR=1` does the same. State words and safety meaning never depend on color. - -Opening, moving, or creating a session can push the session name into the terminal title; returning to the picker restores `session kit`. Set `SESSION_KIT_TAB_TITLE=off` to disable title pushes. - ## Local data and privacy No hosted service, no Session Kit account, no analytics, no update beacon, no telemetry. @@ -237,44 +293,39 @@ By default: Optional history can contain prompts, source code, command output, credentials, and other sensitive terminal content. Read [Security and local data](docs/security-and-data.md) before enabling it. -## Maintenance +## Running it ```bash -session-kit doctor -session-kit update --source -session-kit rollback [--to ] +session-kit doctor # what is installed, what is wrong, and what to do +sp help # every command, with exit codes and selectors +``` -session-kit services enable -session-kit services disable -session-kit services status +`doctor` is the first thing to run when anything looks wrong. Updates install an immutable local release and move `current` atomically; rollback selects a verified release already on the machine. Read [Update and roll back](docs/update-and-rollback.md) before changing releases, [Configure Session Kit](docs/configuration.md) for colour, terminal titles and every setting, and [Troubleshooting](docs/troubleshooting.md) when `doctor` names something you have not met. -sp help -sp help exit-codes -sp help selectors -``` +Session Kit runs on [shpool](https://github.com/shell-pool/shpool) and neither vendors nor replaces it. The repository carries optional shpool patches with their scope and checks; `doctor` records the shpool binary validated at installation and reports when it later changes. Read [`shpool-patch/`](shpool-patch/) before choosing a patched binary. -Updates install an immutable local release and atomically move `current`. Rollback selects a verified release already on the machine. Read [Update and roll back](docs/update-and-rollback.md) before changing releases. +## Beta and support -## shpool +**Settled:** the picker, session identity and the guards around every action, install, update and rollback, and the Claude Code and Codex integrations. Around 2,700 tests run in CI, with native Linux and macOS coverage. -Session Kit runs on [shpool](https://github.com/shell-pool/shpool). It does not vendor or replace it. +**Not settled:** the beta moves quickly, thirteen releases in its first three weeks, so expect to update. Codex does not report a blocking prompt yet. -The repository includes optional shpool patches with their scope and checks. `session-kit doctor` records the shpool binary validated at installation and reports when that binary later changes. See `shpool-patch/` before choosing a patched binary. +**What is promised:** this is a tool its author uses daily and publishes; it is not a supported product. Replies are best-effort, security fixes go into the current release with no dated windows and no back-ports, and nothing here is a security boundary against another process already running as your Unix user. Start on a single trusted Unix account where provider conversations can be recovered. ## Documentation +**[Documentation index](docs/)**, every document grouped by what you are trying to do. + +The ones people reach for first: + | Topic | Document | |---|---| | Install | [docs/install.md](docs/install.md) | -| Configure | [docs/configuration.md](docs/configuration.md) | | Use Session Kit | [docs/usage.md](docs/usage.md) | +| Configure | [docs/configuration.md](docs/configuration.md) | | Picker navigation | [docs/picker-navigation.md](docs/picker-navigation.md) | -| Projects | [docs/projects.md](docs/projects.md) | -| Claude Code and Codex integration | [docs/provider-integration.md](docs/provider-integration.md) | -| Security and local data | [docs/security-and-data.md](docs/security-and-data.md) | | Troubleshooting | [docs/troubleshooting.md](docs/troubleshooting.md) | -| Update and rollback | [docs/update-and-rollback.md](docs/update-and-rollback.md) | -| Uninstall | [docs/uninstall.md](docs/uninstall.md) | +| Security and local data | [docs/security-and-data.md](docs/security-and-data.md) | | Architecture | [docs/architecture.md](docs/architecture.md) | | Release history | [CHANGELOG.md](CHANGELOG.md) | diff --git a/SECURITY.md b/SECURITY.md index c2bf031..290315e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,16 +2,21 @@ ## Supported versions -Each beta minor line is supported for 90 days after its first release, or for -30 days after the next beta minor release, whichever ends later. A security fix -may require moving to the latest patch release on that line. +Session Kit is maintained by one person alongside other work, so this policy +says only what one person can actually keep. + +Security fixes go into the current release. There are no dated support windows +and nothing is back-ported to an older line: if you are running an older +release, the fix is to move to the current one. `releases/latest` resolves to the current beta. -[All releases](https://github.com/dob323/session-kit/releases) shows that line -and everything published before it. +[All releases](https://github.com/dob323/session-kit/releases) lists it and +everything published before it. Earlier lines stay published so that whatever +you already downloaded keeps its checksum and provenance files beside it, not +because those lines still receive fixes. -The current beta supports its documented Linux targets and macOS 14 or newer on -Apple Silicon and Intel. Other operating systems, older macOS releases, Apple +The current release supports its documented Linux targets and macOS 14 or newer +on Apple Silicon and Intel. Other operating systems, older macOS releases, Apple Bash 3.2, and service arrangements outside the documented systemd and per-user launchd models are outside this policy. diff --git a/SOURCE.json b/SOURCE.json index 2536808..d26805a 100644 --- a/SOURCE.json +++ b/SOURCE.json @@ -1,5 +1,5 @@ { - "exported_files": 312, + "exported_files": 315, "schema_version": 1, - "source_commit": "240553e085a426e79e49a0957b9a0f04d94f79d0" + "source_commit": "9a7af3a1e2c38eb8050afa8be018780ea6f777f2" } diff --git a/bashrc/shpool.bashrc b/bashrc/shpool.bashrc index 1f06d33..3d12cb0 100644 --- a/bashrc/shpool.bashrc +++ b/bashrc/shpool.bashrc @@ -86,7 +86,7 @@ if [[ -n ${SHPOOL_SESSION_NAME:-} && $- == *i* ]]; then __sk_expected="$__sk_start.expected" __sk_launch="$__sk_start.launch" __sk_account="$__sk_start.account" - # The shell can start before `sp` has WRITTEN the main record at all — the + # The shell can start before `sp` has WRITTEN the main record at all, the # session exists the moment shpool spawns it, and on a loaded box the # launcher's first write can land a second later. A one-shot existence # check here made that launch permanently silent. The wait is bounded so a @@ -104,7 +104,7 @@ if [[ -n ${SHPOOL_SESSION_NAME:-} && $- == *i* ]]; then # 3 seconds covers a healthy launch; a loaded box can take longer to arm # (a lost-by-milliseconds launch once left a relaunch hanging on a # plain shell). At the 3-second mark, a FRESH main record proves a launch - # is still in progress, and only that case earns the longer wait — a + # is still in progress, and only that case earns the longer wait, a # stale record still costs later shells 3 seconds, not 30. for __sk_arm_attempt in {1..300}; do [[ -r $__sk_expected ]] && break @@ -460,7 +460,7 @@ print(profile) # hooks, so a session that boots before its name intent exists # (fresh uuid, or a pre-baked resume) shows a stale title until # the provider restarts. The marker lets the picker request one - # safe bounce once a name exists — same contract as Codex. + # safe bounce once a name exists, same contract as Codex. __sk_claude_intent_uuid=$__sk_uuid [[ $__sk_launch_mode == new ]] && __sk_claude_intent_uuid=$__sk_new_claude_uuid if [[ -n $__sk_claude_intent_uuid && \ @@ -482,9 +482,9 @@ print(profile) __sk_pdn_helper=${__sk_inventory_core%/*}/sessionkit_inventory/project_dir_name.py if [[ -f $__sk_pdn_helper ]]; then # The version proof must name the exact file this shell then - # executes. A bare `claude` can re-resolve differently — a + # executes. A bare `claude` can re-resolve differently, a # stale command hash, a launcher symlink retargeted after the - # proof — and an older executable silently ignores the export + # proof, and an older executable silently ignores the export # after the directory has already been renamed (review lane # rv-pdn-1, 2026-08-17). One realpath is proved, and an armed # export launches exactly that path. @@ -699,7 +699,7 @@ PY if [[ -z $__sk_app_dir ]]; then # The private directory chain refused, so the gate quietly used # the direct TUI and every reachability feature stayed dark for - # the life of the window. Name the reason once — on stderr, and + # the life of the window. Name the reason once, on stderr, and # in this session's App Server log when one already exists. python3 - "$__sk_state_root" "$SHPOOL_SESSION_NAME" <<'PY' || true import os @@ -707,7 +707,7 @@ import stat import sys state_root, session_id = sys.argv[1:] -line = "[session-kit: Codex App Server disabled — ~/.local/state must be mode 0700]" +line = "[session-kit: Codex App Server disabled: ~/.local/state must be mode 0700]" try: metadata = os.lstat(state_root) private = ( @@ -719,7 +719,7 @@ except OSError: private = False if private: line = ( - "[session-kit: Codex App Server disabled — " + "[session-kit: Codex App Server disabled: " "its private state directory is unavailable]" ) print(line, file=sys.stderr) @@ -812,7 +812,7 @@ PY # as a per-launch theme override (status line, thread-title item). # Resumes and forks color from the conversation's effective color; # a brand-new session has no conversation ID yet, so it launches - # with a color picked from the shpool session name — the collector + # with a color picked from the shpool session name, the collector # adopts that pick as the conversation's override once the ID # exists, keeping window, picker, and future resumes identical. # Fail-open: unknown color or missing theme file launches plain. @@ -1477,7 +1477,7 @@ PY # conversation this was, so ending it would lose the thing the operator # would want back. # - # When the conversation IS recorded, the caller closes instead — see + # When the conversation IS recorded, the caller closes instead, see # __sk_close_keeps_the_conversation. Leaving a row behind that says # "provider exited" for 72 hours was the operator's complaint (2026-08-15): # "the session closes at once and lands in Closed sessions, recoverable, @@ -1569,7 +1569,7 @@ PY # goes with them (operator rule, 2026-08-11, replacing the detach-and-stay- # reopenable rule of the same day): the managed shell ends, shpool ends the # session with it, and the terminal number returns to the ordinary 7-day - # quarantine — the same outcome as pressing k on this row from the main + # quarantine, the same outcome as pressing k on this row from the main # menu, reached through the same close this shell has always used rather # than a second implementation of it. The exact conversation stays # recoverable for its retention window; /kit is the verb for leaving one diff --git a/bin/session_kit_common b/bin/session_kit_common index 94ee9be..b5e84c9 100755 --- a/bin/session_kit_common +++ b/bin/session_kit_common @@ -109,8 +109,8 @@ sk_now_unix_ms() { # The wait is bounded on both. An unbounded acquire meant one holder wedged # inside the daemon stopped every attach, create and close on the machine for # as long as it stayed wedged, with no message and no way out but finding the -# process by hand. The bound is generous — an honest create holds this lock -# through an attach and its generation proof — so reaching it means the holder +# process by hand. The bound is generous, an honest create holds this lock +# through an attach and its generation proof, so reaching it means the holder # is stuck, not merely slow. SK_LOCK_WAIT_SECONDS=${SESSION_KIT_LOCK_WAIT_SECONDS:-120} [[ $SK_LOCK_WAIT_SECONDS =~ ^[0-9]+$ && $SK_LOCK_WAIT_SECONDS -ge 1 ]] || @@ -1627,7 +1627,7 @@ sk_provider_name() { } # How a session is named to a person: its session number and its own title, -# or its provider when it has neither. Never an internal ID — no human-readable +# or its provider when it has neither. Never an internal ID, no human-readable # Session Kit output prints one, anywhere. # The kit owns the terminal tab name (K3), on both providers. # @@ -1706,8 +1706,8 @@ sk_human_label() { # * a person at a terminal is told what is about to happen, by name, and the # action proceeds; # * automation, which has no one to tell, must name the exact session it is -# acting on in SESSION_KIT_CONFIRM_ID. That half is a proven safety net — -# a pipe feeding "y" was never a person reading a label — and it stays. +# acting on in SESSION_KIT_CONFIRM_ID. That half is a proven safety net: +# a pipe feeding "y" was never a person reading a label, and it stays. # # The exact ID travels as an argument and is never printed back. sk_confirm_exact() { diff --git a/bin/session_kit_notice b/bin/session_kit_notice index e6a6b1e..e8ab9a9 100755 --- a/bin/session_kit_notice +++ b/bin/session_kit_notice @@ -1104,7 +1104,7 @@ def watchdog_record_notices() -> list[dict[str, Any]]: "class": "repair-failed", "key": f"repair:{session}", "evidence": "failed", - "line": f"{title} ({provider}) — automatic repair FAILED", + "line": f"{title} ({provider}), automatic repair FAILED", "subject": f"Automatic repair failed: {title}", "body": ( f"{title} ({provider}) stopped working and the automatic repair " @@ -1118,7 +1118,7 @@ def watchdog_record_notices() -> list[dict[str, Any]]: # Strength, so an escalation from ordinary silence to the # daemon's own handoff failure is a new thing to say. "evidence": clean(item.get("evidence"), 60) or "silence-only", - "line": f"{title} ({provider}) — quiet, nothing changed", + "line": f"{title} ({provider}), quiet, nothing changed", "subject": f"Session quiet, nothing changed: {title}", "body": ( f"{title} ({provider}) was reported quiet and nothing was changed: " @@ -1174,7 +1174,7 @@ def stall_notices() -> list[dict[str, str]]: "class": "stall", "key": f"stall:{key}", "evidence": reason or "stalled", - "line": f"a stalled session — {told}", + "line": f"a stalled session, {told}", "subject": "A session has stalled", "body": ( f"A session flagged as stalled: {told}. Nothing was changed. " @@ -1210,7 +1210,7 @@ def question_notices() -> list[dict[str, str]]: "class": CLASS_QUESTION, "key": f"question:{identifier}", "evidence": "open", - "line": f"{who} is waiting on a decision — {header}", + "line": f"{who} is waiting on a decision, {header}", "subject": f"A window is waiting on a decision: {header}", "body": f"{who} has an open question. It is in the question inbox.", "at_unix": ( diff --git a/bin/session_kit_watchdog b/bin/session_kit_watchdog index bccaca6..b96feb9 100755 --- a/bin/session_kit_watchdog +++ b/bin/session_kit_watchdog @@ -1150,12 +1150,12 @@ report_once() { # The 2026-08-17 estate jam: a self-name held config.lock and then waited on # its own second descriptor for it, so every collector, picker refresh and -# name attempt queued behind one process for twenty minutes — while the +# name attempt queued behind one process for twenty minutes, while the # session manager answered normally, so manager_answers saw nothing. The # durable, cheap symptom of that whole class: the snapshot has stopped aging # forward AND a kit state lock has waiters. Report the holder by pid so the # person (or an agent) can end it. config.lock also serialises atomic config -# writes (colors, names), so ending the holder can abort that one command — +# writes (colors, names), so ending the holder can abort that one command; # the write itself stays whole either way, and collections retry on their # own. The message says exactly that instead of promising universal safety # (review lane rv-pdn-2, 2026-08-17). diff --git a/bin/shpool_login b/bin/shpool_login index 20b5a02..f81b59d 100755 --- a/bin/shpool_login +++ b/bin/shpool_login @@ -182,14 +182,14 @@ REPAIR_FILE=${SESSION_KIT_WATCHDOG_REPAIRS:-"$SK_STATE_DIR/watchdog-repairs.json # Recovery pending count is refreshed with the snapshot, not on every # keystroke: recovery state only changes through actions or refreshes, and -# recomputing it per redraw ran a full status build each keypress — most of +# recomputing it per redraw ran a full status build each keypress, most of # the visible menu lag on a loaded box. PENDING_CACHE=0 # Start from a clean frame: after a daemon restart the previous attachment's # dead screen is still in the terminal, and menus painted over it read as # corruption. Then treat a briefly-unavailable manager as a wait, not a -# failure — a planned restart takes a few seconds. +# failure, a planned restart takes a few seconds. picker_clear_screen if ! refresh_snapshot; then echo " The session manager is not responding. Retrying for a minute." @@ -226,7 +226,7 @@ while true; do # The kernel's winsize on stderr is the one honest answer here: macOS # tput answers 80 from terminfo when it cannot ioctl the captured stdout, # and an exported COLUMNS can be stale after a resize. A pty that was - # never sized reports 0x0 — treat 0 as no answer, then fall back to tput + # never sized reports 0x0, treat 0 as no answer, then fall back to tput # (ncurses honours $COLUMNS), then to the exported size itself. __sk_size=$(command stty size <&2 2>/dev/null) || __sk_size= __sk_cols=${__sk_size##* }; __sk_rows=${__sk_size%% *} diff --git a/bin/shpool_reaper b/bin/shpool_reaper index 050d501..dea8821 100755 --- a/bin/shpool_reaper +++ b/bin/shpool_reaper @@ -2,7 +2,7 @@ # Safe session-kit reaper. # # The normal report records verified idle empty-shell candidates for confirmed -# `sp prune` — seven days by default, SESSION_KIT_PRUNE_DAYS otherwise, and the +# `sp prune`, seven days by default, SESSION_KIT_PRUNE_DAYS otherwise, and the # window travels with the list it produces. `--auto-close` is narrower, is now # a BACKSTOP (see WHAT --auto-close IS FOR NOW, under the variable block), and # may close only a generated, provider-exited terminal after 72 continuous @@ -108,7 +108,7 @@ source "$SCRIPT_DIR/session_kit_common" # bashrc once, at session start; installing a release does not restart a # live session, so every session already running when the new close # landed keeps the old path for the rest of its life. -# 2. The close itself failed — an unwritable ledger, a python3 that was +# 2. The close itself failed, an unwritable ledger, a python3 that was # momentarily unavailable. The shell keeps such a session on purpose and # says so; the record and the conversation are both intact, which is # exactly what this engine needs, so it finishes the job three days on. @@ -116,7 +116,7 @@ source "$SCRIPT_DIR/session_kit_common" # daemon still advertises a session whose shell process is gone, no # picker action can reach it, and only a daemon restart or this pass # clears it. (Stock shpool leaves such a row behind whenever a session -# nobody is attached to loses its shell — measured on shpool 0.11.) This +# nobody is attached to loses its shell, measured on shpool 0.11.) This # class clears on its first complete pass; the 72-hour window protects # live empty shells and has no safety value once every session process is # proved absent. @@ -318,7 +318,7 @@ def stale_tui_capture(directory: int, name: str, cutoff_ns: int) -> bool: return False finally: os.close(capture) -# mktemp writes