magic-beans 0.1.0__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.
@@ -0,0 +1,185 @@
1
+ Metadata-Version: 2.4
2
+ Name: magic-beans
3
+ Version: 0.1.0
4
+ Summary: Graph-based issue tracker for AI agent coordination
5
+ Author: Henrique Bastos
6
+ Author-email: Henrique Bastos <henrique@bastos.net>
7
+ License-Expression: MIT
8
+ Requires-Dist: pydantic>=2.0
9
+ Requires-Dist: typer>=0.9
10
+ Requires-Python: >=3.14
11
+ Description-Content-Type: text/markdown
12
+
13
+ # 🫘 Beans
14
+
15
+ **Graph-based issue tracker for AI agent coordination.**
16
+
17
+ Coding with AI makes developers absurdly productive. Coffee keeps them going.
18
+ Beans keep the whole loop fed. ☕
19
+
20
+ Beans is a lightweight, embedded issue tracker designed for AI agents to coordinate work
21
+ across tasks. It models issues as nodes in a dependency graph, backed by SQLite, with a
22
+ CLI that both humans and agents can use.
23
+
24
+ ```
25
+ $ beans create "Fix auth middleware"
26
+ bean-a3f2dd1c 2025-06-15 10:42 Fix auth middleware
27
+
28
+ $ beans list
29
+ bean-a3f2dd1c 2025-06-15 10:42 Fix auth middleware
30
+ bean-7e2b9f01 2025-06-15 10:43 Add rate limiting
31
+
32
+ $ beans --json list
33
+ [{"id": "bean-a3f2dd1c", "title": "Fix auth middleware", "status": "open", ...}]
34
+ ```
35
+
36
+ ## Why Beans exists
37
+
38
+ Beans was born from analyzing [beads](https://github.com/steveyegge/beads), a
39
+ graph-based issue tracker with a similar goal: giving AI agents a structured way to
40
+ coordinate work. The idea is excellent. The execution, however, raised serious concerns.
41
+
42
+ ### The problem with beads
43
+
44
+ Beads is invasive software that assumes too much about the user's intentions:
45
+
46
+ - **Curl-pipe-bash installer** that silently escalates to `sudo` to install a ~200MB
47
+ binary system-wide
48
+ - **Strips macOS code signatures** and applies ad-hoc signatures to bypass Gatekeeper —
49
+ the same technique used by actual malware
50
+ - **Installs 5 persistent git hooks** (`pre-commit`, `post-merge`, `pre-push`,
51
+ `post-checkout`, `prepare-commit-msg`) that run on nearly every git operation
52
+ - **Runs a background database daemon** (Dolt SQL server) that binds to TCP ports
53
+ 3307/3308 — a MySQL-protocol server running on your machine
54
+ - **Silently modifies commit messages** by appending metadata trailers without asking
55
+ - **Auto-pushes data** to remote servers every 5 minutes
56
+ - **Fingerprints your machine** with unique UUIDs for the repo, clone, and project
57
+ - **Injects system prompts** into AI agent configuration files (e.g.,
58
+ `.claude/settings.local.json`) to force agents to use the tool
59
+ - **Includes a "stealth mode"** that hides its presence from collaborators
60
+ - **Ships with telemetry** that records every command and all arguments, including who
61
+ ran them
62
+
63
+ The core data model is a 50+ field struct covering everything from agent heartbeats to
64
+ ephemeral "wisps." It's not a simple tool — it's a complex system that assumes you want
65
+ all of it.
66
+
67
+ ### What beans does differently
68
+
69
+ Beans keeps the good idea — a dependency graph for AI agent coordination — and throws
70
+ away everything else:
71
+
72
+ | | Beads | Beans |
73
+ |---|---|---|
74
+ | **Storage** | Dolt SQL server (~200MB, background daemon, TCP ports) | SQLite (zero-config, embedded, stdlib) |
75
+ | **Installation** | `curl \| bash` + `sudo` + signature stripping | `uv add beans` or `pip install beans` |
76
+ | **Git hooks** | 5 persistent hooks installed silently | None |
77
+ | **Commit modification** | Silent trailer injection | Never touches your commits |
78
+ | **Background processes** | Persistent MySQL-protocol daemon | None |
79
+ | **Network** | Opens TCP ports, auto-pushes every 5 min | Fully offline |
80
+ | **Telemetry** | OTel spans with actor identity + full args | None |
81
+ | **Stealth mode** | Yes, hides from collaborators | No — transparency is a feature |
82
+ | **AI config injection** | Writes to `.claude/settings.local.json` | Provides recipes you copy yourself |
83
+ | **Data model** | 50+ fields | ~10 fields + dependency edges |
84
+ | **Agent integration** | Forced via injected prompts | Opt-in via `AGENTS.md` instructions |
85
+
86
+ ## Design principles
87
+
88
+ **Polite software.** Beans never modifies files without asking, never installs hooks,
89
+ never runs background processes, and never phones home. It's a CLI tool that reads and
90
+ writes to a local SQLite file. That's it.
91
+
92
+ **Embedded storage.** A single `.beans/beans.db` SQLite file with WAL mode. No servers,
93
+ no ports, no daemons. Works offline, works in CI, works anywhere Python runs.
94
+
95
+ **Graph-native.** Issues (beans) are nodes. Dependencies are typed edges. "What's ready
96
+ to work on?" is a graph query — show me all open beans with no open blockers.
97
+
98
+ **Agent-friendly.** The `--json` flag on every command makes beans machine-readable.
99
+ Agents don't need to parse human output. The CLI is a thin wrapper around a clean Python
100
+ API — agents can also use the library directly.
101
+
102
+ **Minimal by default.** A bean has an id, title, status, and timestamps. Everything else
103
+ is optional. No 50-field structs, no agent heartbeat tracking, no ephemeral wisps.
104
+
105
+ **Journal-based sync.** Changes are recorded in an append-only JSONL journal that can be
106
+ committed to git. The SQLite database is a materialized view that can be rebuilt from the
107
+ journal at any time. Sync through git, not through custom protocols.
108
+
109
+ ## Installation
110
+
111
+ ```bash
112
+ uv add beans
113
+ ```
114
+
115
+ Or with pip:
116
+
117
+ ```bash
118
+ pip install beans
119
+ ```
120
+
121
+ ## Quick start
122
+
123
+ ```bash
124
+ # Create some beans
125
+ beans create "Set up database schema"
126
+ beans create "Build API endpoints"
127
+ beans create "Write integration tests"
128
+
129
+ # List all beans
130
+ beans list
131
+
132
+ # JSON output for agent consumption
133
+ beans --json list
134
+ ```
135
+
136
+ ## Architecture
137
+
138
+ ```
139
+ src/beans/
140
+ ├── models.py # Pydantic models (pure, no I/O)
141
+ ├── store.py # SQLite storage (I/O boundary)
142
+ └── cli.py # Typer CLI (thin wiring layer)
143
+ ```
144
+
145
+ - **models.py** — Pure data. Bean is a Pydantic model with validation. No I/O, no side
146
+ effects, easy to test.
147
+ - **store.py** — The I/O boundary. BeanStore wraps a SQLite connection with
148
+ create/read/update/delete operations. Accepts an injected connection for testing.
149
+ - **cli.py** — Thin wiring. Parses args, calls store methods, formats output. No
150
+ business logic.
151
+
152
+ ## For AI agents
153
+
154
+ Beans is designed to be used by AI agents as a coordination mechanism. Add this to your
155
+ project's `AGENTS.md`:
156
+
157
+ ```markdown
158
+ ## Task tracking
159
+
160
+ This project uses `beans` for task tracking. Use `beans --json` for all commands.
161
+
162
+ - Check available work: `beans --json ready`
163
+ - Claim a task: `beans claim <id>`
164
+ - Mark done: `beans close <id>`
165
+ - Create subtasks: `beans create "<title>" --parent <id>`
166
+ ```
167
+
168
+ ## License
169
+
170
+ MIT
171
+
172
+ ## Contributing
173
+
174
+ Beans is built with Python 3.14, managed with [uv](https://docs.astral.sh/uv/).
175
+
176
+ ```bash
177
+ git clone https://github.com/henriquebastos/beans.git
178
+ cd beans
179
+ uv sync
180
+ uv run pytest
181
+ uv run ruff check src/ tests/
182
+ ```
183
+
184
+ Tests use real SQLite `:memory:` databases — no mocks. The test suite runs in under a
185
+ second.
@@ -0,0 +1,173 @@
1
+ # 🫘 Beans
2
+
3
+ **Graph-based issue tracker for AI agent coordination.**
4
+
5
+ Coding with AI makes developers absurdly productive. Coffee keeps them going.
6
+ Beans keep the whole loop fed. ☕
7
+
8
+ Beans is a lightweight, embedded issue tracker designed for AI agents to coordinate work
9
+ across tasks. It models issues as nodes in a dependency graph, backed by SQLite, with a
10
+ CLI that both humans and agents can use.
11
+
12
+ ```
13
+ $ beans create "Fix auth middleware"
14
+ bean-a3f2dd1c 2025-06-15 10:42 Fix auth middleware
15
+
16
+ $ beans list
17
+ bean-a3f2dd1c 2025-06-15 10:42 Fix auth middleware
18
+ bean-7e2b9f01 2025-06-15 10:43 Add rate limiting
19
+
20
+ $ beans --json list
21
+ [{"id": "bean-a3f2dd1c", "title": "Fix auth middleware", "status": "open", ...}]
22
+ ```
23
+
24
+ ## Why Beans exists
25
+
26
+ Beans was born from analyzing [beads](https://github.com/steveyegge/beads), a
27
+ graph-based issue tracker with a similar goal: giving AI agents a structured way to
28
+ coordinate work. The idea is excellent. The execution, however, raised serious concerns.
29
+
30
+ ### The problem with beads
31
+
32
+ Beads is invasive software that assumes too much about the user's intentions:
33
+
34
+ - **Curl-pipe-bash installer** that silently escalates to `sudo` to install a ~200MB
35
+ binary system-wide
36
+ - **Strips macOS code signatures** and applies ad-hoc signatures to bypass Gatekeeper —
37
+ the same technique used by actual malware
38
+ - **Installs 5 persistent git hooks** (`pre-commit`, `post-merge`, `pre-push`,
39
+ `post-checkout`, `prepare-commit-msg`) that run on nearly every git operation
40
+ - **Runs a background database daemon** (Dolt SQL server) that binds to TCP ports
41
+ 3307/3308 — a MySQL-protocol server running on your machine
42
+ - **Silently modifies commit messages** by appending metadata trailers without asking
43
+ - **Auto-pushes data** to remote servers every 5 minutes
44
+ - **Fingerprints your machine** with unique UUIDs for the repo, clone, and project
45
+ - **Injects system prompts** into AI agent configuration files (e.g.,
46
+ `.claude/settings.local.json`) to force agents to use the tool
47
+ - **Includes a "stealth mode"** that hides its presence from collaborators
48
+ - **Ships with telemetry** that records every command and all arguments, including who
49
+ ran them
50
+
51
+ The core data model is a 50+ field struct covering everything from agent heartbeats to
52
+ ephemeral "wisps." It's not a simple tool — it's a complex system that assumes you want
53
+ all of it.
54
+
55
+ ### What beans does differently
56
+
57
+ Beans keeps the good idea — a dependency graph for AI agent coordination — and throws
58
+ away everything else:
59
+
60
+ | | Beads | Beans |
61
+ |---|---|---|
62
+ | **Storage** | Dolt SQL server (~200MB, background daemon, TCP ports) | SQLite (zero-config, embedded, stdlib) |
63
+ | **Installation** | `curl \| bash` + `sudo` + signature stripping | `uv add beans` or `pip install beans` |
64
+ | **Git hooks** | 5 persistent hooks installed silently | None |
65
+ | **Commit modification** | Silent trailer injection | Never touches your commits |
66
+ | **Background processes** | Persistent MySQL-protocol daemon | None |
67
+ | **Network** | Opens TCP ports, auto-pushes every 5 min | Fully offline |
68
+ | **Telemetry** | OTel spans with actor identity + full args | None |
69
+ | **Stealth mode** | Yes, hides from collaborators | No — transparency is a feature |
70
+ | **AI config injection** | Writes to `.claude/settings.local.json` | Provides recipes you copy yourself |
71
+ | **Data model** | 50+ fields | ~10 fields + dependency edges |
72
+ | **Agent integration** | Forced via injected prompts | Opt-in via `AGENTS.md` instructions |
73
+
74
+ ## Design principles
75
+
76
+ **Polite software.** Beans never modifies files without asking, never installs hooks,
77
+ never runs background processes, and never phones home. It's a CLI tool that reads and
78
+ writes to a local SQLite file. That's it.
79
+
80
+ **Embedded storage.** A single `.beans/beans.db` SQLite file with WAL mode. No servers,
81
+ no ports, no daemons. Works offline, works in CI, works anywhere Python runs.
82
+
83
+ **Graph-native.** Issues (beans) are nodes. Dependencies are typed edges. "What's ready
84
+ to work on?" is a graph query — show me all open beans with no open blockers.
85
+
86
+ **Agent-friendly.** The `--json` flag on every command makes beans machine-readable.
87
+ Agents don't need to parse human output. The CLI is a thin wrapper around a clean Python
88
+ API — agents can also use the library directly.
89
+
90
+ **Minimal by default.** A bean has an id, title, status, and timestamps. Everything else
91
+ is optional. No 50-field structs, no agent heartbeat tracking, no ephemeral wisps.
92
+
93
+ **Journal-based sync.** Changes are recorded in an append-only JSONL journal that can be
94
+ committed to git. The SQLite database is a materialized view that can be rebuilt from the
95
+ journal at any time. Sync through git, not through custom protocols.
96
+
97
+ ## Installation
98
+
99
+ ```bash
100
+ uv add beans
101
+ ```
102
+
103
+ Or with pip:
104
+
105
+ ```bash
106
+ pip install beans
107
+ ```
108
+
109
+ ## Quick start
110
+
111
+ ```bash
112
+ # Create some beans
113
+ beans create "Set up database schema"
114
+ beans create "Build API endpoints"
115
+ beans create "Write integration tests"
116
+
117
+ # List all beans
118
+ beans list
119
+
120
+ # JSON output for agent consumption
121
+ beans --json list
122
+ ```
123
+
124
+ ## Architecture
125
+
126
+ ```
127
+ src/beans/
128
+ ├── models.py # Pydantic models (pure, no I/O)
129
+ ├── store.py # SQLite storage (I/O boundary)
130
+ └── cli.py # Typer CLI (thin wiring layer)
131
+ ```
132
+
133
+ - **models.py** — Pure data. Bean is a Pydantic model with validation. No I/O, no side
134
+ effects, easy to test.
135
+ - **store.py** — The I/O boundary. BeanStore wraps a SQLite connection with
136
+ create/read/update/delete operations. Accepts an injected connection for testing.
137
+ - **cli.py** — Thin wiring. Parses args, calls store methods, formats output. No
138
+ business logic.
139
+
140
+ ## For AI agents
141
+
142
+ Beans is designed to be used by AI agents as a coordination mechanism. Add this to your
143
+ project's `AGENTS.md`:
144
+
145
+ ```markdown
146
+ ## Task tracking
147
+
148
+ This project uses `beans` for task tracking. Use `beans --json` for all commands.
149
+
150
+ - Check available work: `beans --json ready`
151
+ - Claim a task: `beans claim <id>`
152
+ - Mark done: `beans close <id>`
153
+ - Create subtasks: `beans create "<title>" --parent <id>`
154
+ ```
155
+
156
+ ## License
157
+
158
+ MIT
159
+
160
+ ## Contributing
161
+
162
+ Beans is built with Python 3.14, managed with [uv](https://docs.astral.sh/uv/).
163
+
164
+ ```bash
165
+ git clone https://github.com/henriquebastos/beans.git
166
+ cd beans
167
+ uv sync
168
+ uv run pytest
169
+ uv run ruff check src/ tests/
170
+ ```
171
+
172
+ Tests use real SQLite `:memory:` databases — no mocks. The test suite runs in under a
173
+ second.
@@ -0,0 +1,43 @@
1
+ [project]
2
+ name = "magic-beans"
3
+ version = "0.1.0"
4
+ description = "Graph-based issue tracker for AI agent coordination"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "Henrique Bastos", email = "henrique@bastos.net" }
9
+ ]
10
+ requires-python = ">=3.14"
11
+ dependencies = [
12
+ "pydantic>=2.0",
13
+ "typer>=0.9",
14
+ ]
15
+
16
+ [build-system]
17
+ requires = ["uv_build>=0.10.7,<0.11.0"]
18
+ build-backend = "uv_build"
19
+
20
+ [tool.uv.build-backend]
21
+ module-name = "beans"
22
+
23
+ [project.scripts]
24
+ beans = "beans.cli:app"
25
+
26
+ [dependency-groups]
27
+ dev = [
28
+ "hypothesis>=6.151.9",
29
+ "pytest>=9.0.2",
30
+ "pytest-cov>=7.0.0",
31
+ "ruff>=0.15.6",
32
+ "time-machine>=3.2.0",
33
+ ]
34
+
35
+ [tool.pytest.ini_options]
36
+ testpaths = ["tests"]
37
+ addopts = "--cov=beans --cov-report=term-missing --no-header -q"
38
+
39
+ [tool.ruff]
40
+ line-length = 120
41
+
42
+ [tool.ruff.lint]
43
+ select = ["E", "F", "I", "N", "UP", "RUF"]
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,64 @@
1
+ # Python imports
2
+ import json
3
+ from datetime import datetime
4
+ from typing import Annotated
5
+
6
+ # Pip imports
7
+ import typer
8
+
9
+ # Internal imports
10
+ from beans.models import Bean
11
+ from beans.store import BeanStore
12
+
13
+ app = typer.Typer()
14
+
15
+ # Global state shared across commands
16
+ state: dict = {}
17
+
18
+
19
+ @app.callback()
20
+ def main(
21
+ db: Annotated[str | None, typer.Option(help="Path to SQLite database")] = None,
22
+ json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
23
+ ):
24
+ state["db"] = db
25
+ state["json"] = json_output
26
+
27
+
28
+ def local_timestamp(dt: datetime, fmt="%Y-%m-%d %H:%M") -> str:
29
+ return dt.astimezone().strftime(fmt)
30
+
31
+
32
+ def format_bean(bean: Bean) -> str:
33
+ return f"{bean.id} {local_timestamp(bean.created_at)} {bean.title}"
34
+
35
+
36
+ def get_store() -> BeanStore:
37
+ db_path = state.get("db") or "beans.db" # TODO: project discovery (Phase 6.2)
38
+ return BeanStore.from_path(db_path)
39
+
40
+
41
+ @app.command()
42
+ def create(title: str):
43
+ """Create a new bean."""
44
+ bean = Bean(title=title)
45
+ with get_store() as store:
46
+ store.create_bean(bean)
47
+
48
+ if state.get("json"):
49
+ typer.echo(bean.model_dump_json())
50
+ else:
51
+ typer.echo(format_bean(bean))
52
+
53
+
54
+ @app.command("list")
55
+ def list_beans():
56
+ """List all beans."""
57
+ with get_store() as store:
58
+ beans = store.list_beans()
59
+
60
+ if state.get("json"):
61
+ typer.echo(json.dumps([b.model_dump(mode="json") for b in beans]))
62
+ else:
63
+ for bean in beans:
64
+ typer.echo(format_bean(bean))
@@ -0,0 +1,29 @@
1
+ # Python imports
2
+ import secrets
3
+ from datetime import UTC, datetime
4
+ from functools import partial
5
+ from typing import Literal
6
+
7
+ # Pip imports
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ ID_BYTES = 4
12
+
13
+
14
+ def generate_id(prefix="bean-", fn=partial(secrets.token_hex, ID_BYTES)) -> str:
15
+ return prefix + fn()
16
+
17
+
18
+ class Bean(BaseModel):
19
+ id: str = Field(default_factory=generate_id)
20
+ title: str
21
+ type: str = "task"
22
+ status: Literal["open", "in_progress", "closed"] = "open"
23
+ priority: int = Field(default=2, ge=0, le=4)
24
+ body: str = ""
25
+ parent_id: str | None = None
26
+ assignee: str | None = None
27
+ created_by: str | None = None
28
+ ref_id: str | None = None
29
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
@@ -0,0 +1,82 @@
1
+ # Python imports
2
+ import sqlite3
3
+
4
+ # Internal imports
5
+ from beans.models import Bean
6
+
7
+ def columns(cursor: sqlite3.Cursor) -> list[str]:
8
+ return [desc[0] for desc in cursor.description]
9
+
10
+
11
+ def row(cols: list[str], values: tuple) -> dict:
12
+ return dict(zip(cols, values))
13
+
14
+
15
+ SCHEMA = """
16
+ PRAGMA journal_mode=WAL;
17
+ PRAGMA foreign_keys=ON;
18
+
19
+ CREATE TABLE IF NOT EXISTS beans (
20
+ id TEXT PRIMARY KEY,
21
+ title TEXT NOT NULL,
22
+ type TEXT NOT NULL DEFAULT 'task',
23
+ status TEXT NOT NULL DEFAULT 'open',
24
+ priority INTEGER NOT NULL DEFAULT 2,
25
+ body TEXT NOT NULL DEFAULT '',
26
+ parent_id TEXT,
27
+ assignee TEXT,
28
+ created_by TEXT,
29
+ ref_id TEXT,
30
+ created_at TEXT NOT NULL
31
+ );
32
+ """
33
+
34
+
35
+ class BeanStore:
36
+ def __init__(self, conn: sqlite3.Connection):
37
+ self.conn = conn
38
+ self.init_db(conn)
39
+
40
+ @staticmethod
41
+ def init_db(conn: sqlite3.Connection, schema: str = SCHEMA):
42
+ conn.executescript(schema)
43
+
44
+ @classmethod
45
+ def from_path(cls, db_path: str) -> BeanStore:
46
+ return cls(sqlite3.connect(db_path))
47
+
48
+ def close(self):
49
+ self.conn.close()
50
+
51
+ def __enter__(self):
52
+ return self
53
+
54
+ def __exit__(self, *args):
55
+ self.close()
56
+
57
+ def create_bean(self, bean: Bean) -> Bean:
58
+ self.conn.execute(
59
+ """INSERT INTO beans
60
+ (id, title, type, status, priority, body, parent_id, assignee, created_by, ref_id, created_at)
61
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
62
+ (
63
+ bean.id,
64
+ bean.title,
65
+ bean.type,
66
+ bean.status,
67
+ bean.priority,
68
+ bean.body,
69
+ bean.parent_id,
70
+ bean.assignee,
71
+ bean.created_by,
72
+ bean.ref_id,
73
+ bean.created_at.isoformat(),
74
+ ),
75
+ )
76
+ self.conn.commit()
77
+ return bean
78
+
79
+ def list_beans(self) -> list[Bean]:
80
+ cursor = self.conn.execute("SELECT * FROM beans")
81
+ cols = columns(cursor)
82
+ return [Bean(**row(cols, values)) for values in cursor.fetchall()]