Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
a2f141d
fix: design-audit round 1 — write-back unification, secret heuristic,…
t41372 Jul 26, 2026
5a36520
fix: design-audit round 2 — jammed secret spellings, meta-aware plan …
t41372 Jul 26, 2026
736bb6b
test: cover the design-audit fixes (rounds 1 and 2)
t41372 Jul 26, 2026
9ed2152
fix: design-audit round 4 — post-#33 merge review follow-ups
t41372 Jul 26, 2026
67d7ee1
fix: design-audit round 5 — max-review findings on the audit branch i…
t41372 Jul 26, 2026
5598394
test: cover the round-5 fixes
t41372 Jul 26, 2026
c6c41ff
fix: design-audit round 6 — structural repairs where round 5's point …
t41372 Jul 26, 2026
9c84639
test: cover the round-6 fixes
t41372 Jul 26, 2026
01fb12a
fix: design-audit round 7 — segment-granular secrecy, cache-window pr…
t41372 Jul 26, 2026
2d2fc77
fix: design-audit round 7b — a bare number is an index unless it stan…
t41372 Jul 26, 2026
2a05d28
test: cover the round-7 fixes
t41372 Jul 26, 2026
84f90a9
fix: design-audit round 8 — count context earns its shards, hints spe…
t41372 Jul 27, 2026
9e3d0d8
test: cover the round-8 fixes
t41372 Jul 27, 2026
31ae26b
fix: design-audit round 9 — the library the index lost, and the flag …
t41372 Jul 27, 2026
fb459fe
fix: design-audit round 10 — the flag that stopped at a layer, and a …
t41372 Jul 27, 2026
eab4e71
fix: design-audit round 11 — the editor door nobody gated, and a chor…
t41372 Jul 27, 2026
17ab038
fix: design-audit round 12 — refuse-never-drop reaches `skit params`
t41372 Jul 27, 2026
0d42413
test: measure mutation blind spots
t41372 Jul 28, 2026
32b2232
fix: centralize cli exit contracts
t41372 Jul 28, 2026
817f2f7
fix: make prompting commands deterministic
t41372 Jul 28, 2026
30deb41
test: harden noninteractive state boundaries
t41372 Jul 28, 2026
5b0e37b
fix: close cli outcome contracts
t41372 Jul 28, 2026
b156409
test: exercise real cli exit boundaries
t41372 Jul 28, 2026
a370139
test: cover interrupted persistence and source guards
t41372 Jul 28, 2026
45134fb
refactor: type parameter edit notices
t41372 Jul 28, 2026
747e3ea
Merge remote-tracking branch 'origin/main' into fix/design-audit
t41372 Aug 3, 2026
17a59b0
test: pin the LF fixture for the drift body assert
t41372 Aug 3, 2026
bae1a3a
fix: refuse an ambiguous name in resolve
t41372 Aug 3, 2026
dd3ba13
fix: type state-write failures into the exit taxonomy
t41372 Aug 3, 2026
d23295f
fix: write a prompt's schema and managed list in one meta write
t41372 Aug 3, 2026
6862117
fix: purge stale plaintext before the secret flag lands
t41372 Aug 3, 2026
4742342
fix: raw runs honor the C3 scrub
t41372 Aug 3, 2026
24d5e96
fix: source-reading meta-tests must read the real tree
t41372 Aug 3, 2026
085ca76
test: close the real mutation gaps the targeted run exposed
t41372 Aug 3, 2026
f3af36f
fix: close the review round-2 follow-ups
t41372 Aug 3, 2026
bf6e1ab
fix: guard post-acceptance persistence with entry identity
t41372 Aug 4, 2026
a7ac6ac
fix: authorize every held-entry write by exact identity, under the en…
t41372 Aug 4, 2026
eb351c3
fix: claim and authorize every CLI writer that waits or derives
t41372 Aug 4, 2026
eec0533
fix: compare-and-claim, authorized removal, staged editor sessions
t41372 Aug 4, 2026
b6d73db
test: make the staged-edit assertions platform-neutral
t41372 Aug 5, 2026
b47eab5
fix: gate the launch itself, claim by content, CAS the source version
t41372 Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@ Key grammar: a chord keeps one meaning per context class — Ctrl+E always opens
on the screen's current subject, Ctrl+N always creates the screen's primary object (a
script on the add step, an agent on a runner picker), Ctrl+T always inserts a value,
Ctrl+R re-runs/refreshes the screen's subject (the run form runs it; Script settings
resyncs its definitions from the script), and Ctrl+S saves/commits the screen's work
(the run form's save-as-preset included). Ctrl+A (cursor-home) and — while an Input has focus —
Ctrl+E (end-of-line) belong to the Input: screen chords for them are never
resyncs its definitions from the script), Ctrl+S saves/commits the screen's work
(the run form's save-as-preset included), Ctrl+O always restores the screen's current
field to its source default (README documents it on the run form — it must never mean
anything else), and Ctrl+L always opens the screen's variable/candidate picker (the
prompt review panel and Script settings). A new screen action takes an UNCLAIMED chord,
never a second meaning for a claimed one. Ctrl+A (cursor-home) and — while an Input has
focus — Ctrl+E (end-of-line) belong to the Input: screen chords for them are never
priority-bound; the chip is the path mid-edit.
Never bind a text-editing chord (Ctrl+K and friends) with `priority=True` on a screen
full of Inputs — the Input's own editing wins there, and the chip stays the mouse path.
Expand All @@ -47,7 +51,10 @@ principles. What this means in practice:
untouched; skit errors are 125/126/127), `--no-input`, `--dry-run`, and dynamic completion.
The non-interactive contract is absolute: in a pipe, in CI, or under `--no-input`, never
guess, never prompt, never silently assemble a broken command. When choosing between designs,
prefer the one an agent can drive deterministically.
prefer the one an agent can drive deterministically. One sanctioned bend, recorded like the
A5 exception: read commands never prompt and never touch user data, but a listing may
self-heal **skit's own registry index** (`store._repair_rows`) — atomic, lock-protected,
skipped under any contention, and convergent, so reads stay safe to run concurrently.

**5. Verification gate:** 100% test coverage floor, ruff, ty (strictest mode), mutation
testing with mutmut (zero surviving mutants), and the i18n coverage gate are all hard CI
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ skit run review --runner codex --set target=src/app.py --no-input

## Install

skit is built on [uv](https://docs.astral.sh/uv/) (tested against 0.11.26). Don't have it? skit asks first, then downloads a pinned uv into its own private directory — your `PATH` and global environment stay untouched. A system-wide [install](https://docs.astral.sh/uv/getting-started/installation/) is still preferred.
skit is built on [uv](https://docs.astral.sh/uv/) (tested against 0.11.26). Don't have it? skit asks first, then downloads a pinned uv into its own private directory — your `PATH` and global environment stay untouched. (Non-interactive runs — CI, pipes — skip the question and download right away.) A system-wide [install](https://docs.astral.sh/uv/getting-started/installation/) is still preferred.

```bash
# Install skit with uv tool from PyPI (the package is named skit-cli; the command is skit)
Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ skit run review --runner codex --set target=src/app.py --no-input

## 安装

skit 建立在 [uv](https://docs.astral.sh/uv/) 之上(以 0.11.26 版本测试)。还没装 uv?skit 会先征得你同意,再把锁定版本的 uv 下载到自己的私有目录——不碰你的 `PATH`,也不碰全局环境。当然,参考[官方文档](https://docs.astral.sh/uv/getting-started/installation/) 安装 uv 会更好。
skit 建立在 [uv](https://docs.astral.sh/uv/) 之上(以 0.11.26 版本测试)。还没装 uv?skit 会先征得你同意,再把锁定版本的 uv 下载到自己的私有目录——不碰你的 `PATH`,也不碰全局环境(在 CI、pipe 这类非交互环境下不会问,直接下载)。当然,参考[官方文档](https://docs.astral.sh/uv/getting-started/installation/) 安装 uv 会更好。

```bash
# 用 uv tool 从 PyPI 安装 skit(包名是 skit-cli,装好的命令是 skit)
Expand Down
2 changes: 1 addition & 1 deletion README.zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ skit run review --runner codex --set target=src/app.py --no-input

## 安裝

skit 建立在 [uv](https://docs.astral.sh/uv/) 之上(以 0.11.26 版測試)。還沒裝 uv?skit 會先徵求你同意,再把釘定版本的 uv 下載到自己的私有目錄——不碰你的 `PATH`,也不碰全域環境。當然,參考[官方文檔](https://docs.astral.sh/uv/getting-started/installation/) 安裝 uv 會更好。
skit 建立在 [uv](https://docs.astral.sh/uv/) 之上(以 0.11.26 版測試)。還沒裝 uv?skit 會先徵求你同意,再把釘定版本的 uv 下載到自己的私有目錄——不碰你的 `PATH`,也不碰全域環境(在 CI、pipe 這類非互動環境下不會問,直接下載)。當然,參考[官方文檔](https://docs.astral.sh/uv/getting-started/installation/) 安裝 uv 會更好。

```bash
# 用 uv tool 從 PyPI 安裝 skit(套件名是 skit-cli,裝好的指令是 skit)
Expand Down
12 changes: 7 additions & 5 deletions docs/content/docs/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ the same actions, deterministic behavior.

- **`--json`** on every read command: `list`, `show`, `params`, `deps`,
`preset list`, `runner list`, `doctor`, `config`.
- **`--no-input`** on the commands that could prompt: `add`, `run`,
`runner remove`. Read commands never prompt and don't take it.
- **`--no-input`** on the commands that could prompt: `add`, `run`, and the
destructive trio `remove`, `preset delete`, `runner remove` (which confirm
unless `--yes`). Read commands never prompt and don't take it.
- **`--dry-run`** on `run` only: prints the exact command — tokens and globs
expanded — and runs nothing.
- **Non-interactive contract**: in a pipe, in CI, or under `--no-input`, skit
Expand Down Expand Up @@ -95,8 +96,9 @@ never replays them.

## skit remove / rename / describe / edit

- `skit remove NAME [--yes]` — remove an entry (the original file is untouched;
copy mode deletes skit's copy). Confirms unless `--yes`.
- `skit remove NAME [--yes] [--no-input]` — remove an entry (the original file
is untouched; copy mode deletes skit's copy). Confirms unless `--yes`;
non-interactively, a missing `--yes` is a clean exit-2 refusal.
- `skit rename NAME NEW` — presets, remembered values, and history follow.
- `skit describe NAME TEXT` — set the description; empty text clears it.
- `skit edit NAME` — open the stored source (the original, in reference mode) in
Expand Down Expand Up @@ -150,7 +152,7 @@ Package deps apply to Python and JS/TS entries; `--need` applies to every kind.
- `skit preset save NAME PRESET [--from-last]` — `--from-last` snapshots the
last run's values without asking.
- `skit preset list NAME [--json]`
- `skit preset delete NAME PRESET`
- `skit preset delete NAME PRESET [--yes] [--no-input]` — confirms unless `--yes`.

## skit runner

Expand Down
5 changes: 4 additions & 1 deletion docs/content/docs/parameters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ per-field `delivers_empty` in `skit show --json`.
a preset: presets store the run verbatim, cleared fields included.
- **The `--` tail** (extra arguments after `--`) is remembered per entry and
replays on the next argument-less run, with a note on stderr. `--forget-args`
clears it; `--raw` never replays it.
clears it; `--raw` never replays it. A replayed tail keeps the semantics it
was captured with: a tail typed into the launch menu's extra field re-expands
its tokens and globs on each run, while a tail your shell already processed
replays literally — in both the CLI and the TUI.
- **Run history**: each entry's state file records the last run's time, exit
code, and exact values.
- All of this lives in skit's state directory (one TOML per entry) — see
Expand Down
10 changes: 6 additions & 4 deletions skills/skit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ is *the user's curated space*: treat it like their dotfiles.
Propose `skit add` when you've written something reusable; don't add it silently.
5. **Pass `--no-input` on every `skit run` and `skit add`.** It guarantees those never
block on a prompt; if information is missing, skit fails fast with a named error
instead. `skit remove` confirms instead of taking `--no-input` — pass `-y`. The
read commands (`list`, `show`, `params`, …) never prompt and don't take the flag.
instead. The destructive commands (`remove`, `preset delete`, `runner remove`)
confirm unless you pass `-y`; non-interactively, a missing `-y` is a clean exit-2
refusal, never a hang. The read commands (`list`, `show`, `params`, …) never prompt
and don't take the flag.

## Discover entries

Expand Down Expand Up @@ -200,7 +202,7 @@ Named value sets per entry, ideal for recurring jobs:
skit run <name> --set a=1 --set b=2 --save-preset nightly --dry-run --no-input # create without running
skit preset list <name> --json
skit run <name> -p nightly --no-input
skit preset delete <name> nightly
skit preset delete <name> nightly -y # confirms without -y
```

## Prompts & runners
Expand Down Expand Up @@ -249,7 +251,7 @@ skit params <name> --no-interpolate # switch insertion off; --interpolate turns
skit runner list --json # [{"name": …, "argv": […]}]
skit runner add mycli -- mycli run {{prompt}} # each word = one argument, no shell
skit runner add mycli --force -- mycli run --model opus {{prompt}} # --force replaces an existing runner (edit)
skit runner remove mycli -y # confirms without -y, like skit remove
skit runner remove mycli -y # confirms without -y, like skit remove and preset delete
```

- The agent's own per-run flags pass through after `--`:
Expand Down
15 changes: 15 additions & 0 deletions src/skit/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@
from collections.abc import Callable, Mapping


@dataclass
class ArgSpec:
"""A script's own statically-read CLI surface (argparse/click/typer, parseArgs,
getopts, fish argparse, PowerShell param()). Lives HERE with the other neutral
result types — Candidate/Analysis/Report — for the same reason they do: every
language's cli_reader returns one, and a shell reader must not import the Python
analyzer package just to name its result type."""

# Each field is a delivery=flag ParamDecl (binding="none": the script owns the parser,
# skit only reflects it). List position carries declaration order — there is no order field.
fields: list[ParamDecl] = field(default_factory=list)
ok: bool = True # False -> whole-parser degradation (passthrough escape only)
reason: str = "" # symbolic: "subparsers" | "dynamic" (UI owns the wording)


@dataclass
class Candidate:
"""A candidate parameter. const/envdefault are keyed by variable name; input by call
Expand Down
24 changes: 23 additions & 1 deletion src/skit/argstate.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,25 @@ def _strip_secrets(values: dict[str, str], secret_names: Iterable[str]) -> dict[


def load_state(slug: str) -> dict[str, Any]:
"""Return {"values": {…}, "extra_args": […], "presets": {name: {…}}, "last_run": {…}}.
"""Return {"values": {…}, "extra_args": […], "extra_args_raw": bool,
"presets": {name: {…}}, "last_run": {…}}.

last_run is {"at": ISO-8601 str, "exit": int} after the first recorded run, else {}.
extra_args_raw says HOW the remembered tail was captured: True = raw intent text
(the TUI's extra field — tokens/globs expand on replay), False/absent = already
shell-processed (the CLI's `-- args` — replays literally, in both faces). Without
it, one stored tail replayed under two different expansion regimes depending on
which face happened to rerun it.
"""
doc = _load_doc(slug)
return {
"values": dict(doc.get("values", {})),
"extra_args": list(doc.get("extra_args", [])),
# `is True`, not bool(): the house rule for hand-editable bools (models.py's
# interpolate, config.py's enabled). A hand-edited `extra_args_raw = "no"`
# must degrade to the safe literal-replay default, never coerce truthy toward
# re-expansion — the exact direction the provenance marker exists to prevent.
"extra_args_raw": doc.get("extra_args_raw") is True,
"presets": {k: dict(v) for k, v in doc.get("presets", {}).items()},
"last_run": dict(doc.get("last_run", {})),
}
Expand All @@ -107,6 +118,7 @@ def save_last(
*,
values: dict[str, str] | None = None,
extra_args: list[str] | None = None,
extra_args_raw: bool = False,
secret_names: Iterable[str] = (),
) -> None:
"""Remember last-used (read-modify-write, keeping presets). Secret keys are stripped (C3).
Expand All @@ -116,6 +128,10 @@ def save_last(
check made cleared extra args resurrect forever: the form saved nothing, the next
run re-read the old value, reused it, and wrote it back.)

extra_args_raw records the tail's provenance (see load_state) and travels WITH the
tail: it is written or cleared exactly when extra_args is, so a marker can never
describe a tail it didn't come with.

Even on a call that carries no new values, any name in secret_names is dropped from
the previously-stored values — a value saved while a parameter was public must not
survive on disk after it becomes secret.
Expand All @@ -129,6 +145,12 @@ def save_last(
doc["values"] = _strip_secrets(doc.get("values", {}), banned)
if extra_args is not None:
doc["extra_args"] = extra_args
if extra_args and extra_args_raw:
doc["extra_args_raw"] = True
else:
# _save_doc prunes falsy values, so False is stored as absence; pop so a
# cleared/processed tail never inherits a stale raw marker.
doc.pop("extra_args_raw", None)
_save_doc(slug, doc)


Expand Down
Loading