Repository navigation
Expand file tree
/
Copy pathpyproject.toml
More file actions
379 lines (354 loc) · 16.5 KB
/
Copy pathpyproject.toml
File metadata and controls
379 lines (354 loc) · 16.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
[project]
# The DISTRIBUTION name, which is not the import package (`grove`) or the
# command (`grove`). `grove` on PyPI is an unrelated log-collection framework
# we do not own, so publishing under it is impossible and a bare
# `uv tool install grove` fetches the wrong product entirely.
name = "grove-factory"
version = "0.0.12"
description = "Run your coding agents as a software factory, each task in its own workspace and tracked from ticket to trace."
# Built from README.md by the fancy-pypi-readme hook at the end of this file.
dynamic = ["readme"]
license = { text = "MIT" }
authors = [{ name = "Krishnakanth Alagiri" }]
requires-python = ">=3.12"
keywords = ["git", "worktree", "tmux", "tui", "textual", "ai", "agents"]
classifiers = [
"Development Status :: 3 - Alpha",
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: POSIX :: Linux",
"Operating System :: MacOS",
"Operating System :: Microsoft :: Windows",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Version Control :: Git",
"Topic :: Terminals",
]
dependencies = [
"pydantic>=2.7,<3",
"platformdirs>=4.0,<5",
"libtmux>=0.55,<0.56",
"textual>=0.80,<9",
"typer>=0.12,<1",
"humanize>=4.9,<5",
"loguru>=0.7,<1",
"httpx>=0.28.1,<1",
"watchfiles>=1.1,<2",
# A real bash grammar, for attributing a shell tool call's time to the
# executable that led it. Pure Python, no extension module. Measured over
# 111,578 real commands: naive first-token attribution is ~31% correct and
# a careful `shlex` segmentation ~79% while never signalling failure, where
# bashlex is effectively 100% correct wherever it parses and raises
# loudly otherwise. `grove.core.usage._command` still imports it
# defensively and degrades to its own scan, so an older install keeps
# working rather than failing to import `grove.core`.
"bashlex>=0.18,<1",
# Verify TLS against the OS trust store instead of the venv's certifi
# bundle. Grove talks to a user's OWN forge, gateway and collector, which
# are exactly the endpoints a public root bundle cannot verify — and the
# symptom is never "TLS failed" but "these tickets have no titles". Pure
# Python, no dependencies of its own; `grove._truststore` still imports it
# defensively so an older install keeps working rather than failing to
# start.
"truststore>=0.9,<1",
# ── Every surface ships in the base install ──────────────────────────────
# A plain `uv tool install grove-factory` (or `uvx grove-factory`) must
# give the daemon, the MCP server, the SSH client and telemetry with no
# extra to remember: an extra left off is a surface that fails on first
# use, far from the install that caused it. Python packaging cannot
# SUBTRACT a dependency, so there is no lean install from the index any
# more.
#
# web dashboard: Node itself, from PyPI, so `grove web` and the dashboard
# build never depend on whatever Node the host has (usually an nvm default
# older than Next 16's floor). This is the established way to ship Node
# through PyPI (basedpyright hard-depends on the same binaries). The
# `nodejs-wheel` wrapper, not the bare `-binaries`, because only it
# installs working `node`/`npm`/`npx` console scripts; the binaries' own
# `bin/npm` is a copied symlink that cannot resolve its module. Per-platform
# wheels, ~60 MB. Capped so a Node major lands deliberately.
"nodejs-wheel>=22,<25",
# daemon
"fastapi>=0.115,<1",
"uvicorn[standard]>=0.30,<1",
# client (the SSH transport)
"asyncssh>=2.14,<3",
# MCP. Capped <2: SDK 2.0.0 deleted mcp.server.fastmcp entirely (Grove's
# whole MCP surface is built on FastMCP + mcp.server.lowlevel.Server), so
# an uncapped install silently shipped a dead grove-mcp.
"mcp>=1.2,<2",
# telemetry: LangFuse ingestion via raw OTLP — deliberately NOT the
# `langfuse` SDK package (its `start_observation()` has no `start_time`
# param, so it can't backfill a session that already happened). Inert
# until `TelemetryConfig.enabled`; `grove.core.trace` imports it lazily.
"opentelemetry-sdk>=1.27,<2",
"opentelemetry-exporter-otlp-proto-http>=1.27,<2",
# Otherwise arrives only transitively via opentelemetry-sdk; declared
# explicitly because tests/core/test_telemetry_semconv.py imports its
# `_incubating` GenAI attribute names directly to prove semconv.py's own
# literals haven't drifted. Capped on its own 0.xbN line rather than the
# 1.x line above: this package is the pre-stable convention distribution,
# and its `_incubating` path is expected to move.
"opentelemetry-semantic-conventions>=0.64b0,<1",
]
# Empty on purpose: every one of these is now in the base install. They stay
# declared so the `grove-factory[all]` / `[daemon]` specs already written into
# docs, installers and people's shell history keep resolving instead of
# warning about an unknown extra.
[project.optional-dependencies]
daemon = []
client = []
mcp = []
telemetry = []
all = []
[project.scripts]
grove = "grove.cli:app"
# Dedicated hot-path entry: Claude spawns one process per hook event per
# session, so this must not import the Typer/FastAPI stack.
grove-agent-hook = "grove.hook_producer:run_hook_from_stdin"
grove-mcp = "grove.mcp.server:main"
grove-native-worker = "grove.core.native_worker:main"
[project.urls]
Homepage = "https://github.com/bearlike/Grove"
Repository = "https://github.com/bearlike/Grove"
Documentation = "https://factory.mewbo.com/"
[build-system]
requires = ["hatchling", "hatch-fancy-pypi-readme>=24"]
build-backend = "hatchling.build"
# PyPI renders the README but hosts none of the repository's files, so every
# relative image and link in it resolves against pypi.org and breaks. The repo
# README stays relative so each forge renders its own tree; only the copy baked
# into the package metadata is made absolute, against the public branch.
[tool.hatch.metadata.hooks.fancy-pypi-readme]
content-type = "text/markdown"
fragments = [{ path = "README.md" }]
[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]]
pattern = 'src="(?!https?://)(?:\./)?([^"]+)"'
replacement = 'src="https://raw.githubusercontent.com/bearlike/Grove/current/\1"'
[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]]
pattern = '(href="|\]\()(?!https?://|#|mailto:)(?:\./)?([^")]+)'
replacement = '\1https://github.com/bearlike/Grove/blob/current/\2'
[tool.hatch.build.targets.wheel]
packages = ["src/grove"]
# The prebuilt web dashboard (`make webapp-bundle`) is gitignored build output,
# which hatch skips by default; this puts it back into the wheel. Absent from a
# plain checkout, so a wheel built without the bundle step simply has no
# webapp and `grove web` says how to get one.
artifacts = ["src/grove/_webapp/**"]
# The sdist is what `pip install` falls back to when no wheel fits, so it needs
# the package and nothing else. Hatch's default is the whole tracked tree —
# docs videos, screenshots, the webapp sources — measured at 152 MB, past
# PyPI's 100 MB per-file limit, so the upload itself would be refused.
[tool.hatch.build.targets.sdist]
only-include = ["src/grove", "README.md", "LICENSE", "pyproject.toml"]
exclude = ["src/grove/_webapp"]
[dependency-groups]
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.23",
"ruff>=0.6",
"mypy>=1.11,<2.0",
"import-linter>=2.0",
"textual-dev>=1.5",
# Out-of-process surfaces — duplicated from the base install so the dev
# group alone stays a complete test environment.
"fastapi>=0.115,<1",
"uvicorn[standard]>=0.30,<1",
"httpx>=0.27,<1",
"asyncssh>=2.14,<3",
"mcp>=1.2,<2",
"opentelemetry-sdk>=1.27,<2",
"opentelemetry-exporter-otlp-proto-http>=1.27,<2",
# tools/screenshots/frame.py composites the doc screenshots onto a 16:9
# frame. It reads as a docs concern, but `tests/tools/screenshots/` imports
# it, so it belongs to whatever group the TEST job installs. Keeping it
# under `docs` is what forced the gates to sync that group at all. Never a
# runtime dependency: nothing under src/grove imports Pillow.
"Pillow>=11,<12",
# tests/test_ci_components.py parses the workflow files. Declared rather
# than inherited, because it only arrived through uvicorn[standard].
"pyyaml>=6,<7",
]
docs = [
"mike",
"mkdocs",
"mkdocs-callouts>=1.16.0",
# Brand-fork of mkdocs-shadcn — same `theme: shadcn` entrypoint plus the
# brand chrome (Ask AI, sidebar nav-icons, prefetch, ToC rail, search
# modal, full SEO). Since v1.2 the brand identity (tab title, Ask AI
# greeting, WebMCP tool name) derives from `site_name`, so Grove ships no
# template overrides.
#
# Unversioned on purpose — resolved from the flat index declared under
# `[[tool.uv.index]]` below, so the theme tracks its own latest release
# and no version appears here to go stale.
"mkdocs-shadcn-mewbo",
"mkdocstrings[python]",
"pymdown-extensions>=10.20.1",
]
# ─────────────────────────────────────────────────────────────────────────────
# Tooling
# ─────────────────────────────────────────────────────────────────────────────
# The docs theme is published only to GitHub Releases, never to PyPI, so it
# needs a package index that is not PyPI. GitHub does not serve a PEP 503
# index, but `/releases/expanded_assets/<tag>` returns exactly one flat page of
# `<a href>` links to that release's assets — which is what `format = "flat"`
# (a PEP 508 "find-links" page) consumes. Pointing it at the moving
# `latest-master` tag makes uv RESOLVE the version instead of us naming one:
# the page is re-rendered per request, so a new theme release is picked up by
# the next resolution with nothing to edit here.
#
# This is why the old "never pin latest-master" rule no longer applies. That
# rule was about a direct URL dependency, where the wheel FILENAME carries the
# version and 404s the moment the theme ships a release. An index is read, not
# fetched by name, so the rotating filename is the mechanism rather than the
# hazard.
#
# `explicit = true` confines this index to the one package named in
# `[tool.uv.sources]`; without it every dependency in the tree would be allowed
# to resolve from a GitHub release page.
#
# Why `latest-master` and not the newest v* TAG, which is the more obvious
# reading of "latest release": resolving a tag name means an api.github.com
# call, and that API answers 403 to unauthenticated requests. The docs job runs
# on BOTH mirrors, and the internal Gitea runner holds no github.com token —
# measured 2026-09-16, the API returned 403 anonymously while this release page
# returned 200. A static URL that needs no credential is the only mechanism
# that works on both. The cost, per the theme's own release.yaml: this tracks
# master's HEAD, so between a tag and the next one it can serve commits that
# are newer than any tagged release while still carrying the tagged version
# number. We own that repo and its master is the release branch, so that is
# acceptable here; it would not be for a third-party dependency.
#
# The trade-off, accepted deliberately: this gives up a pinned, immutable
# dependency. `uv.lock` still pins the resolved version, so ordinary builds
# stay reproducible — but the lock is refreshed on every docs CI run (see
# `.github/workflows/docs.yml`), so a docs build is reproducible only against
# whatever the theme's latest release was at that moment. That is the intended
# behavior: we own both repos, a theme release is meant to reach the docs
# site without a bump commit, and the docs site is rebuilt from scratch every
# time. Do NOT extend this pattern to a runtime dependency of the `grove`
# package itself, where a shipped wheel must resolve identically forever.
[[tool.uv.index]]
name = "mkdocs-shadcn-releases"
url = "https://github.com/mewbo-com/mkdocs-shadcn/releases/expanded_assets/latest-master"
format = "flat"
explicit = true
[tool.uv.sources]
mkdocs-shadcn-mewbo = { index = "mkdocs-shadcn-releases" }
[tool.ruff]
target-version = "py312"
line-length = 100
src = ["src", "tests"]
[tool.ruff.lint]
select = [
"E", "W", # pycodestyle
"F", # pyflakes
"I", # isort
"B", # bugbear
"C4", # comprehensions
"UP", # pyupgrade
"ARG", # unused arguments
"SIM", # simplify
"TID", # tidy imports
"PL", # pylint subset
"RUF", # ruff-specific
]
ignore = [
"PLR0913", # too many arguments — Textual + manager APIs need them
"PLR2004", # magic numbers in tests are fine
]
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["ARG", "PLR2004", "S101"]
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_ignores = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
warn_redundant_casts = true
files = ["src/grove"]
plugins = ["pydantic.mypy"]
[[tool.mypy.overrides]]
module = ["libtmux.*", "textual.*", "humanize.*"]
ignore_missing_imports = true
[tool.pytest.ini_options]
testpaths = ["tests"]
markers = [
"integration: real tmux + git subprocesses; opt-in via -m integration",
"requires_tmux: skipped on platforms without tmux (Windows-native CI)",
]
asyncio_mode = "auto"
# ─────────────────────────────────────────────────────────────────────────────
# Architectural boundary: grove.core must not import UI layers, and (when
# we add a second client) sibling clients must not import each other.
# Enforced by import-linter; CI fails on violation.
#
# Why import-linter, not just ruff TID251: ruff's banned-api covers a
# single-forbidden-list contract today, but the multi-client architecture
# we've explicitly committed to wants `independence` and `layers` contracts,
# which import-linter expresses cleanly and ruff does not.
# ─────────────────────────────────────────────────────────────────────────────
[tool.importlinter]
root_packages = ["grove"]
include_external_packages = true
[[tool.importlinter.contracts]]
name = "Core has no UI dependencies"
type = "forbidden"
source_modules = ["grove.core"]
forbidden_modules = [
"textual",
"rich",
"typer",
"click",
"grove.tui",
]
# Daemon is the in-process HTTP wrapper around grove.core. It MUST NOT
# import any client-facing module — the dependency direction is
# clients → daemon → core, never the reverse. Catches accidental
# UI/SDK leakage into the server process at CI time.
[[tool.importlinter.contracts]]
name = "Daemon depends only on grove.core"
type = "forbidden"
source_modules = ["grove.daemon"]
forbidden_modules = [
"grove.client",
"grove.tui",
]
# The client SDK is the public boundary every other client speaks. It
# talks to grove.core.contracts (wire shapes) and to the daemon over
# HTTP — it must not reach into the daemon's process internals nor
# pull in any presentation layer.
[[tool.importlinter.contracts]]
name = "Client SDK does not import daemon or tui"
type = "forbidden"
source_modules = ["grove.client"]
forbidden_modules = [
"grove.daemon",
"grove.tui",
]
# The MCP server speaks ONLY through the client SDK (issues #1/#6):
# MCP client → grove.mcp → GroveClient → daemon REST → core. Direct
# imports of engine internals would bypass the daemon's single authority.
# allow_indirect_imports because GroveClient/contracts legitimately reach
# core modules underneath — the rule binds grove.mcp's own imports.
[[tool.importlinter.contracts]]
name = "MCP server speaks only through the client SDK"
type = "forbidden"
source_modules = ["grove.mcp"]
allow_indirect_imports = "true"
forbidden_modules = [
"grove.daemon",
"grove.tui",
"grove.core.activity",
"grove.core.auth",
"grove.core.config",
"grove.core.git",
"grove.core.manager",
"grove.core.registry",
"grove.core.store",
"grove.core.tmux",
"grove.core.workspace",
]