Skip to content

Reflective sprint: six load-bearing improvements - #12

Merged
FrancoisChastel merged 6 commits into
mainfrom
feat/cli-redesign-recall
Jun 15, 2026
Merged

Reflective sprint: six load-bearing improvements#12
FrancoisChastel merged 6 commits into
mainfrom
feat/cli-redesign-recall

Conversation

@FrancoisChastel

Copy link
Copy Markdown
Owner

Six commits, each independently reviewable, addressing the highest-leverage items from the recent reflective review.

What landed

1. `.shellllmrc` — per-project context (commit 1)

A plain-text file at any directory above `cwd` (innermost wins, walk stops at `$HOME`). Its contents become a "Project context" system message that prefixes every `,` and `?` invocation in that tree. Like `.editorconfig` for the LLM. Read fresh every call so edits take immediate effect; 4KB cap with tail truncation.

2. Diagnose-then-suggest reliability (commit 2)

Five worked examples baked into the `,, ` system prompt — one per category — so smaller tiers (fast MoE) classify as well as balanced. Adds a fourth category, `Uncertain:`, that the model can emit when it genuinely can't tell which fix is right; the CLI sees that and falls through to the picker instead of dropping a confidently-wrong fix.

Live verification, both tiers, four cases:

Case Fast Balanced
`du --max-deth` Typo ✓ Typo ✓
`git log` no-repo Environment ✓ Environment ✓
`kubectl` auth-fail Uncertain ✓ Uncertain ✓
`cat nonexistent` Environment ✓ Environment ✓

3. Archive lifecycle (commit 3)

The archive grew unbounded. New commands:

```
??? --prune --older-than 90d # drop sessions older than 90 days
??? --prune --keep 1000 # keep only the 1000 most recent
??? --vacuum # reclaim disk space after a prune
??? --status # now also shows MB on disk
```

Age spec accepts `s/m/h/d/w` suffixes; garbage rejected with a clear error. Composes (both flags in one call cuts both ways). 29 new tests.

4. `?? --doctor` health check (commit 4)

One command checks: platform, required & optional binaries on PATH, zsh layer sourced, environment variables that bite people, every chat tier with loaded model name, embed server, archive size + counts, and which tier models are in the HF cache. Plain ASCII output — round-trips through GitHub issues, Slack pastes, and email verbatim. Read-only by design.

5. Streaming for `,` and `,,` (commit 5)

The fix-mode no-picker case (90% of `,,` usage) used to wait for the full JSON. Now we stream:

```
[before] thinking… ~5-10s → print fix
[after] thinking… (1 so far) ~1.3s → diagnosis line + print fix
```

Measured live: 1.27s end-to-end on the fast tier for `du --max-deth` → corrected fix on prompt + diagnosis on stderr. Two robustness wins along the way: `_extract_complete_command_items` uses `raw_decode` to chip complete `{command,note}` objects off the stream (handles partial strings with escapes); `_try_extract_diagnosis` waits for the closing quote of the string value (avoids surfacing a truncated "Typ" when the full "Typo: ..." hasn't arrived yet).

6. mypy type checking in CI (commit 6)

Added between `ruff format --check` and `pytest`. Moderate config (`check_untyped_defs`, `warn_unreachable`, `strict_optional`), not strict. Two pre-existing type errors fixed: a `_pick` result that wasn't narrowed, and a name reuse for `assistant_msg` with incompatible dict shapes.

Test plan

  • 302 tests pass (`pytest -q`) — 38 new across the six features
  • `ruff check` and `ruff format --check` clean
  • `mypy` clean (17 files)
  • Live verification of `,,` streaming hot path: 1.27s end-to-end (was 5-10s)
  • Live verification of category classification on both fast and balanced tiers
  • `?? --doctor` runs cleanly with three tiers up, embed up
  • Manual: create a project `.shellllmrc`, verify it appears in the next `,` call's system messages
  • Manual: `??? --prune --keep 5` after archive has accumulated more sessions; verify count drops and `--vacuum` reclaims expected space

A small plain-text file at the root of (or anywhere above the cwd
inside) your project. Its contents become a "Project context" system
message that prefixes every `,` and `?` invocation in that tree.

  $ cat .shellllmrc
  This project uses pnpm (not npm).
  We target Python 3.11+.
  Prefer ripgrep over grep.

  $ , find files modified today
  → proposes `rg --files --max-age 1d ...` instead of `find`

Discovery walks up from cwd, stops at $HOME (we never read system-
wide rc files), innermost file wins — same precedence as
.editorconfig / .gitignore. Read fresh every call so edits take
immediate effect; capped at 4KB with tail truncation so a runaway
file can't blow out the context budget.

Wired through both `,` (comma.py _build_messages) and `?` (ask.py
run_agent) so any English-prompt or repair call picks the context
up. Doesn't touch `???` recall — that's archive search, not new
conversation.

9 new tests cover discovery (cwd, walk-up, innermost-wins, stops at
HOME), rendering (system message format, blank-file handling), and
the size cap.

Today only the system message is documented for users to put project
conventions in. The file format stays plain text so we can add
structured keys later without breaking anything.
The diagnose-then-suggest design only works as well as the model's
category assignment. On the fast tier I measured the kubectl
auth-failure case getting misclassified as 'Logic:' — the model
forced a category because the prompt offered no out, and the
suggestions degraded accordingly.

Two tightenings:

1. **Five worked examples** baked into the system prompt, one per
   category, showing the exact shape (`previous command:` /
   `output:` / `→ Category: ...` / `→ first command: ...`). Smaller
   models match the pattern reliably; even the fast tier (3B-active
   MoE) now classifies the kubectl case correctly.

2. **'Uncertain:'** as a fourth valid category. When the model
   genuinely can't tell which of Typo/Environment/Logic applies, it
   says so — and `_pick` is invoked automatically instead of dropping
   a potentially-wrong fix on the prompt. Better honest uncertainty
   than confident wrongness.

Live verification (post-change, both tiers):

  case              fast              balanced
  -----------       ----------------  ----------------
  du --max-deth     Typo ✓            Typo ✓
  git log no-repo   Environment ✓     Environment ✓
  kubectl auth      Uncertain ✓       Uncertain ✓

The Uncertain branch composes with --pick: passing --pick still
shows the picker; emitting 'Uncertain:' also shows the picker. The
two paths converge on the same fall-through code.

275 tests pass, including a new one that mocks an Uncertain
diagnosis and asserts the picker fires even without --pick.
The archive grows forever today. No TTL, no cap, no compactor —
power users would end up with a 500MB archive.db in six months and
recall slows down silently. Three new commands plus a status hint
make the archive operable:

  ??? --prune --older-than 90d        drop sessions older than 90 days
  ??? --prune --keep 1000             keep only the 1000 most recent
  ??? --prune --older-than 30d --keep 500    both (cuts both ways)
  ??? --vacuum                        reclaim disk space after a prune
  ??? --status                        now also shows MB on disk

Age spec accepts s / m / h / d / w suffixes (`30d`, `12h`, `2w`).
Garbage rejected at parse time with a clear error.

The Archive class gains three methods (`prune_older_than`,
`prune_to_keep_newest`, `vacuum`, plus `db_size_bytes`). The FTS5
search index is kept consistent automatically — the schema's DELETE
trigger handles it, no manual rebuild needed.

`?? --prune` and `--vacuum` are mutually exclusive with the
recall-filter flags (`--ask` / `--comma`) the same way the other
global modes are — pruning is global, filtering recall is per-cmd.

29 new tests (archive-level + CLI-level) cover happy paths, edge
cases (zero kept, no rows to prune), and the parse rejections.
\`?? --doctor\` runs every check a new user is likely to hit during
setup and prints them in one screen. Exit code is 0 if everything
required is fine, 1 if anything required is missing.

What it checks:
  - platform basics (uname, shell)
  - required binaries (shellllm-{comma,ask,recall}, llama-server, fzf)
  - optional binaries (huggingface-cli — informational, no fail)
  - zsh layer sourced
  - the three SHELLLM_* env vars that bite people most often
    (SHELLLM_SHELL_CONTEXT, SHELLLM_BASE_URL, SHELLLM_AUTOSTART)
  - every chat tier's running state + the loaded model name
  - embed server (optional, marked as info)
  - archive + memory counts and on-disk size
  - which tier models are already in the HF cache

Output is plain ASCII (no Unicode glyphs, no colour) so it
round-trips through GitHub issues, Slack pastes, and email
verbatim — copy-paste it into a bug report and the maintainer has
everything they need in one block.

Read-only by design: \`--doctor\` never starts/stops anything, never
touches the archive, never modifies anything in the user's
environment. Safe to run as the first thing in any new shell.

Wired into the \`??\` dispatcher and listed in \`?? --help\`.
The fix-mode no-picker case (the bare ,, that 90% of users will hit
90% of the time) used to wait for the full JSON to arrive before
doing anything. On the balanced tier that's 5-10s of "thinking…"
for what amounts to a typo correction the model knew in the first
half second.

Now we stream the JSON. As soon as the first complete `{command,note}`
object is parseable AND a non-Uncertain diagnosis has streamed, the
hot path bails out of the iteration and prints the command. End-to-end
latency drops to ~1.3s on the fast tier in live measurement.

What changed:

- client.chat_stream now accepts response_format=json_schema (it
  previously only handled the tool-call streaming case). The
  server-side stream is unchanged.

- comma._extract_complete_command_items uses json.JSONDecoder.raw_decode
  to chip complete {command,note} objects off the streaming buffer one
  at a time. Robust against partial strings with escapes.

- comma._try_extract_diagnosis recovers the diagnosis field as soon as
  its closing quote has streamed. Earlier "append closing brace" tricks
  could surface truncated diagnoses (`Typ` instead of `Typo: ...`); the
  raw_decode approach won't fire until the JSON string value has
  legitimately terminated.

- comma._stream_command_items wraps both above: yields each command
  object as it forms, surfaces the diagnosis via a callback, and lets
  the caller break out of iteration early when it has what it needs.

- comma.main's hot path: spinner shows `thinking… (N so far)` updates,
  fix-mode-no-pick-no-Uncertain breaks after the first item + diagnosis,
  the diagnosis is captured during streaming and surfaced to stderr
  *after* the spinner exits (writing to stderr while Rich holds the
  cursor was producing mangled output).

The picker path (plain `,`, `, --ctx`, `,, --pick`, and Uncertain
diagnoses) still accumulates the full list before showing fzf —
streaming wins there are smaller and would need a more complex fzf
reload protocol.

Test fixture updates: `fake_model` now patches both chat and
chat_stream. A `_patch_chat_both` helper lets local fakes do the same
without copying boilerplate. All 302 tests pass; no behavior change
for any flow we previously tested.
ruff catches style and basic correctness; mypy catches the kind of
bug that's currently caught only by noticing weird behaviour during
demo renders. With the new streaming hot path and the diagnose-then-
suggest schema work, that's exactly the kind of mistake that gets
through ruff and bites at runtime.

Two pre-existing errors surfaced:
  - comma.main: `chosen = _pick(...)` returned `str | None` but was
    later used as `str`. Now explicitly typed and narrowed.
  - ask.run_agent: two heterogeneous-dict literals (one with tool_calls,
    one without) shared a variable name and produced no-redef +
    incompatible-dict-item errors. Renamed the second to
    `assistant_with_tools` and gave both `dict[str, Any]` annotations.

mypy config in pyproject.toml is intentionally moderate, not strict:
  - check_untyped_defs / no_implicit_optional / strict_optional /
    warn_unused_ignores / warn_redundant_casts / warn_unreachable
  - tests/* is excluded — the monkey-patched fakes there are not the
    place to fight types.

CI gets a new step between `ruff format --check` and `pytest`:
  - name: Type-check with mypy
    run: mypy

Local dev: `pip install -e ".[dev]"` now pulls mypy too.

302 tests pass, ruff clean, ruff format clean, mypy clean.
@FrancoisChastel
FrancoisChastel merged commit f9d4499 into main Jun 15, 2026
2 checks passed
@FrancoisChastel
FrancoisChastel deleted the feat/cli-redesign-recall branch June 15, 2026 19:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant