{"id":"dc1c7524-cb33-4635-86b3-2fcbfbd2e9f9","slug":"grad","kind":"skill","name":"Grad","summary":"A research harness for ML, built on the Claude Agent SDK. Claude Code with a literature funnel, a prediction ledger, and gates in front of every dollar.","owner":"view321","repoUrl":"https://github.com/view321/Grad","license":"MIT","licenseTier":"permissive","trust":"community","stars":7,"match":null,"fewStars":false,"format":"skill","createdAt":"2026-08-13T19:37:38.000Z","addedAt":"2026-10-22T03:25:13.621Z","logoUrl":"https://avatars.githubusercontent.com/u/57069886?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["developer-tools"],"tags":[],"runtime":"python","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-25T16:14:18.223Z","sample":false,"url":"https://conduits.dev/tools/grad","language":"Python","transports":["stdio"],"install":{"pypi":"grad"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"<h1 align=\"center\">Grad</h1>\n\n<p align=\"center\"><b>A research harness for machine learning, built on the Claude Agent SDK.</b></p>\n\n<p align=\"center\">\n  <img alt=\"status: in development\" src=\"https://img.shields.io/badge/status-in%20development-FFD400?style=flat-square&labelColor=14100C\">\n  <img alt=\"python 3.11+\" src=\"https://img.shields.io/badge/python-3.11+-12A594?style=flat-square&labelColor=14100C\">\n  <img alt=\"built on the Claude Agent SDK\" src=\"https://img.shields.io/badge/built%20on-Claude%20Agent%20SDK-B04A2C?style=flat-square&labelColor=14100C\">\n  <img alt=\"licence: MIT\" src=\"https://img.shields.io/badge/licence-MIT-EFE8D8?style=flat-square&labelColor=14100C\">\n</p>\n\n<p align=\"center\">\n  <img src=\"docs/screenshots/workspace.png\" alt=\"The Grad workspace: agent chat, the task list, and the retrieval funnel side by side\" width=\"900\">\n</p>\n\nGrad is Claude Code pointed at research work, with the rest of a research\nproject built around it. The loop is the one you already use — one model, six\ntools, files on disk. What is added is the apparatus a paper needs and a chat\nwindow does not: a literature funnel, a ledger of predictions and outcomes,\nmechanical guards in front of anything that spends money, submitters for GPU\nbackends, and the artifacts at the end.\n\nIt is for people who already work with Claude and would rather not keep the rest\nof the work in eleven browser tabs and a notes app.\n\n> Grad runs on a **Claude subscription** (Pro or Max) and wants a **Voyage AI\n> key** for reranking. Both are covered under [What you need](#what-you-need).\n> It is **in active development** — see [Status](#status) before you rely on it.\n\n---\n\n## What it is, precisely\n\n`agent.py` opens a `ClaudeSDKClient` with a small system prompt and six tools —\n`Read`, `Write`, `Edit`, `Bash`, `Glob`, `Grep`. Everything Grad adds is a\ncommand-line tool the model reaches over Bash: `--json` on every subcommand, a\nstable envelope, and errors that carry the literal next command to run.\n\nThat is the whole architectural idea. **Capability is a CLI, not a framework.**\nA tool that is a program can be run by the agent, by you, by a test, and by a\nbackground task runner, and it lands in the same ledger every time.\n\nThe absences are deliberate too. `Task` is denied, because a subagent is a model\ncall the main loop never issues and therefore never meters — and metering is the\npoint. `WebSearch` and `WebFetch` are denied as well: literature comes through\nthe funnel, where it is ranked, traced and citable, rather than pasted in from a\nsearch result.\n\n### What surrounds the loop\n\n- **Papers in.** A five-stage retrieval funnel — expand → retrieve → rerank →\n  triage → select — over Papers with Code and your own local index. arXiv LaTeX\n  source is ingested into section-aware chunks; the index is SQLite FTS5 plus\n  vectors, fused by reciprocal rank. Every stage writes a trace, so a search you\n  disagree with can be inspected rather than re-run.\n- **A gate in front of every dollar.** Nothing reaches paid hardware without a\n  passing preflight for that exact submission, a pre-registered prediction, room\n  under the project's ceiling, and no stale uncollected run. These are programs\n  that refuse, not sentences in a prompt.\n- **Compute where you have it.** The same verbs — `submit`, `status`, `collect`\n  — against Modal (T4 through B200, billed by the second), Hugging Face Jobs, any\n  SSH host you can reach, and Kaggle's free GPU/TPU, where an hours-based quota\n  is enforced because a dollar ceiling measures nothing.\n- **Evidence that outlives the session.** An append-only ledger of expectations\n  and results. Replicated runs are compared interval against interval, so\n  \"matches the prediction\" is a statement with a spread behind it.\n- **Papers out.** `report draft → write → cite → check → build` produces a LaTeX\n  paper whose every number traces to a run record and whose every citation\n  resolves to a real document. `check` refuses on a claim you have not judged.\n- **A wiki for what got built.** Generated pipeline code, explained — half\n  extracted from the source, half written.\n- **One workspace.** A tiling desktop app: agent chat, JupyterLab, the ledger,\n  the funnel, the queue, quota, preflight, the paper editor.\n\n---\n\n## The workspace\n\n<table>\n<tr>\n<td width=\"50%\"><img src=\"docs/screenshots/ledger.png\" alt=\"The ledger window\"><br><sub><b>Ledger</b> — each prediction registered before the run, its outcome drawn against the predicted band, and the comparability note saying what would make the two incomparable.</sub></td>\n<td width=\"50%\"><img src=\"docs/screenshots/quota.png\" alt=\"The quota window\"><br><sub><b>Quota</b> — all four kinds of token, weighted into the one number the ceiling sees, with a standing note that this is a self-measured tally rather than a fuel gauge.</sub></td>\n</tr>\n</table>\n\n<p align=\"center\">\n  <img src=\"docs/screenshots/funnel.png\" alt=\"The retrieval funnel window\" width=\"820\">\n</p>\n\n<p align=\"center\"><sub><b>Funnel</b> — 300 candidates to 50 to 15, the expanded queries that found them, and a warning when the survivors were ranked on titles alone.</sub></p>\n\nThe design is deliberate: cream paper, 2px ink rules, monospace for anything the\nmachine produced, and exactly one accent colour per state — yellow when it needs\nyou, teal when it passed, red when it broke. Windows tile, split and swap; the\narrangement persists.\n\n---\n\n## What you need\n\n### A Claude subscription\n\nGrad runs on your Claude subscription, not on the Developer Platform. Pro, Max,\nTeam or Enterprise will all authenticate; **Max is what it was built and\nbudgeted against**, and Pro will run out sooner than you expect on a long\nsession.\n\n```bash\nnpm install -g @anthropic-ai/claude-code   # the SDK spawns this binary\nclaude setup-token                          # mints the OAuth token\n```\n\n`ANTHROPIC_API_KEY` is removed from the process environment at startup, on\npurpose: it outranks the OAuth token in the credential chain, and a stray export\nwould silently bill the API instead of the subscription. **Running Grad on API\ncredits means editing the code.** `python agent.py --check` reports which\ncredential it is using and what it removed.\n\n### A Voyage AI key\n\nRetrieval's second stage is a dedicated cross-encoder reranker (`rerank-2.5`),\nand the local index is embedded with `voyage-4`. Both are Voyage, both are billed\nin credits rather than subscription quota, and both are metered in the same\nledger as everything else.\n\nWithout a key, `paper_search --no-rerank` and `paper_ingest --no-vectors` still\nrun and retrieval falls back to keyword ranking. That is a real drop in quality,\nnot a shrug — the reranker is what turns 300 plausible titles into 15 papers\nworth reading. An OpenRouter key can carry the rerank stage instead; embeddings\nhave no second rail.\n\n### Optional, one per backend\n\n| Credential | Buys you |\n|---|---|\n| `hf_token` | Hugging Face Jobs |\n| `kaggle_key` + account | free GPU/TPU, rationed by the hour |\n| `modal_token_id` + `modal_token_secret` | Modal sandboxes: T4 through B200, by the second |\n| SSH host or key | your own box |\n| `openrouter_key` | a second rail for reranking |\n| `context7_key`, `asta_api_key` | higher rate limits on docs and discovery |\n\nCredentials go to the OS credential store, never into a config file and never\ninto the agent's environment. The tools that need them shell out, so the secret\nenters one child process at the moment of use.\n\n**On a headless host there is no credential store**, which is a normal situation\nrather than an exotic one — a GPU box reached over SSH has no D-Bus session, so\n`keyring` has no Secret Service to talk to and every read fails. The way through\nis to say so explicitly and pass the secrets in the environment instead:\n\n```bash\nexport GRAD_ALLOW_ENV_CREDENTIALS=1\nexport GRAD_CLAUDE_OAUTH_TOKEN=...        # GRAD_ + the credential's name, upper-cased\nexport GRAD_VOYAGE_KEY=...\n```\n\nIt is off by default because §9's whole argument is that a token in the\nenvironment is a token the agent can read. Turning it on is a deliberate trade\nfor a machine where the alternative is not authenticating at all. Optional\ncredentials need none of this: an unreachable store reads as an absent key, so\nanonymous retrieval still runs.\n\n---\n\n## Install\n\n**Windows** — the full desktop app, a Start Menu shortcut, no console window:\n\n```powershell\nirm https://raw.githubusercontent.com/view321/Grad/main/install.ps1 | iex\n```\n\n**Anything else** — the CLI and the browser UI:\n\n```bash\ngit clone https://github.com/view321/Grad && cd Grad && ./install.sh\n```\n\nOr by hand, if you would rather see what happens:\n\n```bash\npip install -e \".[agent,notebook,retrieval,remote,ui,math]\"\n```\n\nThen store the credentials, either from the app's **setup** window or from a\nterminal:\n\n```bash\npython -m tools.jobs credential set claude_oauth_token\npython -m tools.jobs credential set voyage_key\n```\n\nThe installer asks for a **workspace** folder separate from the checkout, and\nthat separation is worth accepting. The installation is code and is replaced by\n`grad --update`; the workspace is your ledger, notebooks, notes and figures, and\nnothing ever touches it. Keeping them together makes every update a merge whose\nconflicts land in an append-only ledger.\n\n---\n\n## A session, end to end\n\n```bash\npython -m tools.budget new --id proj-scaling --title \"width vs depth\" \\\n    --gpu-usd 50 --quota-tokens 5e6 --credits-usd 10 --use --json\npython -m tools.preflight run --spec pipeline/spec.toml --json\npython -m tools.ledger expect --task scaling --quantity val_loss@1e9_tokens \\\n    --low 2.9 --high 3.2 --basis 'arXiv:2001.08361|Fig 3|3.05|1.3B params' --json\npython -m tools.kaggle submit --spec pipeline/spec.toml --expect exp-... --json\npython -m tools.kaggle collect run-... --json\npython -m tools.ledger verdict run-... --quantity val_loss@1e9_tokens \\\n    --verdict bug --note 'lr schedule off by one step' --json\npython -m tools.report check --project proj-scaling --json\n```\n\nSkip the preflight or the prediction and `submit` refuses, naming the command you\nskipped. Skip the verdict and `report check` refuses. In practice you do not type\nany of this — the agent does, and you read what came back.\n\n---\n\n## What it refuses to do\n\n> **Anything that spends money, destroys work, or must be true before the fact is\n> enforced mechanically, not by prompt.**\n\nThe model is trusted to do research. It is not trusted to remember its own safety\nrails at two in the morning with a deadline.\n\n| Thing that must hold | Enforced by |\n|---|---|\n| Code passes QA before it costs money | the preflight record, keyed by a hash of the exact submission |\n| Code runs on the *remote* before a full run | a capped smoke job, folded into that record |\n| A prediction exists before the result does | the expectation gate, bound at submit time |\n| Results get recorded at all | a stale uncollected run blocks every later submission |\n| Cumulative spend stays bounded | actuals for collected runs, estimates for runs in flight |\n| A free backend's rationed hours stay bounded too | the same fold over accelerator hours |\n| Token spend is bounded, not merely measured | checked before every turn, over all four kinds of token |\n| Every number in a report traces to a run record | `report check`, which refuses on an unresolved claim |\n| Every citation is a real paper | resolved against the corpus, never against the model's memory |\n| A result nobody judged cannot be published | `report check` again |\n\nA usage error, a gate refusal and an upstream failure are three different exit\ncodes, because the agent should not have to read prose to tell them apart. The\nfull table, and the reasoning behind every row above, is in\n[`CLAUDE_README.md`](CLAUDE_README.md).\n\n---\n\n## The tools\n\nEach is a CLI with `--json` on every subcommand.\n\n| CLI | What it does |\n|---|---|\n| `paper_search` | the five-stage retrieval funnel |\n| `paper_ingest` | arXiv LaTeX → section-aware chunks → the local index |\n| `nb` | persistent Jupyter kernel: `exec`, `verify`, `restart` |\n| `preflight` | the QA gate, and the record the submitters read |\n| `jobs` / `gpu` / `kaggle` / `modal` | the same verbs against four backends |\n| `ledger` | `expect`, `query`, `verdict`, `falsify`, `abandon`, `verify` |\n| `quota` / `budget` | what was spent, and what may still be |\n| `evolve` | evolutionary search as a budgeted campaign |\n| `task` / `wakeup` | run something long without holding the turn |\n| `report` | `draft`, `write`, `cite`, `check`, `build` |\n| `project` / `experiments` | what this project knows; every run ever, across workspaces |\n| `docs` | is this library call current? introspection first, then Context7 |\n| `wiki` / `projwiki` / `lab` / `traces` | human-facing surfaces |\n\n---\n\n## Status\n\n**Grad is in development, and the honest summary is that the machinery is solid\nand the mileage is thin.** It has one author, one machine, and no users but its\nauthor. Treat it as something to run and read rather than something to depend on.\n\nWhat has run against live services, measured from the ledger it keeps on itself:\n\n- The agent loop, the retrieval funnel end to end (expansion, retrieval,\n  reranking and triage), Voyage embeddings, and wiki generation.\n- **Kaggle**: fifteen real runs submitted and collected, three of them carried\n  through to a recorded verdict against their prediction.\n\nWhat is implemented, tested and **not yet exercised on live hardware**: the\nHugging Face Jobs and SSH submitters, the evolutionary campaign loop, and the\nreport generator's model-driven half. They share the gates, the records and the\nshapes of the paths that have run, and they fail with actionable errors rather\nthan tracebacks — but a real credential and a real run are what find the\nmismatches.\n\nThe test suite runs offline; the network is stubbed by a fixture, because a suite\nthat reaches the network does not fail, it hangs. The gate tests run against a\nreal ledger in a temporary workspace, since a mock of a gate proves nothing about\nthe gate.\n\n```bash\npython -m pytest -q\n```\n\nAlongside the example-based tests, `tests/property/` generates inputs and checks\nrules rather than outputs — a mean lies between the extremes it was taken over, a\nrolling spend never falls when a run is submitted, and if the shell would run\n`ssh` then the deny list says so. That last one found three bypasses on its first\nrun, including `( ssh gpu-box nvidia-smi )`: three tokens, no quoting, and the\nshortest hole the hook ever had. Mutation testing (`mutmut`) is configured for\nthe same modules and run by hand rather than in CI. Both are described in\n[`docs/testing.md`](docs/testing.md).\n\nThree things to know before trusting it with a budget:\n\n- **Interfaces are not stable.** Ledger fields, exit codes and CLI flags still\n  move between releases. There is no deprecation policy yet.\n- **The Agent SDK surface is version-sensitive.** Permission-mode names and\n  semantics have changed between releases, so run `python agent.py --probe` after\n  every SDK upgrade: it attempts a call that should be denied and reports whether\n  it was actually denied.\n- **Windows is the first-class target.** The native window and the\n  notification-area icon are Windows features. Linux and macOS get the CLI and\n  the browser UI, and everything that matters — the ledger, the gates, the\n  notebooks, the submitters — works there. The credential store is `keyring` and\n  is not Windows-only, but a headless host has no backend for it; see\n  [What you need](#optional-one-per-backend) for the environment route.\n  Linux is covered by CI now, which is how the first three Linux-only defects\n  were found — before that it had never been run there at all.\n\n### If you run it\n\nThe most useful thing anyone can send right now is **where they stopped** —\n[`I ran it — here is how far I got`](https://github.com/view321/Grad/issues/new?template=ran_it.yml).\nNothing has to have broken. \"I read this far and did not install it\" is a finding\nabout this page. A project with one author and one machine cannot find the things\nthat only a second machine has.\n\nSomething actually broken goes to the\n[bug report form](https://github.com/view321/Grad/issues/new?template=bug_report.yml),\nwhich asks for `grad --check` — a report of booleans and variable names that\ncarries no credentials. [`CONTRIBUTING.md`](CONTRIBUTING.md) has the rest.\n\n---\n\n## Documentation\n\n| | |\n|---|---|\n| [`CLAUDE_README.md`](CLAUDE_README.md) | the engineering record: every gate, every decision, and what each one cost to learn |\n| [`HANDOFF.md`](HANDOFF.md) · [`HANDOFF-2.md`](HANDOFF-2.md) | the design documents of record |\n| [`prompts/system.md`](prompts/system.md) | the entire system prompt — read it, edit it |\n| [`skills/`](skills) | workflows loaded on demand, not into the default context |\n| [`CONTRIBUTING.md`](CONTRIBUTING.md) | how to report what broke, and what the CI checks before a change lands |\n\n---\n\n## Licence\n\nMIT — see [`LICENSE`](LICENSE).\n"}