[project] name = "no-human" version = "0.2.4" description = "Autonomous AI software-delivery orchestrator driving the Claude Agent SDK on your own Claude credentials" readme = "README.md" requires-python = ">=3.12" # PEP 639. hatchling >= 1.27 reads the SPDX string and `license-files`, and # emits `License-Expression` / `License-File` metadata; without these two the # built wheel declares no licence at all and dependency scanners read it as # unlicensed. The file itself is LICENSE at the repo root and does not change. license = "MIT" license-files = ["LICENSE"] dependencies = [ "aiosqlite>=0.22.1", "claude-agent-sdk>=0.2.120", "click>=8.4.1", "fastapi>=0.138.0", "httpx>=0.28.1", # BOTH BOUNDS ARE LOAD-BEARING. `intake/mcp_bridge.py` imports # `mcp.server.mcpserver`, which exists from 2.0.0 (1.x shipped # `mcp.server.fastmcp` instead; 2.x keeps that path only as a stub that # raises). The upper bound is the lesson of public issue #15: an # unbounded requirement shipped a broken `nh mcp-serve` to every PyPI # install when 2.0.0 moved the module, while every lane that resolves # through uv.lock stayed green. When 3.0 moves it again, `<3` is what # keeps a fresh `pip install no-human` on a version the bridge imports. # Raise either bound only together with the import in # `intake/mcp_bridge.py`; `tests/test_mcp_dependency_bound.py` fails if # the declared bound admits a major the bridge was not ported to. # claude-agent-sdk >=0.2.140 pins `mcp<3.0.0` itself; 0.2.121 pinned # `mcp<2.0.0`, which is why raising this floor also moved that package. "mcp>=2,<3", "psutil>=7.0.0", "pyyaml>=6.0.3", "rich>=15.0.0", "slack-sdk>=3.43.0", "textual>=8.2.7", # [standard] pulls in the `websockets` impl uvicorn needs for the board's # live-update WS. Plain uvicorn can't do the WS handshake — the upgrade then # falls through to the SPA catch-all (HTTP 200) and the board never syncs. "uvicorn[standard]>=0.49.0", ] [project.urls] Homepage = "https://getnohuman.com" Source = "https://github.com/no-human-ai/no_human" Documentation = "https://github.com/no-human-ai/no_human/blob/main/docs/README.md" Changelog = "https://github.com/no-human-ai/no_human/blob/main/CHANGELOG.md" Issues = "https://github.com/no-human-ai/no_human/issues" "Release notes" = "https://github.com/no-human-ai/no_human/releases" [project.scripts] nh = "no_human.cli.commands:main" # The same entry point under the package's own name, so `uvx no-human mcp-serve` # resolves without `--from` — the shape the MCP Registry and its clients run. no-human = "no_human.cli.commands:main" # Redirects HOME to a temp dir before no_human.config can bind DB_PATH / # CONFIG_PATH to the operator's real ~/.no_human — see # src/no_human/testing/pytest_isolated_home.py for why this must be a # pytest11 entry point (loads before test collection) and not a fixture. # Self-guarded: a no-op outside this repo's own suite. [project.entry-points.pytest11] no_human_isolated_home = "no_human.testing.pytest_isolated_home" [dependency-groups] dev = [ "pyinstaller>=6.21.0", "pytest>=8.0", "pytest-asyncio>=0.24", "pytest-xdist>=3.5", "pytest-cov>=5.0", "mypy>=1.11", ] # Optional: browser-driven board E2E (see e2e/). Not in the default test run. e2e = [ "playwright>=1.50", ] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/no_human"] # `packages` takes the tree whole, so a carve-out has to be spelled out. # no_human/ci_gate is a post-PR gate wired to one specific CI estate — it is # not general-purpose, so it stays in this repo for internal use and is kept # out of anything distributed. Its one importer, # blockers/wake.py, imports it lazily inside a try/except and disables the rung # when the import fails. packaging/nh-server.spec excludes it from the frozen # server, and packaging/build-installer.sh fails the build if it slips back in. exclude = [ "src/no_human/ci_gate", # The private half of the publish guard's term inventory. Its own docstring # says it must not be published IN ANY FORM, including hex; vendor_terms.py # imports it with an empty-list fallback designed for exactly this absence. # It was export-excluded from day one but shipped in every wheel/sdist/freeze # until a 2026-07-31 review decoded it out of the sdist. "src/no_human/eval/_vendor_terms_private.py", # Editor droppings and VCS junk that live UNDER the allowlisted root: an # allowlist of directories admits whatever is inside them, and an untracked # vim swap of a source file can carry unsaved pre-scrub content. "**/.*.swp", "**/*.swp", "**/*.orig", "**/*.rej", "**/.DS_Store", ] # The built React board is NOT declared here — see `hatch_build.py`. # # It used to be: `"web/dist" = "no_human/web_dist"`, chosen over an # `artifacts`/`include` glob because force-include RAISES when the source is # absent, so a release cut without `npm run build` fails instead of quietly # shipping a CLI whose board never renders (that shipped once). The guarantee is # right and is preserved; the mechanism was too wide. force-include is static, so # it is evaluated for every build version — including the EDITABLE wheel that # `uv sync` and `pip install -e .` build. web/dist is gitignored, so a clean # clone has none, and the release-time guard fired on a developer's first # command: `uv sync` on a fresh clone died in `build_editable` with # `FileNotFoundError: Forced include not found`, before test collection. (The # comment that stood here claimed "nothing in CI builds a wheel, so this only # ever fires on a release build" — false, as that reproduction shows.) # # `hatch_build.py` re-adds the same entry through `build_data["force_include"]`, # where the build VERSION is visible: a standard wheel/sdist still FAILS without # the board, now with a message naming `npm run build`; an editable install # warns and proceeds. Cost when present, measured 2026-07-31: +486 KB compressed # (724 KB -> 1.2 MB), against a dependency tree of tens of MB. [tool.hatch.build.targets.wheel.force-include] # The schema. `migrations/*.sql` is what creates the tables on first run, and # it lives at the REPO ROOT, so `packages = ["src/no_human"]` never packaged # it: every wheel ever built shipped zero migrations. `core/db.py` globbed a # directory that did not exist, got no matches (glob does not raise), created # no schema, and the first `ALTER TABLE` blew up with "no such table: tasks" — # which then HUNG the process forever on aiosqlite's non-daemon worker thread. # `nh status`, `nh doctor` and `nh task list` never returned for any user who # installed the wheel. force-include for the same reason as the board: # absence must fail the BUILD, not the customer. It stays STATIC where the board # became a hook, because `migrations/` is TRACKED — every clone and every # worktree has it, so this can only fire on a real deletion, never on a clean # clone. "migrations" = "no_human/migrations" # The board's force-include, made version-aware. `source`/`target` are read by # the hook (and by `testing/runner.py::_ensure_forced_build_artifacts`, which # provisions gitignored forced paths into task worktrees). The wheel puts the # board INSIDE the package: the repo-relative `web/dist` resolves outside # site-packages once installed, so `api/app.py::_resolve_web_dist` looks in both # places and this is the one it finds in a wheel. [tool.hatch.build.targets.wheel.hooks.custom] path = "hatch_build.py" source = "web/dist" target = "no_human/web_dist" # The sdist is a MEMBERSHIP list, not "the repo tree minus mistakes". Without # `only-include`, hatchling packages everything under the project root that is # not ignored — which on a development machine includes UNTRACKED local files # (a 2026-07-31 byte-scan of a freshly built sdist found local scratch notes # and the private half of the vendor-term corpus inside it). An sdist built # anywhere must contain exactly what a wheel build needs and nothing else. [tool.hatch.build.targets.sdist] only-include = ["src/no_human"] # Same carve-out as the wheel: the wheel's `exclude` is target-scoped, so # without this line the sdist ships the ci_gate sources the wheel exists to # withhold (D1) — proven by unpacking one, not assumed. Same list as the wheel: # the private inventory and editor junk must not ride in either artifact. exclude = [ "src/no_human/ci_gate", # The employer half of the publish guard's term inventory. Its own docstring # says it must not be published IN ANY FORM, including hex; vendor_terms.py # imports it with an empty-list fallback designed for exactly this absence. # It was export-excluded from day one but shipped in every wheel/sdist/freeze # until a 2026-07-31 review decoded it out of the sdist. "src/no_human/eval/_vendor_terms_private.py", # Editor droppings and VCS junk that live UNDER the allowlisted root: an # allowlist of directories admits whatever is inside them, and an untracked # vim swap of a source file can carry unsaved pre-scrub content. "**/.*.swp", "**/*.swp", "**/*.orig", "**/*.rej", "**/.DS_Store", ] # The board is declared by `hatch_build.py` for this target too — same reason as # the wheel, and the sdist has no editable version, so in practice the hook here # only ever raises or includes. It keeps the board at its repo-relative path so # that a wheel built FROM the sdist (what `pip install` does when it cannot use # a prebuilt wheel) still finds `web/dist` where the wheel target expects it. # Note hatchling force-includes `hatch_build.py` into every sdist by itself # (`SdistBuilder.get_default_build_data`, DEFAULT_BUILD_SCRIPT), so the sdist # stays a sufficient build source without a line here. [tool.hatch.build.targets.sdist.force-include] # Same rationale as web/dist: kept at its repo-relative path so a wheel built # FROM the sdist (what `pip install` does when it cannot use a prebuilt wheel) # still finds `migrations` where the wheel target above reads it. "migrations" = "migrations" [tool.hatch.build.targets.sdist.hooks.custom] path = "hatch_build.py" source = "web/dist" target = "web/dist" [tool.pytest.ini_options] asyncio_mode = "auto" testpaths = ["tests"] # Make a COPY of this repo test ITSELF. Without it, `import no_human` # in any checkout resolves through the editable install in the active # environment — i.e. the ORIGINAL working tree — so a sandbox, a # worktree or a bench clone silently validates code it did not change. pythonpath = ["src"] markers = [ "real_backend: exercises the real ClaudeBackend over a mocked SDK client — exempt from the hermetic stub", "slow: marks tests that take >10s (deselect with '-m \"not slow\"')", "nightly: heavy guard whose protection is consumed off the push path (at DMG/publish time, or only when the demo is rebuilt) — runs in the nightly/release lane, not per PR. Lane composition is pinned by tests/test_test_lanes.py.", "repoguard: repo-wide guard that a change in ANY directory can break, so `.no_human.yml`'s scoped web/desktop routes select it with `-m repoguard` instead of naming files. The guard set differs between checkouts; selecting nothing is exit 5, so an emptied marker fails loudly.", ]