Skip to content

feat(cli): harden and polish loci init command#9

Merged
senna-lang merged 4 commits into
mainfrom
fix/init-hardening
Apr 21, 2026
Merged

feat(cli): harden and polish loci init command#9
senna-lang merged 4 commits into
mainfrom
fix/init-hardening

Conversation

@senna-lang
Copy link
Copy Markdown
Owner

Summary

This PR hardens loci init across multiple dimensions — UI, prompt robustness, resilience to failures, automatic hook installation, and better error messages for environment-level problems. Re-running loci init after a failure is now safe.

Changes

UI: richer startup banner

  • Renders codeatrium in the pagga half-block figlet font with a blue vertical gradient (#7bb8ff → #1b45a8), ⦿ mascot glyph, tagline, and version — framed by a rich Panel. Fits in 80 cols without wrapping.

Interactive prompts: no more silent fallback

Previously, invalid prompt input silently defaulted — "y" for Yes was treated as No because only "2" worked. Now:

  • Re-prompt on invalid input for all 4 interactive prompts (min_chars, skip-count choice, custom counts, distill priority).
  • yes/no aliases: 1/2/y/n/yes/no (case/whitespace tolerant) for the "run distillation now?" prompt.
  • Range validation: custom exchange count must be 1..total; custom min_chars must be >= 0.

Resilience: safe failure & interrupt

  • Execution-phase try/except: if anything fails mid-init, .codeatrium/ is removed (shutil.rmtree) so re-running loci init is safe. Previously the half-created DB was picked up by the Already initialized fast-path and blocked retry.
  • KeyboardInterrupt: exits cleanly with code 130 and the same cleanup.
  • Per-file resilience: one bad .jsonl no longer aborts the indexing loop — it logs ⚠ skip <file>: <err> and continues.
  • git_root catches FileNotFoundError: no crash when git isn't on $PATH.
  • parse_exchanges returns [] for missing files instead of raising.
  • find_project_root no longer traverses parents when outside a git repo (prevents picking up an unrelated .codeatrium/).

Automation: hooks auto-installed

  • loci init now calls install_hooks at the end (writes to ~/.claude/settings.json). loci init + loci hook install → just loci init.
  • Opt out with --no-hooks.
  • Hook install failure is warn-only; DB is preserved and user can retry with loci hook install.

Embedder fail-fast with remediation hints

Previously a broken sentence_transformers import (common cause: numpy 2.x + older pyarrow) produced a full traceback followed by 242 per-row error lines. Now:

  • New EmbedderSetupError distinguishes environment-level failures from per-row errors.
  • _ensure_model wraps import/init failures with a remediation-aware message (pip install 'numpy<2' or pip install -U pyarrow).
  • distill_all re-raises EmbedderSetupError to stop the loop on first occurrence.
  • loci init's distill handler catches it specifically and prints a single clean message — no traceback, no per-row spam.

Test plan

  • Banner renders with pagga characters and subtitle
  • Invalid prompt input re-prompts with "Invalid choice" hint
  • y / n aliases accepted; custom count range-validated
  • Execution-phase failure triggers .codeatrium/ cleanup
  • KeyboardInterrupt exits 130 with cleanup
  • Per-file index failure doesn't abort init
  • Hooks installed by default
  • --no-hooks skips install
  • Hook install failure warn-only, DB preserved
  • EmbedderSetupError wraps import failure
  • init shows friendly message on EmbedderSetupError, preserves DB
  • All 142 tests pass (pytest, ruff check, pyright clean)

🤖 Generated with Claude Code

Revamp the `loci init` experience across several axes:

UI
- New startup banner: pagga half-block figlet rendering "codeatrium"
  with a blue vertical gradient, ⦿ mascot, tagline, and version —
  framed by a rich Panel. Fits in 80 cols with no wrapping.

Interactive prompts
- Re-prompt on invalid input instead of silent fallback (previously
  "y" for Yes was treated as No because only "2" worked).
- Accept y/n/yes/no (case/whitespace tolerant) for the distill-now
  prompt.
- Range-validate custom exchange counts (1..total) and custom
  min_chars (>= 0).

Resilience
- Wrap the execution phase in try/except with shutil.rmtree cleanup
  on failure or KeyboardInterrupt — retrying `loci init` is safe
  again (previously a half-initialized DB blocked re-init).
- Per-file try/except around index_file loop so one bad .jsonl
  logs a warning and doesn't abort the whole run.
- git_root catches FileNotFoundError (handles missing git binary).
- parse_exchanges returns [] for missing files instead of raising.
- find_project_root no longer traverses parents outside a git repo
  (avoids picking up an unrelated .codeatrium/).

Automation
- Auto-install Claude Code hooks at the end of init. Opt out with
  --no-hooks. Hook failure is warn-only; DB is preserved.

Embedder fail-fast
- New EmbedderSetupError distinguishes environment-level failures
  (numpy/pyarrow binary mismatch) from per-row errors.
- distill_all re-raises EmbedderSetupError to stop the loop early.
- init's distill handler prints a single friendly message with
  remediation hints ("pip install 'numpy<2' or pip install -U
  pyarrow") instead of a full traceback followed by N per-row
  error lines.

Tests
- 12 new tests covering invalid-input reprompts, cleanup on
  failure, KeyboardInterrupt, per-file resilience, hook install
  paths (default / --no-hooks / failure), and EmbedderSetupError
  wrapping + friendly message. All 142 tests pass.
- Add autouse fixture in test_init.py that redirects $HOME to
  tmp_path so tests don't touch the real ~/.claude/settings.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented Apr 21, 2026

Code Review

One issue found.

Bug: failure-cleanup can delete pre-existing .codeatrium/ contents

File: src/codeatrium/cli/__init__.py, lines 121-124

The guard at line 88 only checks db.exists() (memory.db), not whether .codeatrium/ itself pre-existed. Meanwhile, the config-write at line 127 intentionally skips overwriting a user-authored config.toml but both exception handlers at lines 194 and 199 unconditionally rmtree the entire directory, including any files init did not create.

Concrete scenario: a user pre-creates .codeatrium/config.toml with custom settings before running loci init. The db.exists() guard is bypassed (no memory.db yet), line 127 correctly skips overwriting their config but a mid-init failure (Ctrl-C or disk error) calls rmtree on the whole directory, destroying that file.

The inconsistency is clear: line 127 goes out of its way not to overwrite a pre-existing config.toml, yet the cleanup at lines 194/199 destroys it anyway.

Suggested fix: capture whether .codeatrium/ pre-existed before the try block and guard the cleanup with if not dir_preexisted: shutil.rmtree(...).

- assets/banner.svg generated via Freeze (charmbracelet/tap/freeze) so
  the font glyphs are embedded as <path> elements and the rendering is
  consistent across GitHub and local browsers — no fallback-font seams
  or row-boundary lines.
- scripts/generate-banner.sh reproducibly regenerates the SVG from the
  live `loci` banner.
- README.md / README.ja.md embed the banner above the H1 and are
  updated to reflect the new init behavior: hook auto-install (with
  --no-hooks opt-out), y/n prompt aliases, re-prompt on invalid input,
  and automatic cleanup on failure.
- Swap the tagline mascot from ⦿ to ● (guaranteed single-width in
  JetBrains Mono) so the rich Panel border stays flush.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@senna-lang
Copy link
Copy Markdown
Owner Author

@claude
fix

@claude
Copy link
Copy Markdown

claude Bot commented Apr 21, 2026

Claude finished @senna-lang's task in 1m 46s —— View job


Fixed: pre-existing .codeatrium/ preserved on failure cleanup

  • Gather context and understand the request
  • Read src/codeatrium/cli/__init__.py — found the issue at lines 122, 194, 199
  • Implement fix: capture dir_preexisted before the try block, guard rmtree with it
  • Add regression test test_init_preserves_preexisting_dir_on_failure
  • Commit and push changes

What changed:

src/codeatrium/cli/__init__.py — one line added before the try block:

dir_preexisted = codeatrium_dir.exists()

Both shutil.rmtree calls in the except handlers now check if not dir_preexisted: before removing the directory. If .codeatrium/ existed before loci init ran, it is left untouched on failure — consistent with the existing logic that already skips overwriting a pre-existing config.toml.

tests/test_init.py — new regression test test_init_preserves_preexisting_dir_on_failure: pre-creates .codeatrium/config.toml with custom content, injects a mid-init failure, then asserts the directory and file survive intact.
| Branch: fix/init-hardening

github-actions Bot and others added 2 commits April 21, 2026 08:24
Capture `dir_preexisted` before the execution-phase `try` block and
guard both `shutil.rmtree` cleanup calls with `if not dir_preexisted`.
Previously, a mid-init failure (exception or Ctrl-C) would unconditionally
delete the entire `.codeatrium/` directory, destroying files the user had
created before running `loci init` (e.g. a custom `config.toml`). This
was inconsistent with the config-write path, which already skips
overwriting pre-existing `config.toml`.

Also adds a regression test `test_init_preserves_preexisting_dir_on_failure`
to verify the directory and its contents survive an execution-phase failure.

Co-authored-by: sena inomata <senna-lang@users.noreply.github.com>
- pyproject.toml / __init__.py: 0.1.0 → 0.2.0
- CHANGELOG.md: add 0.2.0 section documenting init auto-hook-install,
  --no-hooks flag, invalid-input re-prompts, cleanup-on-failure,
  per-file resilience, EmbedderSetupError, and the SVG README banner.
- Regenerate assets/banner.svg so the embedded version string matches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@senna-lang senna-lang merged commit 66be3fa into main Apr 21, 2026
3 checks passed
@senna-lang senna-lang deleted the fix/init-hardening branch April 21, 2026 12:18
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