📦 Packaging: the installable distribution and its command surface¶
Status: reference.
What chitragupta-cli installs, what you type once it is installed, and
which of the three names that look identical is registered anywhere.
Written for anyone installing this without a git checkout, and anyone about to change a command name.
Not covered here: what each command does. CLI.md is the exhaustive per-flag reference and stays so; this file covers the surface and the naming.
The import package is chitragupta, pyproject.toml declares a
[build-system], and poetry build produces
chitragupta_cli-<version>-py3-none-any.whl, which installs the
chitragupta and cg commands. A major or minor release is published
to PyPI; a PATCH release gets a GitHub Release only (see below).
🧭 Table of contents¶
- Three names, one of them registered
- The command surface
- The module form, and why it survives
- What the decision answers
- Why the zip ships too
- What a shipped command name costs
🏷 Three names, one of them registered¶
They read as one name and are three unrelated things. Only the first is globally unique; the other two are directories any distribution may write into.
| Value | Uniqueness enforced by | |
|---|---|---|
| Distribution (the PyPI project) | chitragupta-cli |
PyPI, globally |
Import package (site-packages/) |
chitragupta |
nobody: first writer, then overwrite |
Console scripts (venv bin/) |
chitragupta, cg |
nobody, same as above |
pip install chitragupta does not reach this project. That name
belongs to an unrelated package (chitragupta 0.1.1, "Pytest for your
prompts", uploaded 2026-05-07), whose own wheel declares a chitragupta
console script and a chitragupta import package. chitragupta-cli was
free; the command you type stays chitragupta, because the command name
is not a PyPI name and never was.
1 | |
Published from a tagged major or minor release by Trusted Publishing
(GitHub's OIDC token exchanged for a short-lived PyPI one), so there is
no long-lived API token in repository settings to leak or rotate.
A PATCH release is not published here. PyPI never
accepts a re-upload of a version number, not even a deleted or yanked
one, so .github/workflows/release.yml's publish-pypi job only runs
for a tag ending .0 (X.0.0 or X.Y.0); a PATCH tag still gets a GitHub
Release with the wheel attached, just not a PyPI upload. The publish job
runs after the GitHub Release is created, so a PyPI failure would
leave a complete, downloadable release behind rather than a tag with
nothing attached.
cg is likewise not a second PyPI project. One distribution declares
both executables against one entry point:
1 2 3 | |
The residual collision is accepted on one condition. Both distributions
in one environment overwrite each other's site-packages/chitragupta/ and
bin/chitragupta, and pip does not refuse it. The condition is that
chitragupta doctor
detects the competing distribution and names it. An overwritten command is
survivable; an undetected one running the wrong program under the right
name is the failure class
HOOKS.md exists to prevent.
⌨ The command surface¶
Four layers, the same ones python -m chitragupta.<layer>
exposes, plus commands that only make sense once the code is
installed rather than cloned. Every flag, exit code and subcommand name is
the one that command already has; the console script adds a front door
and redesigns nothing.
📦 The package itself¶
| Command | What it does |
|---|---|
chitragupta init [DIR] [--force] [--dry-run] [--agent NAME] |
Scaffold a project directory: config.toml, .claude/ skills and hooks, papers/, content/, assets/, the prose docs. What the release zip ships today. --agent codex and --agent opencode add that harness's launcher and its own copy of each SKILL.md (.codex/ and .agents/, .opencode/); .claude/ is written for every agent, since it holds the hooks and the only copy of each skill's references and assets; repeat for several, default claude (LLM-AGENTS.md for each agent's setup, HARNESS.md for the design) |
chitragupta doctor |
Probe and report: OS binaries, the enrich extra, torch against the GPU driver, a competing chitragupta distribution, a hook launcher on any harness that cannot start. Exits 0 on findings; an aid, never a gate |
chitragupta install os-deps\|gpu-torch\|enrich |
Run the shipped install_full_pipeline.sh for the stages pip cannot do, or install the enrich extra at the running version. all, dev-deps and python-deps are refused by name with the pip equivalent |
chitragupta figure sync [PATH ...] [--check] |
Stamp the house figure-style block into each figure file that lacks it and refresh a stale one, never touching a block edited by hand (TIKZ-STYLE.md). Exits 0 on findings; --check writes nothing and exits 1 if any file is not current; 2 if the installed block cannot be read |
chitragupta figure check <figure.tex> [--page article\|book] [--width PT] [--layout NAME] [--json] |
Report every rule one figure file breaks, by rule id, the deliberate exceptions it records as % cg-allow: lines, and its size beside its layout's reference ranges (figure-checks/README.md). Exits 0 whatever it finds; 2 on a usage error or with no lualatex or tikz.sty |
chitragupta figure new <figures-dir> <name> --layout ID |
Start a figure from a layout's scaffold in assets/tikz/, its % cg-layout: line kept and its house block synced, never overwriting a figure (CLI.md). Exits 0 once written; 2 on a refusal or an unreadable install |
chitragupta figure look <figure.tex> [--draft DRAFT] [--output-dir DIR] |
Set one figure on the page it prints on (a book unit's figure on the book's), cropped to it, as a PDF and a 300 dpi PNG under content/rendered/, or DIR; with --draft, also render the draft and picture the page that holds the figure (CLI.md). Exits 0 whatever it finds; 2 on an unreadable figure, with no lualatex, tikz.sty or pdftoppm (or pandoc, with --draft), or for a figure outside content/drafts/ with no --output-dir |
chitragupta --version |
The installed distribution's version, from importlib.metadata |
📚 corpus -- the deterministic run¶
| Command | Flags |
|---|---|
chitragupta corpus sync |
--reparse, --remove-stale |
chitragupta corpus ledger |
--list, --status, --citekey, --collection, --collections |
chitragupta corpus topics |
--topic |
chitragupta corpus discover |
[PHRASE ...], --paper CITEKEY, --json, --out FILE, --k, --html FILE, --app DIR, --why, --groups, --compare, --hops, --clusters, --inflation, --path, --family, --origins |
✍ draft -- work on one draft¶
| Command | Subcommands / flags |
|---|---|
chitragupta draft gate <file>... |
-- (takes no options; this is the hard gate) |
chitragupta draft references <file> |
--heading |
chitragupta draft evidence <file> |
--format, --output-dir |
chitragupta draft render <file> |
--format, --documentclass, --fontsize, --papersize, --margin, --csl, --output-dir, --fragment, --no-collapse-citations |
chitragupta draft style <draft>... |
--language, --json |
chitragupta draft retrieve |
search, evidence |
chitragupta draft dossier |
init, status, mark-revision, stamp, sections, prune, outline, brief, set-language, acronyms-suggest, check-evidence, list, export, restore |
chitragupta draft spec |
init, show, sign, status, align, seed |
chitragupta draft unit |
contract, accept, status |
chitragupta draft registry |
build, check, excerpt, index |
chitragupta draft tldr |
write, show |
chitragupta draft figures |
(none; takes a citekey) |
🔍 review -- read-only aids, no gate¶
| Command | Subcommands / flags |
|---|---|
chitragupta review provenance <draft> |
--json, --formats |
chitragupta review verbatim |
overlap, scan, recheck, locate |
chitragupta review coverage <draft> |
--query (required, repeatable), --k, --json, --write, --formats |
chitragupta review synthesis <draft> |
--unit, --json, --write, --formats |
chitragupta review figure <draft> |
--json, --write, --formats |
chitragupta review uncited <draft> |
--genre, --json, --write, --formats |
chitragupta review quotation <draft> |
--json, --write, --formats |
chitragupta review agenda <draft> |
--json, --formats, --accept ID, --baseline PATH |
chitragupta review support <draft> |
--json, --write, --formats |
chitragupta review union <book>/book.tex |
--json, --write, --formats |
chitragupta review digest <draft> |
--json, --formats, --baseline PATH |
🧠 enrich -- optional, whole-corpus¶
| Command | Flags |
|---|---|
chitragupta enrich |
--stages docling,embed,bertopic,extract-keywords,seed-topics,converge,topic-graph, --for-draft PATH, --target host\|docker (informational only; the probes decide) |
That is 4 layers and 28 verbs and aids (4 + 12 + 11 + 1), plus 4 package-level commands, giving 63 invocable leaf commands: 7 + 4 + (6 + 31) + (10 + 4) + 1. The counts are stated because a table is easy to extend and easy to forget to extend; a test pins them by walking the live parsers, so a verb added without a row here fails the suite.
One thing is deliberately absent. chitragupta/sync.py carries a
__main__ block, which makes it look like a fifth entry point; it is a
tombstone that refuses with exit 64 and names the current command
(chitragupta corpus sync). This paragraph deliberately does not spell
the refused invocation out:
tests/test_removed_command_scan.py fails any document that hands a
reader a command that does not work, and it decides by path rather
than by reading the surrounding sentence.
Everything below this line is for the maintainers: why the command surface is shaped and named the way it is, and what a shipped name costs to change. If you are installing and using the pipeline, the sections above cover everything you need.
💡 The module form, and why it survives¶
Every row above has an exact equivalent:
1 | |
Both are supported, deliberately, and they are for different callers:
- The console script is for humans.
- The module form is what
.claude/hooks/and the genre skills use, for a functional reason. A console script lives in one venv'sbin/; the module form resolves from any interpreter that can import the package. CLI.md records why tier 1 exists at all (the gate chain must not be blockable by a broken venv), andchitragupta/hook_launchers.pyrecords the measurement behind it: a hook launcher that does not resolve produces nothing at all, no error and no log entry. Routing the citation gate through aPATHlookup that can silently miss is the one change in this whole series that would be worse than not doing it.
Keep both forms; neither is redundancy to tidy away later.
The module form has one cost, and it is paid for in three places. python
-m puts its working directory first on sys.path, and a hook's child
runs from the project root. A checkout relies on that; in an init-ed
project it would let a committed chitragupta/ shadow the installed
package. So the hooks decide which shape a project is before
starting a child and, in an installed-package project, run it with
PYTHONSAFEPATH=1 (HOOKS.md has how, and the one case it
cannot close); chitragupta init refuses to scaffold into a
directory that already holds chitragupta/ or chitragupta.py, even
with --force; and the prose init scaffolds says python -P -m,
which keeps the working directory off sys.path, so a chitragupta/
committed afterwards is not what a scaffolded skill's command runs
either. That is still the module form, resolved from whichever
interpreter runs it, so nothing above changes.
⚖ What the decision answers¶
Making this a published package answered a set of objections. They are written down as objections rather than as a conclusion because three of them survive.
| The objection | Where it was written | What happened to it |
|---|---|---|
Needs renaming the src layout |
pyproject.toml header |
Retired by doing it. A top-level src in site-packages claims the most generic name on the index; it is unshippable at any price |
| "Don't add a second install path" | DEVELOPER-AGENTS.md (git checkout only) |
Survives as an invariant, not as a mechanism. The goal is one place a dependency fact can be written; DEVELOPER-AGENTS.md describes the two front doors onto it |
| Tier 1 must not be blockable by a broken venv | CLI.md | Retired by keeping the module form (see above). The hooks never move to the console script |
| pip cannot pick a wheel index from the GPU driver | pyproject.toml's torch note |
Survives, reduced. pip install …[enrich] still lands CPU-only torch on a CUDA host. chitragupta doctor detects it and chitragupta install gpu-torch fixes it, but neither is automatic |
| Command names can be renamed freely because nothing external holds them | the PR that shipped an early rename | Survives, sharpened: see the last section |
| The deliverable is the docs and skills, not the code | scripts/release.py's docstring |
Retired by chitragupta init. This was the strongest objection and the reason a bare wheel would have been the wrong shape |
| Everything is anchored to where the code lives | chitragupta/config.py's REPO_ROOT |
Retired by splitting it into a discovered project root and a package-data root |
🗜 Why the zip ships too¶
The release archive does not go away. It is built by a denylist:
scripts/release.py ships every git-tracked file except a named few, so
a new root-level file ships unless someone excludes it. chitragupta
init is an allowlist: a new root-level file is not scaffolded
unless someone adds it.
Two lists that must agree drift: a new root-level CI file would enter
the archive silently. So the two are pinned against each other by a
test. The deliberate differences (bench/, tests/,
.github/, and the CI config that is actively wrong outside this
repository) are held in one named set that both sides read.
⚡ What a shipped command name costs¶
Before a package ships, renaming a command is nearly free: the strings
live only in the repository's own docs, skills and tests, so a rename is
a sweep. A published console script ends that freedom. Once
chitragupta exists in users' bin/, the name also lives in shell
scripts, Makefiles, CI configs, cron
jobs and muscle memory, places git grep cannot reach. A verb rename
becomes a MAJOR bump with a deprecation window, and breaks somebody's
setup anyway.
The verb vocabulary was chosen once and argued for
(chitragupta/draft.py's own docstring sets out why retrieve rather
than retrieval), and it is permanent.