Skip to content

📦 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

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
pip install chitragupta-cli

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
[tool.poetry.scripts]
chitragupta = "chitragupta.__main__:main"
cg          = "chitragupta.__main__:main"

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
chitragupta <layer> <verb> ...   ==   python -m chitragupta.<layer> <verb> ...

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's bin/; 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), and chitragupta/hook_launchers.py records 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 a PATH lookup 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.