{"repo": "atelier-fashion/adlc-toolkit", "files": {"tools/delegate/README.md": {"f067b1673dc91104fcc3a617a31128ed9e42fdee": {"t": 1786068560.6214468, "n": 12965, "h": "89d159e2e8e089e9", "c": "# Delegation CLIs (provider-agnostic)\n\nSmall command-line tools that let a Claude Code session delegate token-heavy\nI/O work — bulk file reading, boilerplate generation, documentation diffs — to a\nconfigured *delegate* model behind any OpenAI-compatible chat-completions\nendpoint. The point is to keep Claude's context window focused on reasoning\nwhile a cheap model does the mechanical reading and writing. Introduced by\nREQ-412; made provider-agnostic by REQ-515.\n\nThe shipped defaults point at [Kimi K2.5](https://platform.moonshot.ai/)\n(Moonshot AI), so an existing setup keeps working with zero changes. A new\nadopter can point the same tools at any OpenAI-compatible provider (Ollama,\nGroq, DeepSeek, an Anthropic OpenAI-compat key, etc.) via a config file or env\nvars — see [Configuration](#configuration).\n\nCommands:\n\n- `adlc-read` — read one or more files and answer a question about them, returning a summary.\n- `adlc-write` — generate boilerplate (tests, config, docstrings, repetitive patterns) to a target file.\n- `extract-chat` — flatten a Claude Code session `.jsonl` transcript into plain text (feeds `adlc-read`).\n\n> **Privacy & data governance:** `adlc-read` and `adlc-write` send file contents\n> to the configured third-party endpoint. **Whether that transmission is\n> permitted is the adopter's responsibility** — confirm your company's\n> data-handling policy before enabling delegation. Only the basename of each\n> path is included in the request — full filesystem paths stay local. Every real\n> run prints a one-line stderr notice; silence it with `--no-warn` or\n> `ADLC_DELEGATE_NO_WARN=1`. `extract-chat` is purely local and makes no API calls.\n\n## Configuration\n\nA \"provider\" is three values: a **base URL**, a **model name**, and the **name\nof an env var** holding the API key (the key value itself is never stored in any\nfile). They are resolved by the following precedence (highest first):\n\n| Precedence | Source | Keys |\n|-----------:|--------|------|\n| 1 | CLI flags | `--model`, `--base-url` |\n| 2 | `ADLC_DELEGATE_*` env | `ADLC_DELEGATE_MODEL`, `ADLC_DELEGATE_BASE_URL`, `ADLC_DELEGATE_API_KEY_ENV` |\n| 3 | config file | `delegate.base_url`, `delegate.model`, `delegate.api_key_env` |\n| 4 | legacy key-env continuity | `MOONSHOT_API_KEY` / `KIMI_API_KEY` |\n| 5 | shipped defaults | `https://api.moonshot.ai/v1`, `kimi-k2.5`, `MOONSHOT_API_KEY` |\n\n### Opt-in (delegation is OFF by default)\n\nOn a fresh install delegation transmits nothing until you explicitly opt in.\nOpt-in is satisfied by **any one** of:\n\n- `enabled: true` under `delegate:` in the config file, OR\n- `ADLC_DELEGATE_ENABLED=1` in the environment, OR\n- an already-set legacy `KIMI_API_KEY` / `MOONSHOT_API_KEY` (key continuity for\n today's installs — these stay enabled exactly as before).\n\nSetting only `ADLC_DELEGATE_BASE_URL` / `_MODEL` is **not** opt-in. Force\ndelegation off entirely with `ADLC_DISABLE_DELEGATE=1`.\n\n### Config file\n\nDefault location `~/.claude/adlc/config.yml` (override with `ADLC_CONFIG`):\n\n```yaml\ndelegate:\n enabled: true # opt-in; absent/false => disabled\n base_url: \"https://api.groq.com/openai/v1\"\n model: \"llama-3.3-70b-versatile\"\n api_key_env: \"GROQ_API_KEY\" # the NAME of an env var, never the key\n```\n\n`api_key_env` must be the **name** of an environment variable, in\n`UPPER_SNAKE_CASE`. The value the whole cascade resolves to is validated —\nincluding an `ADLC_DELEGATE_API_KEY_ENV` override, which outranks the file — so\na key pasted into either place is refused with an actionable error before any\nnetwork call. The refusal never echoes the offending value.\n\n## Setup\n\n1. **Get an API key** for your provider (the default is a Moonshot key from\n → Console → API Keys).\n2. **Run the installer:**\n ```bash\n bash tools/delegate/install.sh\n ```\n This creates a Python venv at `~/.claude/delegate-venv`, installs the `openai`\n client into it, writes wrapper scripts to `~/bin/` (for `adlc-read`,\n `adlc-write`, `extract-chat`), adds\n `~/bin` to your `PATH`, appends the routing block to `~/.claude/CLAUDE.md`,\n and adds the commands to the allowlist in `~/.claude/settings.json`. It is\n idempotent — safe to re-run.\n3. **Set your API key** in your shell rc (the installer does not write it):\n ```bash\n export MOONSHOT_API_KEY=\"sk-...\" # or your provider's key var\n ```\n4. **Restart your shell** (or `source ~/.zshrc`) so `PATH` and the env var take effect.\n\n## Usage\n\n```bash\n# Ask a question across one or more files\nadlc-read --paths src/foo.py src/bar.py --question \"How does foo call bar? Summarize the data flow.\"\n\n# Override the provider per-invocation\nadlc-read --paths notes.md --question \"summarize\" --model some-model --base-url https://host/v1\n\n# Generate boilerplate to a file\nadlc-write --spec \"pytest tests for the parse_args function\" --context src/cli.py --target tests/test_cli.py\nadlc-write --spec \"...\" --context ref.py --target out.py --force # overwrite an existing target\n\n# Flatten a session transcript to plain text\nextract-chat ~/.claude/projects//.jsonl -o /tmp/chat.txt\n```\n\n### Version & resolved provider (`--version`)\n\nAll three CLIs accept `--version` (or `-V`). It is scanned out of the arguments\n*before* parsing, so it needs no other arguments (`adlc-write --version` works\nwithout `--spec`/`--target`), and it runs before every guard — no network call,\nno API key, no config file, and no `openai` SDK required. The scan is\nvalue-aware: it only matches `--version`/`-V` in *flag* position, so\n`adlc-read --question -V` asks about the string `-V` (and gets argparse's own\nerror) rather than silently printing the version, and it stops at `--`.\n\n`adlc-read` and `adlc-write` also print the provider a **real call would\nresolve**, through the same resolver and the same precedence table as above — so\nit answers \"which endpoint is this install actually talking to?\" without reading\nthe config file, the environment, and `_common.py` by hand. That includes\nrank 1: `--model` / `--base-url` passed alongside `--version` are reflected in\nthe output, in both the `--model VALUE` and `--model=VALUE` forms. The API key\n**value** is never read or printed; only the *name* of the env var holding it:\n\n```bash\n$ adlc-read --version\nadlc-toolkit \nbase_url: https://api.groq.com/openai/v1\nmodel: llama-3.3-70b-versatile\napi_key_env: GROQ_API_KEY\nenabled: true\n```\n\n`extract-chat` has no provider config, so it prints the version line only:\n\n```bash\n$ extract-chat --version\nadlc-toolkit \n```\n\nThe output is a stable, machine-parseable contract (exit 0, stdout): the first\nline is always `adlc-toolkit ` — the first line of the repo `VERSION`\nfile, resolved from the script's own location, so it reports the toolkit's\nversion and not anything derived from the directory you ran it in (nor, when the\ntoolkit is vendored inside another git repo, that host repo's version) —\nfollowed by exactly the `base_url`, `model`, `api_key_env`, and `enabled`\n(`true`/`false`) lines.\n\nIf the printed `base_url` carries credentials (`https://user:pass@host/v1`), the\nuserinfo is redacted to `***@host` **on the print path only** — the real call\nstill receives the URL intact.\n\nIf provider resolution is **refused** — the resolved `api_key_env` is a key\nvalue rather than an `UPPER_SNAKE_CASE` env var name, whether it came from the\nconfig file or from `ADLC_DELEGATE_API_KEY_ENV` — `--version` never crashes with\na traceback. It prints the version line plus a single diagnostic line in place\nof the config block, and still exits 0. The refused value is never echoed back:\n\n```bash\n$ adlc-read --version\nadlc-toolkit \nconfig_error: config 'delegate.api_key_env' must be the NAME of an environment variable (e.g. MY_PROVIDER_KEY), not a key value. ...\n```\n\nA config file that simply cannot be *parsed* is a different case: the minimal\nreader yields no `delegate:` keys, resolution falls through to the shipped\ndefaults, and `--version` reports those with no `config_error:` line — matching\nwhat a real call would resolve. `config_error:` means refused, not \"unreadable\".\n\n## CLAUDE.md routing block\n\n`install.sh` appends the canonical routing block to `~/.claude/CLAUDE.md`, and\nskips the append if the `delegate-routing:start` marker (or the legacy\n`kimi-delegation:start` marker from a pre-REQ-522 install) is already present so\nre-running is safe.\n\n\n\nThe block content (including its `` /\n`` HTML-comment markers) is the verbatim contents\nof [`claude-md-routing.txt`](claude-md-routing.txt). To preview what gets\nappended, `cat` that file.\n\n### Updating the Claude routing block\n\nThe routing block is hash-pinned (REQ-426 BR-1 / ADR-1) so a casual edit\nto the canonical file cannot silently change every developer's\n`~/.claude/CLAUDE.md` on the next `install.sh` run. Workflow:\n\n1. Edit `tools/delegate/claude-md-routing.txt` with the new content.\n2. Regenerate the pin (match install.sh's hashing — it hashes the file content\n with trailing newlines collapsed to one):\n\n ```sh\n ROUTING_CONTENT=$(cat tools/delegate/claude-md-routing.txt)\n printf '%s\\n' \"$ROUTING_CONTENT\" | shasum -a 256 | awk '{print $1}' > tools/delegate/claude-md-routing.txt.sha256\n ```\n\n (Use `sha256sum` instead of `shasum -a 256` on Linux — both produce the\n same hex digest.)\n3. Commit both files in the same PR. Reviewers see the diff in both, so a\n stealth edit to the .txt without bumping the .sha256 is impossible to land.\n\n`install.sh` recomputes the hash of the .txt at install time and refuses\nto modify `~/.claude/CLAUDE.md` if the digest does not match the pinned\nvalue. The marker-guarded append (no double-injection) is unchanged.\n\n### Updating dependencies\n\nPython dependencies for the venv are pinned in `tools/delegate/requirements.txt`\nwith exact `==` versions for reproducibility (REQ-416 BR-6/BR-7). `install.sh`\ninstalls strictly from that file — there is no `--upgrade` flag, so re-running\nthe installer will not silently pull a newer `openai` SDK that breaks the CLIs.\n\nTo bump a pinned version:\n\n1. Open a hotfix REQ (the pinned API surface is part of the toolkit contract —\n a bump that changes call shapes is a real change worth tracking).\n2. Edit `tools/delegate/requirements.txt` to the new pin.\n3. Delete `~/.claude/delegate-venv` and re-run `bash tools/delegate/install.sh` on a\n clean state to verify the new pin installs cleanly.\n4. Run the `tools/delegate/tests/` pytest suite against the new venv.\n5. Land the bump with the rest of the hotfix.\n\n### Troubleshooting\n\n- **GUI-launched Claude Code can't see `MOONSHOT_API_KEY`** — usually self-heals via the\n LaunchAgent below, but `adlc-read` also has a last-resort rc-file fallback that reads the\n default Moonshot key directly from `~/.zshrc` (or `~/.bash_profile` / `~/.bashrc`) when the\n env is empty. As long as the export is in one of those files, the default-provider tools\n work regardless of how Claude Code was launched. (Custom provider key vars are expected to\n be set in the environment directly.)\n- **The LaunchAgent** — `install.sh` installs `com.adlc-toolkit.delegate-setenv` that runs at\n every login and re-populates the launchctl session env from your rc. If you change the\n key in `~/.zshrc` mid-session, run `launchctl setenv MOONSHOT_API_KEY \"$MOONSHOT_API_KEY\"`\n once to update the current session (or log out + back in).\n- **bash login shell?** `install.sh` writes the PATH entry to `~/.bash_profile` (not\n `~/.zshrc`) when your login shell is bash. If you previously hand-edited `~/.zshrc`\n and you're on bash, either copy the lines to `~/.bash_profile` or run\n `chsh -s /bin/zsh` and restart Terminal.app for the change to take effect.\n- **Linux** — the venv, CLIs, gate, and telemetry all work on Linux. The macOS-only\n launchctl / LaunchAgent steps are skipped with a notice (not a failure) when `launchctl`\n is absent; set your key var in the environment the usual way.\n- **Inspect the LaunchAgent** — `launchctl list | grep adlc-toolkit` confirms it's\n loaded; `cat ~/Library/Logs/delegate-launchctl-setenv.log` shows what it did at the last\n login (path checked, key length — never the key value itself).\n- **`.in` files** — `tools/delegate/*.in` are install-time templates. `install.sh` copies and\n substitutes (`__HOME__` → your `$HOME`) into the deployed locations. Do not run them in\n place; edit the `.in` source and re-run `install.sh`.\n- **Security trade-off (deliberate)** — once the launchctl session env has the key, ANY\n GUI app the user launches can read it via `launchctl getenv` or its own process env.\n This is the cost of making GUI-launched Claude Code see the key. A compromised\n user-space process can already read `~/.zshrc`; this widens that exposure modestly.\n"}}}}