no-human 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. no_human-0.1.1/.gitignore +63 -0
  2. no_human-0.1.1/LICENSE +21 -0
  3. no_human-0.1.1/PKG-INFO +191 -0
  4. no_human-0.1.1/README.md +170 -0
  5. no_human-0.1.1/hatch_build.py +213 -0
  6. no_human-0.1.1/migrations/0001_init.sql +91 -0
  7. no_human-0.1.1/migrations/0002_project_profiles.sql +13 -0
  8. no_human-0.1.1/migrations/0003_task_kind.sql +4 -0
  9. no_human-0.1.1/migrations/0004_projects.sql +11 -0
  10. no_human-0.1.1/migrations/0005_task_events.sql +10 -0
  11. no_human-0.1.1/migrations/0006_events_fts.sql +28 -0
  12. no_human-0.1.1/migrations/0007_playbooks.sql +19 -0
  13. no_human-0.1.1/migrations/0008_pr_edges.sql +13 -0
  14. no_human-0.1.1/migrations/0009_fts_ci_gate_event_kind.sql +22 -0
  15. no_human-0.1.1/migrations/0010_pr_outcomes.sql +50 -0
  16. no_human-0.1.1/migrations/0011_verification_receipts.sql +44 -0
  17. no_human-0.1.1/migrations/0012_memory_usage_ledger.sql +41 -0
  18. no_human-0.1.1/migrations/0013_fix_pairs.sql +34 -0
  19. no_human-0.1.1/pyproject.toml +186 -0
  20. no_human-0.1.1/src/no_human/__init__.py +7 -0
  21. no_human-0.1.1/src/no_human/agent/__init__.py +0 -0
  22. no_human-0.1.1/src/no_human/agent/advisory.py +68 -0
  23. no_human-0.1.1/src/no_human/agent/backend.py +403 -0
  24. no_human-0.1.1/src/no_human/agent/backend_check.py +272 -0
  25. no_human-0.1.1/src/no_human/agent/claude_backend.py +1302 -0
  26. no_human-0.1.1/src/no_human/agent/codex_backend.py +722 -0
  27. no_human-0.1.1/src/no_human/agent/guard.py +1577 -0
  28. no_human-0.1.1/src/no_human/agent/lint_hook.py +95 -0
  29. no_human-0.1.1/src/no_human/agent/scope_guard.py +340 -0
  30. no_human-0.1.1/src/no_human/agent/supervisor.py +528 -0
  31. no_human-0.1.1/src/no_human/agent/tool_result_cap.py +191 -0
  32. no_human-0.1.1/src/no_human/agent/venv_install_guard.py +552 -0
  33. no_human-0.1.1/src/no_human/agent/verification_receipts.py +993 -0
  34. no_human-0.1.1/src/no_human/agent/worker_context.py +80 -0
  35. no_human-0.1.1/src/no_human/api/__init__.py +3 -0
  36. no_human-0.1.1/src/no_human/api/app.py +4571 -0
  37. no_human-0.1.1/src/no_human/api/models.py +729 -0
  38. no_human-0.1.1/src/no_human/blockers/__init__.py +65 -0
  39. no_human-0.1.1/src/no_human/blockers/actions.py +217 -0
  40. no_human-0.1.1/src/no_human/blockers/answers.py +139 -0
  41. no_human-0.1.1/src/no_human/blockers/challenge.py +114 -0
  42. no_human-0.1.1/src/no_human/blockers/landed_override.py +242 -0
  43. no_human-0.1.1/src/no_human/blockers/pr_closeout.py +103 -0
  44. no_human-0.1.1/src/no_human/blockers/report.py +239 -0
  45. no_human-0.1.1/src/no_human/blockers/shipped.py +277 -0
  46. no_human-0.1.1/src/no_human/blockers/taxonomy.py +371 -0
  47. no_human-0.1.1/src/no_human/blockers/wake.py +1917 -0
  48. no_human-0.1.1/src/no_human/brain/__init__.py +170 -0
  49. no_human-0.1.1/src/no_human/brain/cli.py +351 -0
  50. no_human-0.1.1/src/no_human/brain/client.py +280 -0
  51. no_human-0.1.1/src/no_human/brain/credentials.py +288 -0
  52. no_human-0.1.1/src/no_human/brain/keys.py +61 -0
  53. no_human-0.1.1/src/no_human/brain/render.py +114 -0
  54. no_human-0.1.1/src/no_human/brain/screen.py +237 -0
  55. no_human-0.1.1/src/no_human/brain/settings.py +72 -0
  56. no_human-0.1.1/src/no_human/brain/store.py +444 -0
  57. no_human-0.1.1/src/no_human/brain/sync.py +578 -0
  58. no_human-0.1.1/src/no_human/brain/verify.py +233 -0
  59. no_human-0.1.1/src/no_human/ci/__init__.py +232 -0
  60. no_human-0.1.1/src/no_human/ci/base.py +180 -0
  61. no_human-0.1.1/src/no_human/ci/circleci.py +214 -0
  62. no_human-0.1.1/src/no_human/ci/ghe_checkruns.py +394 -0
  63. no_human-0.1.1/src/no_human/ci/github_actions.py +144 -0
  64. no_human-0.1.1/src/no_human/ci/gitlab.py +567 -0
  65. no_human-0.1.1/src/no_human/ci/jenkins.py +422 -0
  66. no_human-0.1.1/src/no_human/ci/jenkins_session.py +152 -0
  67. no_human-0.1.1/src/no_human/ci/parser.py +49 -0
  68. no_human-0.1.1/src/no_human/cli/__init__.py +62 -0
  69. no_human-0.1.1/src/no_human/cli/api_client.py +332 -0
  70. no_human-0.1.1/src/no_human/cli/commands.py +7178 -0
  71. no_human-0.1.1/src/no_human/cli/init_cmd.py +660 -0
  72. no_human-0.1.1/src/no_human/cli/shell.py +557 -0
  73. no_human-0.1.1/src/no_human/cli/shell_input.py +216 -0
  74. no_human-0.1.1/src/no_human/cli/shell_lanes.py +246 -0
  75. no_human-0.1.1/src/no_human/cli/tui.py +139 -0
  76. no_human-0.1.1/src/no_human/config.py +2124 -0
  77. no_human-0.1.1/src/no_human/context/__init__.py +36 -0
  78. no_human-0.1.1/src/no_human/context/base.py +98 -0
  79. no_human-0.1.1/src/no_human/context/codebase.py +130 -0
  80. no_human-0.1.1/src/no_human/context/gatherer.py +41 -0
  81. no_human-0.1.1/src/no_human/context/outlook.py +35 -0
  82. no_human-0.1.1/src/no_human/context/repo_map.py +134 -0
  83. no_human-0.1.1/src/no_human/context/sessions.py +137 -0
  84. no_human-0.1.1/src/no_human/context/teams.py +126 -0
  85. no_human-0.1.1/src/no_human/core/__init__.py +0 -0
  86. no_human-0.1.1/src/no_human/core/autonomy.py +365 -0
  87. no_human-0.1.1/src/no_human/core/bounds.py +339 -0
  88. no_human-0.1.1/src/no_human/core/build_info.py +199 -0
  89. no_human-0.1.1/src/no_human/core/complexity.py +261 -0
  90. no_human-0.1.1/src/no_human/core/db.py +3674 -0
  91. no_human-0.1.1/src/no_human/core/events.py +85 -0
  92. no_human-0.1.1/src/no_human/core/health.py +158 -0
  93. no_human-0.1.1/src/no_human/core/humanize.py +12 -0
  94. no_human-0.1.1/src/no_human/core/infra_breaker.py +96 -0
  95. no_human-0.1.1/src/no_human/core/jsonparse.py +75 -0
  96. no_human-0.1.1/src/no_human/core/lanes.py +110 -0
  97. no_human-0.1.1/src/no_human/core/metrics.py +369 -0
  98. no_human-0.1.1/src/no_human/core/multi_repo.py +151 -0
  99. no_human-0.1.1/src/no_human/core/orchestrator.py +15549 -0
  100. no_human-0.1.1/src/no_human/core/ordinal.py +12 -0
  101. no_human-0.1.1/src/no_human/core/plan_gate.py +231 -0
  102. no_human-0.1.1/src/no_human/core/pr_evidence.py +142 -0
  103. no_human-0.1.1/src/no_human/core/pricing.py +379 -0
  104. no_human-0.1.1/src/no_human/core/prompt_blocks.py +928 -0
  105. no_human-0.1.1/src/no_human/core/report_quality.py +69 -0
  106. no_human-0.1.1/src/no_human/core/review_routing.py +303 -0
  107. no_human-0.1.1/src/no_human/core/runtime.py +52 -0
  108. no_human-0.1.1/src/no_human/core/scheduler.py +1456 -0
  109. no_human-0.1.1/src/no_human/core/task.py +448 -0
  110. no_human-0.1.1/src/no_human/core/worktree.py +321 -0
  111. no_human-0.1.1/src/no_human/docs_gen.py +242 -0
  112. no_human-0.1.1/src/no_human/doctor.py +577 -0
  113. no_human-0.1.1/src/no_human/eval/__init__.py +25 -0
  114. no_human-0.1.1/src/no_human/eval/bench_compare.py +529 -0
  115. no_human-0.1.1/src/no_human/eval/bench_task.py +613 -0
  116. no_human-0.1.1/src/no_human/eval/funnel_corpus.py +124 -0
  117. no_human-0.1.1/src/no_human/eval/funnel_criteria.py +123 -0
  118. no_human-0.1.1/src/no_human/eval/funnel_eval.py +587 -0
  119. no_human-0.1.1/src/no_human/eval/golden.py +104 -0
  120. no_human-0.1.1/src/no_human/eval/harness.py +175 -0
  121. no_human-0.1.1/src/no_human/eval/harvest.py +122 -0
  122. no_human-0.1.1/src/no_human/eval/judge.py +361 -0
  123. no_human-0.1.1/src/no_human/eval/northstar.py +1079 -0
  124. no_human-0.1.1/src/no_human/eval/northstar_card.py +1459 -0
  125. no_human-0.1.1/src/no_human/eval/replay.py +302 -0
  126. no_human-0.1.1/src/no_human/eval/sandbox_selftest.py +112 -0
  127. no_human-0.1.1/src/no_human/eval/scorecard.py +201 -0
  128. no_human-0.1.1/src/no_human/eval/startup.py +539 -0
  129. no_human-0.1.1/src/no_human/eval/vendor_terms.py +442 -0
  130. no_human-0.1.1/src/no_human/history/__init__.py +10 -0
  131. no_human-0.1.1/src/no_human/history/analyzer.py +414 -0
  132. no_human-0.1.1/src/no_human/history/claude_code.py +250 -0
  133. no_human-0.1.1/src/no_human/history/extractor.py +322 -0
  134. no_human-0.1.1/src/no_human/history/ingester.py +233 -0
  135. no_human-0.1.1/src/no_human/history/machinery.py +123 -0
  136. no_human-0.1.1/src/no_human/history/skills.py +166 -0
  137. no_human-0.1.1/src/no_human/history/topic.py +202 -0
  138. no_human-0.1.1/src/no_human/intake/__init__.py +60 -0
  139. no_human-0.1.1/src/no_human/intake/base.py +115 -0
  140. no_human-0.1.1/src/no_human/intake/classify.py +480 -0
  141. no_human-0.1.1/src/no_human/intake/evaluator.py +879 -0
  142. no_human-0.1.1/src/no_human/intake/github_issues.py +70 -0
  143. no_human-0.1.1/src/no_human/intake/gitlab_issues.py +68 -0
  144. no_human-0.1.1/src/no_human/intake/grill.py +258 -0
  145. no_human-0.1.1/src/no_human/intake/jira.py +254 -0
  146. no_human-0.1.1/src/no_human/intake/jira_poll.py +247 -0
  147. no_human-0.1.1/src/no_human/intake/linear.py +700 -0
  148. no_human-0.1.1/src/no_human/intake/linear_poll.py +284 -0
  149. no_human-0.1.1/src/no_human/intake/mcp_bridge.py +112 -0
  150. no_human-0.1.1/src/no_human/intake/monday.py +707 -0
  151. no_human-0.1.1/src/no_human/intake/monday_poll.py +303 -0
  152. no_human-0.1.1/src/no_human/intake/split_proposal.py +147 -0
  153. no_human-0.1.1/src/no_human/intake/surface_advisory.py +84 -0
  154. no_human-0.1.1/src/no_human/intake/unavailable_inputs.py +177 -0
  155. no_human-0.1.1/src/no_human/integrations/__init__.py +1396 -0
  156. no_human-0.1.1/src/no_human/integrations/slack/__init__.py +11 -0
  157. no_human-0.1.1/src/no_human/integrations/slack/intake.py +198 -0
  158. no_human-0.1.1/src/no_human/integrations/slack/worker.py +131 -0
  159. no_human-0.1.1/src/no_human/learning/__init__.py +35 -0
  160. no_human-0.1.1/src/no_human/learning/corrections.py +268 -0
  161. no_human-0.1.1/src/no_human/learning/curator.py +163 -0
  162. no_human-0.1.1/src/no_human/learning/pii.py +299 -0
  163. no_human-0.1.1/src/no_human/learning/provenance.py +312 -0
  164. no_human-0.1.1/src/no_human/learning/queue.py +1067 -0
  165. no_human-0.1.1/src/no_human/learning/ranking.py +210 -0
  166. no_human-0.1.1/src/no_human/learning/retire.py +208 -0
  167. no_human-0.1.1/src/no_human/learning/scope.py +157 -0
  168. no_human-0.1.1/src/no_human/learning/triggers.py +91 -0
  169. no_human-0.1.1/src/no_human/learning/vocab.py +210 -0
  170. no_human-0.1.1/src/no_human/notify/__init__.py +136 -0
  171. no_human-0.1.1/src/no_human/notify/slack.py +58 -0
  172. no_human-0.1.1/src/no_human/notify/teams.py +217 -0
  173. no_human-0.1.1/src/no_human/onboard.py +548 -0
  174. no_human-0.1.1/src/no_human/profile.py +199 -0
  175. no_human-0.1.1/src/no_human/project_config.py +159 -0
  176. no_human-0.1.1/src/no_human/project_model.py +68 -0
  177. no_human-0.1.1/src/no_human/repo_discovery.py +581 -0
  178. no_human-0.1.1/src/no_human/review/__init__.py +4 -0
  179. no_human-0.1.1/src/no_human/review/lint_evidence.py +378 -0
  180. no_human-0.1.1/src/no_human/review/reviewer.py +2494 -0
  181. no_human-0.1.1/src/no_human/review/selfcheck.py +59 -0
  182. no_human-0.1.1/src/no_human/review/tamper_adjudication.py +456 -0
  183. no_human-0.1.1/src/no_human/review/wiring_evidence.py +166 -0
  184. no_human-0.1.1/src/no_human/telemetry.py +274 -0
  185. no_human-0.1.1/src/no_human/testing/__init__.py +0 -0
  186. no_human-0.1.1/src/no_human/testing/plan_runner.py +362 -0
  187. no_human-0.1.1/src/no_human/testing/pytest_isolated_home.py +180 -0
  188. no_human-0.1.1/src/no_human/testing/repro_gate.py +448 -0
  189. no_human-0.1.1/src/no_human/testing/runner.py +1241 -0
  190. no_human-0.1.1/src/no_human/testing/tamper_guard.py +442 -0
  191. no_human-0.1.1/src/no_human/testing/test_layers.py +163 -0
  192. no_human-0.1.1/src/no_human/updates.py +286 -0
  193. no_human-0.1.1/src/no_human/vcs/__init__.py +106 -0
  194. no_human-0.1.1/src/no_human/vcs/approve_merge.py +632 -0
  195. no_human-0.1.1/src/no_human/vcs/comment_poster.py +326 -0
  196. no_human-0.1.1/src/no_human/vcs/derived_conflict.py +391 -0
  197. no_human-0.1.1/src/no_human/vcs/git.py +943 -0
  198. no_human-0.1.1/src/no_human/vcs/github.py +93 -0
  199. no_human-0.1.1/src/no_human/vcs/gitlab.py +39 -0
  200. no_human-0.1.1/src/no_human/vcs/manifest_repair.py +283 -0
  201. no_human-0.1.1/src/no_human/vcs/merge_order.py +58 -0
  202. no_human-0.1.1/src/no_human/vcs/outbound_scrub.py +63 -0
  203. no_human-0.1.1/src/no_human/vcs/pr_outcome.py +503 -0
  204. no_human-0.1.1/src/no_human/vcs/pr_refs.py +60 -0
  205. no_human-0.1.1/src/no_human/vcs/pr_watcher.py +1524 -0
  206. no_human-0.1.1/src/no_human/vcs/push_hook.py +278 -0
  207. no_human-0.1.1/src/no_human/vcs/receipts.py +106 -0
  208. no_human-0.1.1/src/no_human/vcs/task_pr.py +160 -0
  209. no_human-0.1.1/web/dist/assets/-F63fjptAgt5VM-kVkqdyU8n1i8q131nj-o-BJoXLJYV.woff2 +0 -0
  210. no_human-0.1.1/web/dist/assets/-F63fjptAgt5VM-kVkqdyU8n1iAq131nj-otFQ-DKn25-tQ.woff2 +0 -0
  211. no_human-0.1.1/web/dist/assets/-F63fjptAgt5VM-kVkqdyU8n1iEq131nj-otFQ-C05TWSE2.woff2 +0 -0
  212. no_human-0.1.1/web/dist/assets/-F63fjptAgt5VM-kVkqdyU8n1iIq131nj-otFQ-BKehAWor.woff2 +0 -0
  213. no_human-0.1.1/web/dist/assets/-F63fjptAgt5VM-kVkqdyU8n1isq131nj-otFQ-DkeVBss5.woff2 +0 -0
  214. no_human-0.1.1/web/dist/assets/-F6pfjptAgt5VM-kVkqdyU8n1ioa0XdgregdFOFh-DBCFrjSV.woff2 +0 -0
  215. no_human-0.1.1/web/dist/assets/-F6pfjptAgt5VM-kVkqdyU8n1ioa1XdgregdFA-BkxdLi3-.woff2 +0 -0
  216. no_human-0.1.1/web/dist/assets/-F6pfjptAgt5VM-kVkqdyU8n1ioa23dgregdFOFh-DjXFaAjD.woff2 +0 -0
  217. no_human-0.1.1/web/dist/assets/-F6pfjptAgt5VM-kVkqdyU8n1ioa2HdgregdFOFh-BgiqLiQn.woff2 +0 -0
  218. no_human-0.1.1/web/dist/assets/-F6pfjptAgt5VM-kVkqdyU8n1ioa2ndgregdFOFh-D3ijpaJE.woff2 +0 -0
  219. no_human-0.1.1/web/dist/assets/-F6qfjptAgt5VM-kVkqdyU8n3twJwl1FgsAXHNlYzg-CU9Da17h.woff2 +0 -0
  220. no_human-0.1.1/web/dist/assets/-F6qfjptAgt5VM-kVkqdyU8n3twJwl5FgsAXHNlYzg-BQJS6Ovj.woff2 +0 -0
  221. no_human-0.1.1/web/dist/assets/-F6qfjptAgt5VM-kVkqdyU8n3twJwl9FgsAXHNlYzg-B5e70VyC.woff2 +0 -0
  222. no_human-0.1.1/web/dist/assets/-F6qfjptAgt5VM-kVkqdyU8n3twJwlBFgsAXHNk-C820gu2e.woff2 +0 -0
  223. no_human-0.1.1/web/dist/assets/-F6qfjptAgt5VM-kVkqdyU8n3twJwlRFgsAXHNlYzg-dnJBCtls.woff2 +0 -0
  224. no_human-0.1.1/web/dist/assets/-F6qfjptAgt5VM-kVkqdyU8n3vAOwl1FgsAXHNlYzg-hCF3fsXQ.woff2 +0 -0
  225. no_human-0.1.1/web/dist/assets/-F6qfjptAgt5VM-kVkqdyU8n3vAOwl5FgsAXHNlYzg-BRMVj9uZ.woff2 +0 -0
  226. no_human-0.1.1/web/dist/assets/-F6qfjptAgt5VM-kVkqdyU8n3vAOwl9FgsAXHNlYzg-Dky8cY56.woff2 +0 -0
  227. no_human-0.1.1/web/dist/assets/-F6qfjptAgt5VM-kVkqdyU8n3vAOwlBFgsAXHNk-DpGnXj3s.woff2 +0 -0
  228. no_human-0.1.1/web/dist/assets/-F6qfjptAgt5VM-kVkqdyU8n3vAOwlRFgsAXHNlYzg-DiqaCFl3.woff2 +0 -0
  229. no_human-0.1.1/web/dist/assets/index-D4wFtDnQ.js +96 -0
  230. no_human-0.1.1/web/dist/assets/index-wqze04F6.css +1 -0
  231. no_human-0.1.1/web/dist/assets/module-BBzDlm4e.js +3 -0
  232. no_human-0.1.1/web/dist/assets/rP2Hp2ywxg089UriCZ2IHTWEBlwu8Q-BH18Pfgv.woff2 +0 -0
  233. no_human-0.1.1/web/dist/assets/rP2Hp2ywxg089UriCZOIHTWEBlw-BNY05QUC.woff2 +0 -0
  234. no_human-0.1.1/web/dist/assets/rP2Wp2ywxg089UriCZaSExdy3sGt9zz86GPwyKK58UfivUw4aw-CTzf7qy0.woff2 +0 -0
  235. no_human-0.1.1/web/dist/assets/rP2Wp2ywxg089UriCZaSExdy3sGt9zz86GPwyKy58UfivUw-C5VcRjlN.woff2 +0 -0
  236. no_human-0.1.1/web/dist/index.html +16 -0
  237. no_human-0.1.1/web/dist/nh-mark-512.png +0 -0
  238. no_human-0.1.1/web/dist/nh-mark-64.png +0 -0
@@ -0,0 +1,63 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ # desktop/build is electron-builder's buildResources. Neither icon.icns nor
6
+ # icon.ico is tracked: both are build OUTPUTS, derived at package time from
7
+ # web/public/nh-mark-512.png by packaging/derive-icons.mjs — see that file's
8
+ # header comment for why the binaries stopped being committed.
9
+ !desktop/build/
10
+ desktop/build/*
11
+ dist/
12
+ wheels/
13
+ *.egg-info
14
+
15
+ # Virtual environments
16
+ .venv
17
+
18
+ # Secrets & local state — never commit
19
+ .env
20
+ *.key
21
+ *.pem
22
+ .no_human/
23
+ secrets/
24
+ *.db
25
+ *.db-wal
26
+ *.db-shm
27
+ # ...except the memory-triage runbook's committed test fixture: a tracked
28
+ # INPUT (synthetic, neutral content -- see EXPORT_CLASSIFICATION.txt).
29
+ !testdata/memory_triage_fixture.db
30
+
31
+ # Test scratch
32
+ /tmp_e2e/
33
+ # pytest's node-id cache stores collected test names verbatim, so it carries
34
+ # whatever the test bodies spell. It was NOT ignored — only `__pycache__/` and
35
+ # `*.py[oc]` were — so a `git add -A` after a test run could have tracked it.
36
+ .pytest_cache/
37
+
38
+ # IDE-generated (skills, local settings)
39
+ .claude/
40
+ .windsurf/
41
+
42
+ # web build
43
+ web/node_modules/
44
+
45
+ # north-star bench: the raw built corpus is verbatim operator conversation
46
+ # content (personal + enterprise) — never committed. Curated specs live one
47
+ # level up and ARE tracked.
48
+ eval/northstar_tasks/generated/
49
+ # Machine-local translation from vendor-neutral spec paths to real checkouts.
50
+ # Holds real repo names by definition — never committed.
51
+ eval/repo_map.yaml
52
+ eval/results/northstar/
53
+
54
+ # desktop shell
55
+ desktop/node_modules/
56
+ # also match a SYMLINK: a trailing slash only matches a directory, so a
57
+ # symlinked node_modules slipped past and got committed as an absolute path.
58
+ desktop/node_modules
59
+ web/node_modules
60
+ desktop/dist/
61
+
62
+ # The adoption harness writes its report here on every run.
63
+ e2e/adoption/out/
no_human-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eyal Golan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,191 @@
1
+ Metadata-Version: 2.5
2
+ Name: no-human
3
+ Version: 0.1.1
4
+ Summary: Autonomous AI software-delivery orchestrator driving the Claude Agent SDK on your own Claude credentials
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.12
8
+ Requires-Dist: aiosqlite>=0.22.1
9
+ Requires-Dist: claude-agent-sdk>=0.2.120
10
+ Requires-Dist: click>=8.4.1
11
+ Requires-Dist: fastapi>=0.138.0
12
+ Requires-Dist: httpx>=0.28.1
13
+ Requires-Dist: mcp>=1.28.0
14
+ Requires-Dist: psutil>=7.0.0
15
+ Requires-Dist: pyyaml>=6.0.3
16
+ Requires-Dist: rich>=15.0.0
17
+ Requires-Dist: slack-sdk>=3.43.0
18
+ Requires-Dist: textual>=8.2.7
19
+ Requires-Dist: uvicorn[standard]>=0.49.0
20
+ Description-Content-Type: text/markdown
21
+
22
+ <div align="center">
23
+
24
+ <img src="docs/assets/nh-mark.png" alt="" width="140" height="140">
25
+
26
+ # no_human
27
+
28
+ **From ticket to reviewed pull request.**<br>***Free and open-source, on your machine.***
29
+
30
+ [![latest release](https://img.shields.io/github/v/release/no-human-ai/no_human?label=release&color=4C9AFF)](https://github.com/no-human-ai/no_human/releases/latest) [![CI](https://img.shields.io/github/actions/workflow/status/no-human-ai/no_human/ci.yml?branch=main&label=CI)](https://github.com/no-human-ai/no_human/actions/workflows/ci.yml) [![python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue)](https://www.python.org/) [![license MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
31
+
32
+ [getnohuman.com](https://getnohuman.com) · [Quickstart](docs/quickstart.md) · [Docs](docs/README.md) · [Watch it work a sprint](https://getnohuman.com/demo)
33
+
34
+ [![Download for macOS](https://img.shields.io/badge/Download%20for-macOS-4C9AFF?style=for-the-badge)](https://github.com/no-human-ai/no_human/releases/latest) [![Download for Windows](https://img.shields.io/badge/Download%20for-Windows-4C9AFF?style=for-the-badge)](https://github.com/no-human-ai/no_human/releases/latest) [![Download for Linux](https://img.shields.io/badge/Download%20for-Linux-4C9AFF?style=for-the-badge)](https://github.com/no-human-ai/no_human/releases/latest)
35
+
36
+ <a href="https://getnohuman.com/"><img src="docs/assets/hero-loop-poster.jpg" alt="The no_human board: one task waiting on a question in Needs answer, four tasks working in parallel, one pull request ready for review." width="880"></a>
37
+
38
+ <sub>▶ <a href="https://getnohuman.com/">Watch the loop</a> — a ticket in, a reviewed pull request out; the whole loop in 57 seconds.</sub>
39
+
40
+ </div>
41
+
42
+ The AI coding factory you <ins>**can trust**</ins>:
43
+
44
+ - **A plan before any code**, from the ticket plus what it finds in your repo.
45
+ - **An adversarial review.** A different model, fresh context, read-only tools,
46
+ told to refute "done". You get a pass/fail checklist citing file and line —
47
+ never a numeric self-score.
48
+ - **A tamper guard.** Deleted tests, new skips, an assertion turned into a
49
+ tautology — blocked before a reviewer token is spent.
50
+ - **Proof the fix fixed the bug.** For a bug fix, the tests offered as evidence
51
+ must fail at the merge base and pass on the new tree — the reproduction gate
52
+ enforces that, and you can require it for every change.
53
+ - **Your tests run**, locally and optionally through your CI.
54
+ - **An honest stop.** When it cannot finish, it parks with one specific question
55
+ instead of inventing a plausible diff.
56
+
57
+ ## Install
58
+
59
+ Whichever way you install, you need a **Claude credential**: an OAuth token
60
+ from `claude setup-token` (personal subscription or enterprise), so install the
61
+ Claude Code CLI first — `npm install -g @anthropic-ai/claude-code`, or
62
+ `curl -fsSL https://claude.ai/install.sh | bash`. The desktop app also calls
63
+ that CLI for every task. To pay Anthropic directly instead, set
64
+ `llm.auth_mode: "api_key"` and put your `ANTHROPIC_API_KEY` in
65
+ `~/.no_human/.env`.
66
+
67
+ ### Desktop app
68
+
69
+ [![Download for macOS](https://img.shields.io/badge/Download%20for-macOS-4C9AFF?style=for-the-badge)](https://github.com/no-human-ai/no_human/releases/latest) [![Download for Windows](https://img.shields.io/badge/Download%20for-Windows-4C9AFF?style=for-the-badge)](https://github.com/no-human-ai/no_human/releases/latest) [![Download for Linux](https://img.shields.io/badge/Download%20for-Linux-4C9AFF?style=for-the-badge)](https://github.com/no-human-ai/no_human/releases/latest)
70
+
71
+ Each release ships a SHA-256 alongside the artifact. Platform notes and the
72
+ first-run walk-through: [docs/quickstart.md](docs/quickstart.md).
73
+
74
+ ### From source
75
+
76
+ ```bash
77
+ git clone https://github.com/no-human-ai/no_human.git && cd no_human
78
+ uv sync # installs the `nh` entry point into .venv
79
+ (cd web && npm install && npm run build) # builds the board (cold first install can take minutes)
80
+ uv run nh init # token, config, first repo (about 2 minutes)
81
+ uv run nh doctor # verify the install is real before relying on it
82
+ ```
83
+
84
+ The `web` build is not optional if you want the board: a source checkout ships
85
+ no `web/dist`, so without it `nh start` serves the API only and renders no UI.
86
+ Needs Python 3.12+, [uv](https://github.com/astral-sh/uv), git, and Node with
87
+ npm for the board build.
88
+
89
+ ## Run one task
90
+
91
+ Run `nh` with no arguments for the shell: your lanes, a live event tail, and an
92
+ intake you describe a task to in plain English. Every command below still works.
93
+
94
+ ```bash
95
+ nh # the shell
96
+ nh start # board + worker on 127.0.0.1:8420
97
+ nh task add https://github.com/org/repo/issues/42 --repo ~/git/repo
98
+ nh status # needs-you / working / waiting / done
99
+ nh review <id> # the reviewer's evidence checklist
100
+ nh diff <id> # the diff it wants to ship
101
+ nh approve <id> # your approval squash-lands the PR (git.approve_identity)
102
+ nh reject <id> --reason "..." # send it back with feedback
103
+ ```
104
+
105
+ ## Integrations
106
+
107
+ Point no_human at the tracker you already use and it pulls the tickets to your
108
+ board — a tracker's filter lives in your config, never in a task's own text,
109
+ and a transport error logs and retries on the next tick instead of crashing
110
+ the pool.
111
+
112
+ | Tracker | How tickets arrive | Filter you configure |
113
+ |---|---|---|
114
+ | **Jira Cloud** | Polled via REST `search/jql` (HTTP Basic `email:token`) | `integrations.jira.jql` |
115
+ | **Linear** | Polled via the GraphQL API | `integrations.linear.team_key` + `state_types` + `label` |
116
+ | **monday.com** | Polled via GraphQL v2 | `integrations.monday.board_id` + `status_column` + `todo_labels` |
117
+
118
+ With write-back on (`write_back`, off by default), the ticket moves with the
119
+ task — matched by status category, type, or the label you name, never a
120
+ hard-coded transition id — and gets the PR link; a task that needs a human is commented on, never
121
+ transitioned. GitHub and
122
+ GitLab issues import as tasks by URL, and PRs or MRs open on your own host;
123
+ Slack and Teams get a message when a task needs you; Jenkins and CircleCI can
124
+ run your test layers and gate the loop. Setup for each:
125
+ [docs/adapters.md](docs/adapters.md).
126
+
127
+ **Watch the Jira flow end to end** — tickets synced from a Jira board, scoped,
128
+ implemented, and delivered as a review-passed pull request (click for the full
129
+ video with every step):
130
+
131
+ [![Jira flow demo](https://getnohuman.com/assets/demo-jira.gif)](https://getnohuman.com/assets/demo-jira.mp4)
132
+
133
+ <p align="center">▶️&nbsp;&nbsp;<strong><a href="https://getnohuman.com/assets/demo-jira.mp4">Play the full demo</a></strong> — 1:33, from Jira board to review-passed PR</p>
134
+
135
+ ## MCP server — hand it work from the agent you are already in
136
+
137
+ no_human ships an **MCP (Model Context Protocol) server**: a stdio bridge, built
138
+ on the official Python MCP SDK, that lets Claude Code, Cursor or any MCP client
139
+ file work with your local no_human and check on it.
140
+
141
+ ```bash
142
+ nh mcp-serve # the MCP server, over stdio
143
+ ```
144
+
145
+ Two tools, and no more:
146
+
147
+ | Tool | What it does |
148
+ |---|---|
149
+ | `task_add(title, description, repo_path)` | Files a task. no_human then plans it, writes the change, runs your tests, has a second model review it, and opens the pull request. |
150
+ | `task_status(task_id_or_external_id)` | Returns that task's current state — status, attempts, the PR link once there is one. |
151
+
152
+ It talks to your own no_human at `http://127.0.0.1:8420` and nothing else: no
153
+ auth, because that address is localhost, and no service of ours in between. For
154
+ Claude Code, the same server ships as a plugin — point it at
155
+ [`plugins/no-human/`](plugins/no-human/) and the two tools appear in your
156
+ session.
157
+
158
+ ```jsonc
159
+ // .mcp.json
160
+ { "mcpServers": { "no_human": { "command": "nh", "args": ["mcp-serve"] } } }
161
+ ```
162
+
163
+ ## Docs
164
+
165
+ | | |
166
+ |---|---|
167
+ | [quickstart.md](docs/quickstart.md) | Zero to first task, per platform |
168
+ | [configuration.md](docs/configuration.md) | Every setting and default |
169
+ | [verification.md](docs/verification.md) | The gates, the bounded loop, the limits |
170
+ | [security.md](docs/security.md) | Auth boundary, the never-merge rule, guards |
171
+ | [blockers.md](docs/blockers.md) | Escalation, wake watcher, `nh reply` |
172
+ | [adapters.md](docs/adapters.md) | Intake, context, VCS and CI backends |
173
+ | [eval.md](docs/eval.md) | Golden set, replay scoring, shadow mode |
174
+ | [CHANGELOG.md](CHANGELOG.md) | What changed, per release |
175
+
176
+ ## Development
177
+
178
+ ```bash
179
+ uv sync
180
+ uv run pytest -q
181
+ uv run nh --help
182
+ ```
183
+
184
+ Issues and pull requests welcome; run `uv run pytest -q` before submitting.
185
+
186
+ ## License
187
+
188
+ MIT — see [LICENSE](LICENSE). The licence covers the code, not the name:
189
+ [TRADEMARK.md](TRADEMARK.md) is the policy on using "no_human" and the logo.
190
+ Packaging a binary carries obligations the source tree does not, listed in
191
+ [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md).
@@ -0,0 +1,170 @@
1
+ <div align="center">
2
+
3
+ <img src="docs/assets/nh-mark.png" alt="" width="140" height="140">
4
+
5
+ # no_human
6
+
7
+ **From ticket to reviewed pull request.**<br>***Free and open-source, on your machine.***
8
+
9
+ [![latest release](https://img.shields.io/github/v/release/no-human-ai/no_human?label=release&color=4C9AFF)](https://github.com/no-human-ai/no_human/releases/latest) [![CI](https://img.shields.io/github/actions/workflow/status/no-human-ai/no_human/ci.yml?branch=main&label=CI)](https://github.com/no-human-ai/no_human/actions/workflows/ci.yml) [![python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue)](https://www.python.org/) [![license MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
10
+
11
+ [getnohuman.com](https://getnohuman.com) · [Quickstart](docs/quickstart.md) · [Docs](docs/README.md) · [Watch it work a sprint](https://getnohuman.com/demo)
12
+
13
+ [![Download for macOS](https://img.shields.io/badge/Download%20for-macOS-4C9AFF?style=for-the-badge)](https://github.com/no-human-ai/no_human/releases/latest) [![Download for Windows](https://img.shields.io/badge/Download%20for-Windows-4C9AFF?style=for-the-badge)](https://github.com/no-human-ai/no_human/releases/latest) [![Download for Linux](https://img.shields.io/badge/Download%20for-Linux-4C9AFF?style=for-the-badge)](https://github.com/no-human-ai/no_human/releases/latest)
14
+
15
+ <a href="https://getnohuman.com/"><img src="docs/assets/hero-loop-poster.jpg" alt="The no_human board: one task waiting on a question in Needs answer, four tasks working in parallel, one pull request ready for review." width="880"></a>
16
+
17
+ <sub>▶ <a href="https://getnohuman.com/">Watch the loop</a> — a ticket in, a reviewed pull request out; the whole loop in 57 seconds.</sub>
18
+
19
+ </div>
20
+
21
+ The AI coding factory you <ins>**can trust**</ins>:
22
+
23
+ - **A plan before any code**, from the ticket plus what it finds in your repo.
24
+ - **An adversarial review.** A different model, fresh context, read-only tools,
25
+ told to refute "done". You get a pass/fail checklist citing file and line —
26
+ never a numeric self-score.
27
+ - **A tamper guard.** Deleted tests, new skips, an assertion turned into a
28
+ tautology — blocked before a reviewer token is spent.
29
+ - **Proof the fix fixed the bug.** For a bug fix, the tests offered as evidence
30
+ must fail at the merge base and pass on the new tree — the reproduction gate
31
+ enforces that, and you can require it for every change.
32
+ - **Your tests run**, locally and optionally through your CI.
33
+ - **An honest stop.** When it cannot finish, it parks with one specific question
34
+ instead of inventing a plausible diff.
35
+
36
+ ## Install
37
+
38
+ Whichever way you install, you need a **Claude credential**: an OAuth token
39
+ from `claude setup-token` (personal subscription or enterprise), so install the
40
+ Claude Code CLI first — `npm install -g @anthropic-ai/claude-code`, or
41
+ `curl -fsSL https://claude.ai/install.sh | bash`. The desktop app also calls
42
+ that CLI for every task. To pay Anthropic directly instead, set
43
+ `llm.auth_mode: "api_key"` and put your `ANTHROPIC_API_KEY` in
44
+ `~/.no_human/.env`.
45
+
46
+ ### Desktop app
47
+
48
+ [![Download for macOS](https://img.shields.io/badge/Download%20for-macOS-4C9AFF?style=for-the-badge)](https://github.com/no-human-ai/no_human/releases/latest) [![Download for Windows](https://img.shields.io/badge/Download%20for-Windows-4C9AFF?style=for-the-badge)](https://github.com/no-human-ai/no_human/releases/latest) [![Download for Linux](https://img.shields.io/badge/Download%20for-Linux-4C9AFF?style=for-the-badge)](https://github.com/no-human-ai/no_human/releases/latest)
49
+
50
+ Each release ships a SHA-256 alongside the artifact. Platform notes and the
51
+ first-run walk-through: [docs/quickstart.md](docs/quickstart.md).
52
+
53
+ ### From source
54
+
55
+ ```bash
56
+ git clone https://github.com/no-human-ai/no_human.git && cd no_human
57
+ uv sync # installs the `nh` entry point into .venv
58
+ (cd web && npm install && npm run build) # builds the board (cold first install can take minutes)
59
+ uv run nh init # token, config, first repo (about 2 minutes)
60
+ uv run nh doctor # verify the install is real before relying on it
61
+ ```
62
+
63
+ The `web` build is not optional if you want the board: a source checkout ships
64
+ no `web/dist`, so without it `nh start` serves the API only and renders no UI.
65
+ Needs Python 3.12+, [uv](https://github.com/astral-sh/uv), git, and Node with
66
+ npm for the board build.
67
+
68
+ ## Run one task
69
+
70
+ Run `nh` with no arguments for the shell: your lanes, a live event tail, and an
71
+ intake you describe a task to in plain English. Every command below still works.
72
+
73
+ ```bash
74
+ nh # the shell
75
+ nh start # board + worker on 127.0.0.1:8420
76
+ nh task add https://github.com/org/repo/issues/42 --repo ~/git/repo
77
+ nh status # needs-you / working / waiting / done
78
+ nh review <id> # the reviewer's evidence checklist
79
+ nh diff <id> # the diff it wants to ship
80
+ nh approve <id> # your approval squash-lands the PR (git.approve_identity)
81
+ nh reject <id> --reason "..." # send it back with feedback
82
+ ```
83
+
84
+ ## Integrations
85
+
86
+ Point no_human at the tracker you already use and it pulls the tickets to your
87
+ board — a tracker's filter lives in your config, never in a task's own text,
88
+ and a transport error logs and retries on the next tick instead of crashing
89
+ the pool.
90
+
91
+ | Tracker | How tickets arrive | Filter you configure |
92
+ |---|---|---|
93
+ | **Jira Cloud** | Polled via REST `search/jql` (HTTP Basic `email:token`) | `integrations.jira.jql` |
94
+ | **Linear** | Polled via the GraphQL API | `integrations.linear.team_key` + `state_types` + `label` |
95
+ | **monday.com** | Polled via GraphQL v2 | `integrations.monday.board_id` + `status_column` + `todo_labels` |
96
+
97
+ With write-back on (`write_back`, off by default), the ticket moves with the
98
+ task — matched by status category, type, or the label you name, never a
99
+ hard-coded transition id — and gets the PR link; a task that needs a human is commented on, never
100
+ transitioned. GitHub and
101
+ GitLab issues import as tasks by URL, and PRs or MRs open on your own host;
102
+ Slack and Teams get a message when a task needs you; Jenkins and CircleCI can
103
+ run your test layers and gate the loop. Setup for each:
104
+ [docs/adapters.md](docs/adapters.md).
105
+
106
+ **Watch the Jira flow end to end** — tickets synced from a Jira board, scoped,
107
+ implemented, and delivered as a review-passed pull request (click for the full
108
+ video with every step):
109
+
110
+ [![Jira flow demo](https://getnohuman.com/assets/demo-jira.gif)](https://getnohuman.com/assets/demo-jira.mp4)
111
+
112
+ <p align="center">▶️&nbsp;&nbsp;<strong><a href="https://getnohuman.com/assets/demo-jira.mp4">Play the full demo</a></strong> — 1:33, from Jira board to review-passed PR</p>
113
+
114
+ ## MCP server — hand it work from the agent you are already in
115
+
116
+ no_human ships an **MCP (Model Context Protocol) server**: a stdio bridge, built
117
+ on the official Python MCP SDK, that lets Claude Code, Cursor or any MCP client
118
+ file work with your local no_human and check on it.
119
+
120
+ ```bash
121
+ nh mcp-serve # the MCP server, over stdio
122
+ ```
123
+
124
+ Two tools, and no more:
125
+
126
+ | Tool | What it does |
127
+ |---|---|
128
+ | `task_add(title, description, repo_path)` | Files a task. no_human then plans it, writes the change, runs your tests, has a second model review it, and opens the pull request. |
129
+ | `task_status(task_id_or_external_id)` | Returns that task's current state — status, attempts, the PR link once there is one. |
130
+
131
+ It talks to your own no_human at `http://127.0.0.1:8420` and nothing else: no
132
+ auth, because that address is localhost, and no service of ours in between. For
133
+ Claude Code, the same server ships as a plugin — point it at
134
+ [`plugins/no-human/`](plugins/no-human/) and the two tools appear in your
135
+ session.
136
+
137
+ ```jsonc
138
+ // .mcp.json
139
+ { "mcpServers": { "no_human": { "command": "nh", "args": ["mcp-serve"] } } }
140
+ ```
141
+
142
+ ## Docs
143
+
144
+ | | |
145
+ |---|---|
146
+ | [quickstart.md](docs/quickstart.md) | Zero to first task, per platform |
147
+ | [configuration.md](docs/configuration.md) | Every setting and default |
148
+ | [verification.md](docs/verification.md) | The gates, the bounded loop, the limits |
149
+ | [security.md](docs/security.md) | Auth boundary, the never-merge rule, guards |
150
+ | [blockers.md](docs/blockers.md) | Escalation, wake watcher, `nh reply` |
151
+ | [adapters.md](docs/adapters.md) | Intake, context, VCS and CI backends |
152
+ | [eval.md](docs/eval.md) | Golden set, replay scoring, shadow mode |
153
+ | [CHANGELOG.md](CHANGELOG.md) | What changed, per release |
154
+
155
+ ## Development
156
+
157
+ ```bash
158
+ uv sync
159
+ uv run pytest -q
160
+ uv run nh --help
161
+ ```
162
+
163
+ Issues and pull requests welcome; run `uv run pytest -q` before submitting.
164
+
165
+ ## License
166
+
167
+ MIT — see [LICENSE](LICENSE). The licence covers the code, not the name:
168
+ [TRADEMARK.md](TRADEMARK.md) is the policy on using "no_human" and the logo.
169
+ Packaging a binary carries obligations the source tree does not, listed in
170
+ [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md).
@@ -0,0 +1,213 @@
1
+ """The board is REQUIRED for a distributable build and OPTIONAL for a dev one.
2
+
3
+ `web/dist` is the built React board. `nh start` — the entrypoint README calls
4
+ primary — serves it, so a wheel or sdist built without it installs a CLI whose
5
+ main command renders nothing. That shipped once, and the fix was to declare the
6
+ board with hatchling's `force-include`, which raises `Forced include not found`
7
+ when the source is absent: a release cut without `npm run build` FAILS instead
8
+ of quietly shipping the broken product.
9
+
10
+ That guarantee was right; the mechanism was too wide. `force-include` is static
11
+ configuration, so it is evaluated for EVERY build target and version — including
12
+ the editable wheel that `uv sync` and `pip install -e .` build. `web/dist` is a
13
+ gitignored build artifact, so a clean clone of this repo does not have one, and
14
+ the release-time guard fired on the developer's first command instead:
15
+
16
+ $ git clone … && cd no_human && uv sync
17
+ FileNotFoundError: Forced include not found: <repo>/web/dist
18
+ Call to `hatchling.build.build_editable` failed (exit status: 1)
19
+
20
+ A clean clone could not be installed, developed, or tested — before collection,
21
+ with a message that reads like a broken repository rather than a missing npm
22
+ build. (The comment this replaces claimed "nothing in CI builds a wheel, so this
23
+ only ever fires on a release build". That was false: `uv sync` reaches the same
24
+ code path through `build_editable`.)
25
+
26
+ So the declaration moves here, where the build VERSION is visible. Hatchling
27
+ calls `initialize(version, build_data)` with `version == "editable"` for an
28
+ editable install and `"standard"` for everything else, and honours anything a
29
+ hook adds to `build_data["force_include"]` (merged in `BuilderConfig.
30
+ set_build_data`, read by both `build_standard` and `build_editable_detection`).
31
+ That is the whole seam:
32
+
33
+ * editable + no board -> build it automatically when `npm` is on PATH ("cd
34
+ web && npm install && npm run build"). If npm is absent, the build fails,
35
+ or it exits 0 without producing `index.html`, fall back to warning and
36
+ include nothing — the install still succeeds either way.
37
+ * editable + board -> include it, exactly as before.
38
+ * standard + no board -> FAIL, with a message that names `npm run build` and
39
+ the directory. The release-time guarantee is unchanged; only its blast
40
+ radius is.
41
+ * standard + board -> include it, exactly as before.
42
+
43
+ Presence is judged by `index.html`, not by the directory. `npm run build` writes
44
+ INTO an existing directory, so an interrupted or cleaned build leaves the husk
45
+ behind — and the old directory-existence check would have force-included that
46
+ husk into a release wheel, which is the boardless-CLI bug wearing a directory.
47
+ `api/app.py::_resolve_web_dist` already refuses a husk for the same reason.
48
+
49
+ `migrations/` stays a static `force-include` in `pyproject.toml`: it is TRACKED,
50
+ so it is present in every clone and worktree, and a static declaration is the
51
+ simpler thing that works.
52
+
53
+ The hatchling import is guarded so this module can be imported as a plain script
54
+ by `tests/test_clean_clone_installable.py`. Hatchling is a BUILD dependency — it
55
+ exists only inside the isolated build environment PEP 517 creates, never in this
56
+ repo's own venv — so an unguarded import would make the decision logic below
57
+ untestable anywhere except inside a real build, which is the slowest and least
58
+ specific place to test it.
59
+ """
60
+
61
+ from __future__ import annotations
62
+
63
+ import shutil
64
+ import subprocess
65
+ from collections.abc import Callable
66
+ from pathlib import Path
67
+
68
+ try: # pragma: no cover - the real build environment always has hatchling
69
+ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
70
+ except ModuleNotFoundError: # pragma: no cover - importing this file as a module
71
+ BuildHookInterface = object # type: ignore[assignment,misc]
72
+
73
+ # The version string hatchling passes for `pip install -e .` / `uv sync`.
74
+ EDITABLE = "editable"
75
+
76
+ DEFAULT_SOURCE = "web/dist"
77
+
78
+ NPM = "npm"
79
+
80
+ # `npm install` on a cold `node_modules` can take a while; this bounds an
81
+ # editable install rather than hanging it forever.
82
+ NPM_TIMEOUT = 900
83
+
84
+
85
+ class BoardNotBuiltError(RuntimeError):
86
+ """A distributable artifact was built without a board to put in it."""
87
+
88
+
89
+ def _build_it() -> str:
90
+ return "cd web && npm install && npm run build"
91
+
92
+
93
+ def _npm_available() -> bool:
94
+ return shutil.which(NPM) is not None
95
+
96
+
97
+ def try_build_board(
98
+ root: str | Path,
99
+ source: str,
100
+ runner: Callable[..., subprocess.CompletedProcess] = subprocess.run,
101
+ ) -> bool:
102
+ """Attempt the editable-install board build named by `_build_it()`.
103
+
104
+ Runs `npm install` then `npm run build` in `<root>/<web dir>` (the parent
105
+ of `source`) and returns whether `<root>/<source>/index.html` exists
106
+ afterwards. Never raises: an absent `npm`, a missing `web/package.json`
107
+ (not a real checkout — e.g. a test's bare `tmp_path`), a non-zero exit, a
108
+ crash, or a timeout all just return `False` so the caller can fall back to
109
+ `board_missing_warning`. A 0 exit with no `index.html` is also `False` —
110
+ npm can exit 0 having produced an incomplete build.
111
+ """
112
+ web_dir = Path(root) / Path(source).parent
113
+ if not (web_dir / "package.json").is_file():
114
+ return False
115
+ if not _npm_available():
116
+ return False
117
+ try:
118
+ for argv in (["npm", "install"], ["npm", "run", "build"]):
119
+ result = runner(
120
+ argv, cwd=web_dir, capture_output=True, text=True,
121
+ check=False, timeout=NPM_TIMEOUT,
122
+ )
123
+ if result.returncode != 0:
124
+ return False
125
+ except (OSError, subprocess.SubprocessError):
126
+ return False
127
+ return (Path(root) / source / "index.html").is_file()
128
+
129
+
130
+ def board_missing_warning(index: Path) -> str:
131
+ return (
132
+ f"\nno_human: the web board is not built — {index} is missing.\n"
133
+ f" This dev install will work, but `nh start` will serve a notice\n"
134
+ f" instead of the board until you run:\n"
135
+ f" {_build_it()}\n"
136
+ f" (The board is a gitignored build artifact, so a fresh clone never\n"
137
+ f" has one. The CLI — `nh task`, `nh status` — is unaffected.)\n"
138
+ )
139
+
140
+
141
+ def board_missing_error(index: Path, target_name: str) -> str:
142
+ return (
143
+ f"\nno_human: refusing to build a {target_name} without the web board.\n"
144
+ f"\n"
145
+ f" missing: {index}\n"
146
+ f"\n"
147
+ f" `nh start` is the documented entrypoint and it serves this board, so\n"
148
+ f" a {target_name} built without it installs a CLI whose main command\n"
149
+ f" renders nothing. That has shipped once already, which is why this is\n"
150
+ f" a build failure and not a warning.\n"
151
+ f"\n"
152
+ f" Build the board first:\n"
153
+ f" {_build_it()}\n"
154
+ f"\n"
155
+ f" A dev install does NOT need it: `uv sync` and `pip install -e .`\n"
156
+ f" warn and continue.\n"
157
+ )
158
+
159
+
160
+ def plan_board_inclusion(
161
+ root: str | Path,
162
+ source: str,
163
+ target: str,
164
+ version: str,
165
+ target_name: str = "wheel",
166
+ *,
167
+ builder: Callable[[str | Path, str], bool] | None = None,
168
+ ) -> tuple[dict[str, str], str | None]:
169
+ """Decide what a build of `version` does about the board.
170
+
171
+ Returns `(force_include_additions, warning_or_None)` and raises
172
+ `BoardNotBuiltError` when a distributable build has no board to ship. Split
173
+ out of the hook class so it can be tested without hatchling and without
174
+ running a build — the branch that matters (editable vs standard) is one
175
+ string comparison, and it should be provable in milliseconds.
176
+
177
+ `builder` is test-injectable and only ever consulted on the editable path
178
+ (default `try_build_board`); the standard/distributable path never calls
179
+ it and always fails closed when the board is absent.
180
+ """
181
+ index = Path(root) / source / "index.html"
182
+ if index.is_file():
183
+ return {source: target}, None
184
+ if version == EDITABLE:
185
+ build = builder or try_build_board
186
+ if build(root, source) and index.is_file():
187
+ return {source: target}, None
188
+ return {}, board_missing_warning(index)
189
+ raise BoardNotBuiltError(board_missing_error(index, target_name))
190
+
191
+
192
+ class BoardBuildHook(BuildHookInterface): # type: ignore[misc,valid-type]
193
+ """Injects the board's `force-include` entry, or explains its absence."""
194
+
195
+ PLUGIN_NAME = "no-human-board"
196
+
197
+ def initialize(self, version: str, build_data: dict) -> None:
198
+ source = self.config.get("source", DEFAULT_SOURCE)
199
+ target = self.config.get("target")
200
+ if not isinstance(target, str) or not target:
201
+ msg = (
202
+ "hatch_build.py: the hook needs a `target` in "
203
+ f"[tool.hatch.build.targets.{self.target_name}.hooks.custom] — "
204
+ "the path the board takes inside the artifact."
205
+ )
206
+ raise ValueError(msg)
207
+
208
+ additions, warning = plan_board_inclusion(
209
+ self.root, source, target, version, self.target_name
210
+ )
211
+ if warning:
212
+ self.app.display_warning(warning)
213
+ build_data["force_include"].update(additions)