diff --git a/.claude/ai_notes/docs_todos.md b/.claude/ai_notes/docs_todos.md new file mode 100644 index 00000000..4a4a53c8 --- /dev/null +++ b/.claude/ai_notes/docs_todos.md @@ -0,0 +1,38 @@ +# Docs TODOs + +## Auto-sync README code examples with source + +The `docs/README.rst` has inline code blocks that +duplicate actual example files (e.g. +`examples/infected_asyncio_echo_server.py`). Every time +the public API changes we have to manually sync both. + +Sphinx's `literalinclude` directive can pull code directly +from source files: + +```rst +.. literalinclude:: ../examples/infected_asyncio_echo_server.py + :language: python + :caption: examples/infected_asyncio_echo_server.py +``` + +Or to include only a specific function/section: + +```rst +.. literalinclude:: ../examples/infected_asyncio_echo_server.py + :language: python + :pyobject: aio_echo_server +``` + +This way the docs always reflect the actual code without +manual syncing. + +### Considerations +- `README.rst` is also rendered on GitHub/PyPI which do + NOT support `literalinclude` - so we'd need a build + step or a separate `_sphinx_readme.rst` (which already + exists at `docs/github_readme/_sphinx_readme.rst`). +- Could use a pre-commit hook or CI step to extract code + from examples into the README for GitHub rendering. +- Another option: `sphinx-autodoc` style approach where + docstrings from the actual module are pulled in. diff --git a/.claude/settings.local.json b/.claude/settings.local.json index d2c8f9d9..16c85cf9 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,13 +1,32 @@ { "permissions": { "allow": [ - "Write(.claude/*commit_msg*)", - "Write(.claude/git_commit_msg_LATEST.md)", "Bash(date *)", "Bash(cp .claude/*)", "Bash(git diff *)", "Bash(git log *)", - "Bash(git status)" + "Bash(git status)", + "Bash(git remote:*)", + "Bash(git stash:*)", + "Bash(git mv:*)", + "Bash(test:*)", + "Bash(ls:*)", + "Bash(grep:*)", + "Bash(find:*)", + "Bash(ln:*)", + "Bash(cat:*)", + "Bash(mkdir:*)", + "Bash(gh pr:*)", + "Bash(gh api:*)", + "Bash(UV_PROJECT_ENVIRONMENT=py313 uv sync:*)", + "Bash(UV_PROJECT_ENVIRONMENT=py313 uv run:*)", + "Write(.claude/*commit_msg*)", + "Write(.claude/git_commit_msg_LATEST.md)", + "Skill(run-tests)", + "Bash(echo EXIT:$?:*)", + "Bash(gh api:*)", + "Bash(gh pr:*)", + "Bash(gh issue:*)" ], "deny": [], "ask": [] diff --git a/.claude/skills/conc-anal/SKILL.md b/.claude/skills/conc-anal/SKILL.md new file mode 100644 index 00000000..4f498b7c --- /dev/null +++ b/.claude/skills/conc-anal/SKILL.md @@ -0,0 +1,231 @@ +--- +name: conc-anal +description: > + Concurrency analysis for tractor's trio-based + async primitives. Trace task scheduling across + checkpoint boundaries, identify race windows in + shared mutable state, and verify synchronization + correctness. Invoke on code segments the user + points at, OR proactively when reviewing/writing + concurrent cache, lock, or multi-task acm code. +argument-hint: "[file:line-range or function name]" +allowed-tools: + - Read + - Grep + - Glob + - Task +--- + +Perform a structured concurrency analysis on the +target code. This skill should be invoked: + +- **On demand**: user points at a code segment + (file:lines, function name, or pastes a snippet) +- **Proactively**: when writing or reviewing code + that touches shared mutable state across trio + tasks — especially `_Cache`, locks, events, or + multi-task `@acm` lifecycle management + +## 0. Identify the target + +If the user provides a file:line-range or function +name, read that code. If not explicitly provided, +identify the relevant concurrent code from context +(e.g. the current diff, a failing test, or the +function under discussion). + +## 1. Inventory shared mutable state + +List every piece of state that is accessed by +multiple tasks. For each, note: + +- **What**: the variable/dict/attr (e.g. + `_Cache.values`, `_Cache.resources`, + `_Cache.users`) +- **Scope**: class-level, module-level, or + closure-captured +- **Writers**: which tasks/code-paths mutate it +- **Readers**: which tasks/code-paths read it +- **Guarded by**: which lock/event/ordering + protects it (or "UNGUARDED" if none) + +Format as a table: + +``` +| State | Writers | Readers | Guard | +|---------------------|-----------------|-----------------|----------------| +| _Cache.values | run_ctx, moc¹ | moc | ctx_key lock | +| _Cache.resources | run_ctx, moc | moc, run_ctx | UNGUARDED | +``` + +¹ `moc` = `maybe_open_context` + +## 2. Map checkpoint boundaries + +For each code path through the target, mark every +**checkpoint** — any `await` expression where trio +can switch to another task. Use line numbers: + +``` +L325: await lock.acquire() ← CHECKPOINT +L395: await service_tn.start(...) ← CHECKPOINT +L411: lock.release() ← (not a checkpoint, but changes lock state) +L414: yield (False, yielded) ← SUSPEND (caller runs) +L485: no_more_users.set() ← (wakes run_ctx, no switch yet) +``` + +**Key trio scheduling rules to apply:** +- `Event.set()` makes waiters *ready* but does NOT + switch immediately +- `lock.release()` is not a checkpoint +- `await sleep(0)` IS a checkpoint +- Code in `finally` blocks CAN have checkpoints + (unlike asyncio) +- `await` inside `except` blocks can be + `trio.Cancelled`-masked + +## 3. Trace concurrent task schedules + +Write out the **interleaved execution trace** for +the problematic scenario. Number each step and tag +which task executes it: + +``` +[Task A] 1. acquires lock +[Task A] 2. cache miss → allocates resources +[Task A] 3. releases lock +[Task A] 4. yields to caller +[Task A] 5. caller exits → finally runs +[Task A] 6. users-- → 0, sets no_more_users +[Task A] 7. pops lock from _Cache.locks +[run_ctx] 8. wakes from no_more_users.wait() +[run_ctx] 9. values.pop(ctx_key) +[run_ctx] 10. acm __aexit__ → CHECKPOINT +[Task B] 11. creates NEW lock (old one popped) +[Task B] 12. acquires immediately +[Task B] 13. values[ctx_key] → KeyError +[Task B] 14. resources[ctx_key] → STILL EXISTS +[Task B] 15. 💥 RuntimeError +``` + +Identify the **race window**: the range of steps +where state is inconsistent. In the example above, +steps 9–10 are the window (values gone, resources +still alive). + +## 4. Classify the bug + +Categorize what kind of concurrency issue this is: + +- **TOCTOU** (time-of-check-to-time-of-use): state + changes between a check and the action based on it +- **Stale reference**: a task holds a reference to + state that another task has invalidated +- **Lifetime mismatch**: a synchronization primitive + (lock, event) has a shorter lifetime than the + state it's supposed to protect +- **Missing guard**: shared state is accessed + without any synchronization +- **Atomicity gap**: two operations that should be + atomic have a checkpoint between them + +## 5. Propose fixes + +For each proposed fix, provide: + +- **Sketch**: pseudocode or diff showing the change +- **How it closes the window**: which step(s) from + the trace it eliminates or reorders +- **Tradeoffs**: complexity, perf, new edge cases, + impact on other code paths +- **Risk**: what could go wrong (deadlocks, new + races, cancellation issues) + +Rate each fix: `[simple|moderate|complex]` impl +effort. + +## 6. Output format + +Structure the full analysis as: + +```markdown +## Concurrency analysis: `` + +### Shared state + + +### Checkpoints + + +### Race trace + + +### Classification + + +### Fixes + +``` + +## Tractor-specific patterns to watch + +These are known problem areas in tractor's +concurrency model. Flag them when encountered: + +### `_Cache` lock vs `run_ctx` lifetime + +The `_Cache.locks` entry is managed by +`maybe_open_context` callers, but `run_ctx` runs +in `service_tn` — a different task tree. Lock +pop/release in the caller's `finally` does NOT +wait for `run_ctx` to finish tearing down. Any +state that `run_ctx` cleans up in its `finally` +(e.g. `resources.pop()`) is vulnerable to +re-entry races after the lock is popped. + +### `values.pop()` → acm `__aexit__` → `resources.pop()` gap + +In `_Cache.run_ctx`, the inner `finally` pops +`values`, then the acm's `__aexit__` runs (which +has checkpoints), then the outer `finally` pops +`resources`. This creates a window where `values` +is gone but `resources` still exists — a classic +atomicity gap. + +### Global vs per-key counters + +`_Cache.users` as a single `int` (pre-fix) meant +that users of different `ctx_key`s inflated each +other's counts, preventing teardown when one key's +users hit zero. Always verify that per-key state +(`users`, `locks`) is actually keyed on `ctx_key` +and not on `fid` or some broader key. + +### `Event.set()` wakes but doesn't switch + +`trio.Event.set()` makes waiting tasks *ready* but +the current task continues executing until its next +checkpoint. Code between `.set()` and the next +`await` runs atomically from the scheduler's +perspective. Use this to your advantage (or watch +for bugs where code assumes the woken task runs +immediately). + +### `except` block checkpoint masking + +`await` expressions inside `except` handlers can +be masked by `trio.Cancelled`. If a `finally` +block runs from an `except` and contains +`lock.release()`, the release happens — but any +`await` after it in the same `except` may be +swallowed. This is why `maybe_open_context`'s +cache-miss path does `lock.release()` in a +`finally` inside the `except KeyError`. + +### Cancellation in `finally` + +Unlike asyncio, trio allows checkpoints in +`finally` blocks. This means `finally` cleanup +that does `await` can itself be cancelled (e.g. +by nursery shutdown). Watch for cleanup code that +assumes it will run to completion. diff --git a/.claude/skills/pr-msg/format-reference.md b/.claude/skills/pr-msg/format-reference.md new file mode 100644 index 00000000..0b46ff6f --- /dev/null +++ b/.claude/skills/pr-msg/format-reference.md @@ -0,0 +1,241 @@ +# PR/Patch-Request Description Format Reference + +Canonical structure for `tractor` patch-request +descriptions, designed to work across GitHub, +Gitea, SourceHut, and GitLab markdown renderers. + +**Line length: wrap at 72 chars** for all prose +content (Summary bullets, Motivation paragraphs, +Scopes bullets, etc.). Fill lines *to* 72 — don't +stop short at 50-65. Only raw URLs in +reference-link definitions may exceed this. + +## Template + +```markdown + + +## + +### Summary +- [][] Description of change ending + with period. +- [][] Another change description + ending with period. +- [][] [][] Multi-commit + change description. + +### Motivation +<1-2 paragraphs: problem/limitation first, +then solution. Hard-wrap at 72 chars.> + +### Scopes changed +- [][] `pkg.mod.func()` — what + changed. + * [][] Also adjusts + `.related_thing()` in same module. +- [][] `tests.test_mod` — new/changed + test coverage. + + + +(this pr content was generated in some part by +[`claude-code`][claude-code-gh]) + +[]: https://///commit/ +[claude-code-gh]: https://github.com/anthropics/claude-code + + +``` + +## Markdown Reference-Link Strategy + +Use reference-style links for ALL commit hashes +and cross-service PR refs to ensure cross-service +compatibility: + +**Inline usage** (in bullets): +```markdown +- [f3726cf9][f3726cf9] Add `reg_err_types()` + for custom exc lookup. +``` + +**Definition** (bottom of document): +```markdown +[f3726cf9]: https://github.com/goodboy/tractor/commit/f3726cf9 +``` + +### Why reference-style? +- Keeps prose readable without long inline URLs. +- All URLs in one place — trivially swappable + per-service. +- Most git services auto-link bare SHAs anyway, + but explicit refs guarantee it works in *any* + md renderer. +- The `[hash][hash]` form is self-documenting — + display text matches the ref ID. +- Cross-service PR refs use the same mechanism: + `[github-pr][]` resolves via a ref-link def + at the bottom, trivially fillable post-submit. + +## Cross-Service PR Placeholder Mechanism + +The generated description includes three layers +of cross-service support, all using native md +reference-links: + +### 1. Metadata comment (top of file) + +```markdown + +``` + +A YAML-ish HTML comment block. The `___` +placeholders get filled with PR/patch numbers +after submission. Machine-parseable for tooling +(e.g. `gish`) but invisible in rendered md. + +### 2. Cross-references section (in body) + +```markdown + +``` + +Commented out at generation time. After submitting +to multiple services, uncomment and the ref-links +resolve via the stubs at the bottom. + +### 3. Ref-link stubs (bottom of file) + +```markdown + +``` + +Commented out with `___` number placeholders. +After submission: uncomment, replace `___` with +the actual number. Each service-specific copy +fills in all services' numbers so any copy can +cross-reference the others. + +### Post-submission file layout + +``` +pr_msg_LATEST.md # latest draft (skill root) +msgs/ + 20260325T002027Z_mybranch_pr_msg.md # timestamped + github/ + 42_pr_msg.md # github PR #42 + gitea/ + 17_pr_msg.md # gitea PR #17 + srht/ + 5_pr_msg.md # srht patch #5 +``` + +Each `/_pr_msg.md` is a copy with: +- metadata `submitted:` fields filled in +- cross-references section uncommented +- ref-link stubs uncommented with real numbers +- all services cross-linked in each copy + +This mirrors the `gish` skill's +`/.md` pattern. + +## Commit-Link URL Patterns by Service + +| Service | Pattern | +|-----------|-------------------------------------| +| GitHub | `https://github.com///commit/` | +| Gitea | `https://///commit/` | +| SourceHut | `https://git.sr.ht/~//commit/` | +| GitLab | `https://gitlab.com///-/commit/` | + +## PR/Patch URL Patterns by Service + +| Service | Pattern | +|-----------|-------------------------------------| +| GitHub | `https://github.com///pull/` | +| Gitea | `https://///pulls/` | +| SourceHut | `https://git.sr.ht/~//patches/` | +| GitLab | `https://gitlab.com///-/merge_requests/` | + +## Scope Naming Convention + +Use Python namespace-resolution syntax for +referencing changed code scopes: + +| File path | Scope reference | +|---------------------------|-------------------------------| +| `tractor/_exceptions.py` | `tractor._exceptions` | +| `tractor/_state.py` | `tractor._state` | +| `tests/test_foo.py` | `tests.test_foo` | +| Function in module | `tractor._exceptions.func()` | +| Method on class | `.RemoteActorError.src_type` | +| Class | `tractor._exceptions.RAE` | + +Prefix with the package path for top-level refs; +use leading-dot shorthand (`.ClassName.method()`) +for sub-bullets where the parent module is already +established. + +## Title Conventions + +Same verb vocabulary as commit messages: +- `Add` — wholly new feature/API +- `Fix` — bug fix +- `Drop` — removal +- `Use` — adopt new approach +- `Move`/`Mv` — relocate code +- `Adjust` — minor tweak +- `Update` — enhance existing feature +- `Support` — add support for something + +Target 50 chars, hard max 70. Always backtick +code elements. + +## Tone + +Casual yet technically precise — matching the +project's commit-msg style. Terse but every bullet +carries signal. Use project abbreviations freely +(msg, bg, ctx, impl, mod, obvi, fn, bc, var, +prolly, ep, etc.). + +--- + +(this format reference was generated by +[`claude-code`][claude-code-gh]) +[claude-code-gh]: https://github.com/anthropics/claude-code diff --git a/.claude/skills/run-tests/SKILL.md b/.claude/skills/run-tests/SKILL.md new file mode 100644 index 00000000..8e36e176 --- /dev/null +++ b/.claude/skills/run-tests/SKILL.md @@ -0,0 +1,244 @@ +--- +name: run-tests +description: > + Run tractor test suite (or subsets). Use when the user wants + to run tests, verify changes, or check for regressions. +argument-hint: "[test-path-or-pattern] [--opts]" +allowed-tools: + - Bash(python -m pytest *) + - Bash(python -c *) + - Bash(ls *) + - Bash(cat *) + - Read + - Grep + - Glob + - Task +--- + +Run the `tractor` test suite using `pytest`. Follow this +process: + +## 1. Parse user intent + +From the user's message and any arguments, determine: + +- **scope**: full suite, specific file(s), specific + test(s), or a keyword pattern (`-k`). +- **transport**: which IPC transport protocol to test + against (default: `tcp`, also: `uds`). +- **options**: any extra pytest flags the user wants + (e.g. `--ll debug`, `--tpdb`, `-x`, `-v`). + +If the user provides a bare path or pattern as argument, +treat it as the test target. Examples: + +- `/run-tests` → full suite +- `/run-tests test_local.py` → single file +- `/run-tests test_discovery -v` → file + verbose +- `/run-tests -k cancel` → keyword filter +- `/run-tests tests/ipc/ --tpt-proto uds` → subdir + UDS + +## 2. Construct the pytest command + +Base command: +``` +python -m pytest +``` + +### Default flags (always include unless user overrides): +- `-x` (stop on first failure) +- `--tb=short` (concise tracebacks) +- `--no-header` (reduce noise) + +### Path resolution: +- If the user gives a bare filename like `test_local.py`, + resolve it under `tests/`. +- If the user gives a subdirectory like `ipc/`, resolve + under `tests/ipc/`. +- Glob if needed: `tests/**/test_**.py` + +### Key pytest options for this project: + +| Flag | Purpose | +|---|---| +| `--ll ` | Set tractor log level (e.g. `debug`, `info`, `runtime`) | +| `--tpdb` / `--debug-mode` | Enable tractor's multi-proc debugger | +| `--tpt-proto ` | IPC transport: `tcp` (default) or `uds` | +| `--spawn-backend ` | Spawn method: `trio` (default), `mp_spawn`, `mp_forkserver` | +| `-k ` | pytest keyword filter | +| `-v` / `-vv` | Verbosity | +| `-s` | No output capture (useful with `--tpdb`) | + +### Common combos: +```sh +# quick smoke test of core modules +python -m pytest tests/test_local.py tests/test_rpc.py -x --tb=short --no-header + +# full suite, stop on first failure +python -m pytest tests/ -x --tb=short --no-header + +# specific test with debug +python -m pytest tests/test_discovery.py::test_reg_then_unreg -x -s --tpdb --ll debug + +# run with UDS transport +python -m pytest tests/ -x --tb=short --no-header --tpt-proto uds + +# keyword filter +python -m pytest tests/ -x --tb=short --no-header -k "cancel and not slow" +``` + +## 3. Pre-flight checks (before running tests) + +### Worktree venv detection + +If running inside a git worktree (`git rev-parse +--git-common-dir` differs from `--git-dir`), verify +the Python being used is from the **worktree's own +venv**, not the main repo's. Check: + +```sh +python -c "import tractor; print(tractor.__file__)" +``` + +If the path points outside the worktree (e.g. to +the main repo), set up a local venv first: + +```sh +UV_PROJECT_ENVIRONMENT=py uv sync +``` + +where `` matches the active cpython minor +version (detect via `python --version`, e.g. +`py313` for 3.13, `py314` for 3.14). Then use +`py/bin/python` for all subsequent commands. + +**Why this matters**: without a worktree-local venv, +subprocesses spawned by tractor resolve modules from +the main repo's editable install, causing spurious +`AttributeError` / `ModuleNotFoundError` for code +that only exists on the worktree's branch. + +### Import + collection checks + +Always run these, especially after refactors or +module moves — they catch import errors instantly: + +```sh +# 1. package import smoke check +python -c 'import tractor; print(tractor)' + +# 2. verify all tests collect (no import errors) +python -m pytest tests/ -x -q --co 2>&1 | tail -5 +``` + +If either fails, fix the import error before running +any actual tests. + +## 4. Run and report + +- Run the constructed command. +- Use a timeout of **600000ms** (10min) for full suite + runs, **120000ms** (2min) for single-file runs. +- If the suite is large (full `tests/`), consider running + in the background and checking output when done. +- Use `--lf` (last-failed) to re-run only previously + failing tests when iterating on a fix. + +### On failure: +- Show the failing test name(s) and short traceback. +- If the failure looks related to recent changes, point + out the likely cause and suggest a fix. +- **Check the known-flaky list** (section 8) before + investigating — don't waste time on pre-existing + timeout issues. +- **NEVER auto-commit fixes.** If you apply a code fix + during test iteration, leave it unstaged. Tell the + user what changed and suggest they review the + worktree state, stage files manually, and use + `/commit-msg` (inline or in a separate session) to + generate the commit message. The human drives all + `git add` and `git commit` operations. + +### On success: +- Report the pass/fail/skip counts concisely. + +## 5. Test directory layout (reference) + +``` +tests/ +├── conftest.py # root fixtures, daemon, signals +├── devx/ # debugger/tooling tests +├── ipc/ # transport protocol tests +├── msg/ # messaging layer tests +├── test_local.py # registrar + local actor basics +├── test_discovery.py # registry/discovery protocol +├── test_rpc.py # RPC error handling +├── test_spawning.py # subprocess spawning +├── test_multi_program.py # multi-process tree tests +├── test_cancellation.py # cancellation semantics +├── test_context_stream_semantics.py # ctx streaming +├── test_inter_peer_cancellation.py # peer cancel +├── test_infected_asyncio.py # trio-in-asyncio +└── ... +``` + +## 6. Change-type → test mapping + +After modifying specific modules, run the corresponding +test subset first for fast feedback: + +| Changed module(s) | Run these tests first | +|---|---| +| `runtime/_runtime.py`, `runtime/_state.py` | `test_local.py test_rpc.py test_spawning.py test_root_runtime.py` | +| `discovery/` (`_registry`, `_discovery`, `_addr`) | `test_discovery.py test_multi_program.py test_local.py` | +| `_context.py`, `_streaming.py` | `test_context_stream_semantics.py test_advanced_streaming.py` | +| `ipc/` (`_chan`, `_server`, `_transport`) | `tests/ipc/ test_2way.py` | +| `runtime/_portal.py`, `runtime/_rpc.py` | `test_rpc.py test_cancellation.py` | +| `spawn/` (`_spawn`, `_entry`) | `test_spawning.py test_multi_program.py` | +| `devx/debug/` | `tests/devx/test_debugger.py` (slow!) | +| `to_asyncio.py` | `test_infected_asyncio.py test_root_infect_asyncio.py` | +| `msg/` | `tests/msg/` | +| `_exceptions.py` | `test_remote_exc_relay.py test_inter_peer_cancellation.py` | +| `runtime/_supervise.py` | `test_cancellation.py test_spawning.py` | + +## 7. Quick-check shortcuts + +### After refactors (fastest first-pass): +```sh +# import + collect check +python -c 'import tractor' && python -m pytest tests/ -x -q --co 2>&1 | tail -3 + +# core subset (~10s) +python -m pytest tests/test_local.py tests/test_rpc.py tests/test_spawning.py tests/test_discovery.py -x --tb=short --no-header +``` + +### Re-run last failures only: +```sh +python -m pytest --lf -x --tb=short --no-header +``` + +### Full suite in background: +When core tests pass and you want full coverage while +continuing other work, run in background: +```sh +python -m pytest tests/ -x --tb=short --no-header -q +``` +(use `run_in_background=true` on the Bash tool) + +## 8. Known flaky tests + +These tests have **pre-existing** timing/environment +sensitivity. If they fail with `TooSlowError` or +pexpect `TIMEOUT`, they are almost certainly NOT caused +by your changes — note them and move on. + +| Test | Typical error | Notes | +|---|---|---| +| `devx/test_debugger.py::test_multi_nested_subactors_error_through_nurseries` | pexpect TIMEOUT | Debugger pexpect timing | +| `test_cancellation.py::test_cancel_via_SIGINT_other_task` | TooSlowError | Signal handling race | +| `test_inter_peer_cancellation.py::test_peer_spawns_and_cancels_service_subactor` | TooSlowError | Async timing (both param variants) | +| `test_docs_examples.py::test_example[we_are_processes.py]` | `assert None == 0` | `__main__` missing `__file__` in subproc | + +**Rule of thumb**: if a test fails with `TooSlowError`, +`trio.TooSlowError`, or `pexpect.TIMEOUT` and you didn't +touch the relevant code path, it's flaky — skip it. diff --git a/.gitignore b/.gitignore index 1d5e994d..642de7a2 100644 --- a/.gitignore +++ b/.gitignore @@ -107,9 +107,23 @@ venv.bak/ .git/ # any commit-msg gen tmp files +.claude/skills/commit-msg/msgs/ +.claude/git_commit_msg_LATEST.md .claude/*_commit_*.md .claude/*_commit*.toml .claude/*_commit*.txt +.claude/skills/commit-msg/msgs/* + +.claude/skills/pr-msg/msgs/* +# XXX, for rn, so i can telescope this file. +!/.claude/skills/pr-msg/pr_msg_LATEST.md + +# review-skill ephemeral ctx (per-PR, single-use) +.claude/review_context.md +.claude/review_regression.md + +# per-skill session/conf (machine-local) +.claude/skills/*/conf.toml # nix develop --profile .nixdev .nixdev* @@ -129,3 +143,7 @@ gh/ # LLM conversations that should remain private docs/conversations/ + +# Claude worktrees +.claude/wkts/ +claude_wkts