forkhub 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.
- forkhub-0.1.0/.beads/.gitignore +49 -0
- forkhub-0.1.0/.beads/README.md +81 -0
- forkhub-0.1.0/.beads/backup/backup_state.json +13 -0
- forkhub-0.1.0/.beads/backup/comments.jsonl +0 -0
- forkhub-0.1.0/.beads/backup/config.jsonl +11 -0
- forkhub-0.1.0/.beads/backup/dependencies.jsonl +32 -0
- forkhub-0.1.0/.beads/backup/events.jsonl +60 -0
- forkhub-0.1.0/.beads/backup/issues.jsonl +28 -0
- forkhub-0.1.0/.beads/backup/labels.jsonl +0 -0
- forkhub-0.1.0/.beads/config.yaml +55 -0
- forkhub-0.1.0/.beads/hooks/post-checkout +9 -0
- forkhub-0.1.0/.beads/hooks/post-merge +9 -0
- forkhub-0.1.0/.beads/hooks/pre-commit +9 -0
- forkhub-0.1.0/.beads/hooks/pre-push +9 -0
- forkhub-0.1.0/.beads/hooks/prepare-commit-msg +9 -0
- forkhub-0.1.0/.beads/interactions.jsonl +0 -0
- forkhub-0.1.0/.beads/metadata.json +6 -0
- forkhub-0.1.0/.github/workflows/ci.yml +29 -0
- forkhub-0.1.0/.github/workflows/publish.yml +28 -0
- forkhub-0.1.0/.gitignore +47 -0
- forkhub-0.1.0/.python-version +1 -0
- forkhub-0.1.0/CLAUDE.md +268 -0
- forkhub-0.1.0/LICENSE +21 -0
- forkhub-0.1.0/PKG-INFO +262 -0
- forkhub-0.1.0/README.md +230 -0
- forkhub-0.1.0/demo.md +400 -0
- forkhub-0.1.0/docs/superpowers/plans/2026-03-19-pypi-packaging-ci-cd.md +394 -0
- forkhub-0.1.0/docs/superpowers/specs/2026-03-19-pypi-packaging-ci-cd-design.md +209 -0
- forkhub-0.1.0/env.example +12 -0
- forkhub-0.1.0/forkhub.toml.example +39 -0
- forkhub-0.1.0/pyproject.toml +90 -0
- forkhub-0.1.0/spec.md +1113 -0
- forkhub-0.1.0/src/forkhub/__init__.py +173 -0
- forkhub-0.1.0/src/forkhub/agent/__init__.py +2 -0
- forkhub-0.1.0/src/forkhub/agent/agents.py +28 -0
- forkhub-0.1.0/src/forkhub/agent/hooks.py +105 -0
- forkhub-0.1.0/src/forkhub/agent/prompts.py +108 -0
- forkhub-0.1.0/src/forkhub/agent/runner.py +259 -0
- forkhub-0.1.0/src/forkhub/agent/tools.py +366 -0
- forkhub-0.1.0/src/forkhub/cli/__init__.py +2 -0
- forkhub-0.1.0/src/forkhub/cli/app.py +63 -0
- forkhub-0.1.0/src/forkhub/cli/clusters_cmd.py +97 -0
- forkhub-0.1.0/src/forkhub/cli/config_cmd.py +120 -0
- forkhub-0.1.0/src/forkhub/cli/digest_cmd.py +97 -0
- forkhub-0.1.0/src/forkhub/cli/forks_cmd.py +170 -0
- forkhub-0.1.0/src/forkhub/cli/formatting.py +178 -0
- forkhub-0.1.0/src/forkhub/cli/helpers.py +43 -0
- forkhub-0.1.0/src/forkhub/cli/init_cmd.py +112 -0
- forkhub-0.1.0/src/forkhub/cli/repos_cmd.py +87 -0
- forkhub-0.1.0/src/forkhub/cli/sync_cmd.py +113 -0
- forkhub-0.1.0/src/forkhub/cli/track_cmd.py +191 -0
- forkhub-0.1.0/src/forkhub/config.py +254 -0
- forkhub-0.1.0/src/forkhub/database.py +535 -0
- forkhub-0.1.0/src/forkhub/embeddings/__init__.py +2 -0
- forkhub-0.1.0/src/forkhub/embeddings/local.py +46 -0
- forkhub-0.1.0/src/forkhub/interfaces.py +64 -0
- forkhub-0.1.0/src/forkhub/models.py +277 -0
- forkhub-0.1.0/src/forkhub/notifications/__init__.py +2 -0
- forkhub-0.1.0/src/forkhub/notifications/console.py +35 -0
- forkhub-0.1.0/src/forkhub/providers/__init__.py +2 -0
- forkhub-0.1.0/src/forkhub/providers/github.py +270 -0
- forkhub-0.1.0/src/forkhub/py.typed +0 -0
- forkhub-0.1.0/src/forkhub/services/__init__.py +2 -0
- forkhub-0.1.0/src/forkhub/services/analyzer.py +31 -0
- forkhub-0.1.0/src/forkhub/services/cluster.py +332 -0
- forkhub-0.1.0/src/forkhub/services/digest.py +190 -0
- forkhub-0.1.0/src/forkhub/services/sync.py +229 -0
- forkhub-0.1.0/src/forkhub/services/tracker.py +171 -0
- forkhub-0.1.0/tests/__init__.py +0 -0
- forkhub-0.1.0/tests/conftest.py +39 -0
- forkhub-0.1.0/tests/test_agent_hooks.py +322 -0
- forkhub-0.1.0/tests/test_agent_runner.py +391 -0
- forkhub-0.1.0/tests/test_agent_tools.py +666 -0
- forkhub-0.1.0/tests/test_cli.py +823 -0
- forkhub-0.1.0/tests/test_clusters.py +597 -0
- forkhub-0.1.0/tests/test_config.py +589 -0
- forkhub-0.1.0/tests/test_console_backend.py +613 -0
- forkhub-0.1.0/tests/test_database.py +646 -0
- forkhub-0.1.0/tests/test_digest.py +632 -0
- forkhub-0.1.0/tests/test_embeddings.py +190 -0
- forkhub-0.1.0/tests/test_forkhub_api.py +494 -0
- forkhub-0.1.0/tests/test_github_provider.py +642 -0
- forkhub-0.1.0/tests/test_integration.py +424 -0
- forkhub-0.1.0/tests/test_interfaces.py +337 -0
- forkhub-0.1.0/tests/test_models.py +808 -0
- forkhub-0.1.0/tests/test_packaging.py +24 -0
- forkhub-0.1.0/tests/test_smoke.py +42 -0
- forkhub-0.1.0/tests/test_sync.py +563 -0
- forkhub-0.1.0/tests/test_tracker.py +346 -0
- forkhub-0.1.0/uv.lock +2179 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Dolt database (managed by Dolt, not git)
|
|
2
|
+
dolt/
|
|
3
|
+
dolt-access.lock
|
|
4
|
+
|
|
5
|
+
# Runtime files
|
|
6
|
+
bd.sock
|
|
7
|
+
bd.sock.startlock
|
|
8
|
+
sync-state.json
|
|
9
|
+
last-touched
|
|
10
|
+
|
|
11
|
+
# Local version tracking (prevents upgrade notification spam after git ops)
|
|
12
|
+
.local_version
|
|
13
|
+
|
|
14
|
+
# Worktree redirect file (contains relative path to main repo's .beads/)
|
|
15
|
+
# Must not be committed as paths would be wrong in other clones
|
|
16
|
+
redirect
|
|
17
|
+
|
|
18
|
+
# Sync state (local-only, per-machine)
|
|
19
|
+
# These files are machine-specific and should not be shared across clones
|
|
20
|
+
.sync.lock
|
|
21
|
+
export-state/
|
|
22
|
+
|
|
23
|
+
# Ephemeral store (SQLite - wisps/molecules, intentionally not versioned)
|
|
24
|
+
ephemeral.sqlite3
|
|
25
|
+
ephemeral.sqlite3-journal
|
|
26
|
+
ephemeral.sqlite3-wal
|
|
27
|
+
ephemeral.sqlite3-shm
|
|
28
|
+
|
|
29
|
+
# Dolt server management (auto-started by bd)
|
|
30
|
+
dolt-server.pid
|
|
31
|
+
dolt-server.log
|
|
32
|
+
dolt-server.lock
|
|
33
|
+
|
|
34
|
+
# Legacy files (from pre-Dolt versions)
|
|
35
|
+
*.db
|
|
36
|
+
*.db?*
|
|
37
|
+
*.db-journal
|
|
38
|
+
*.db-wal
|
|
39
|
+
*.db-shm
|
|
40
|
+
db.sqlite
|
|
41
|
+
bd.db
|
|
42
|
+
daemon.lock
|
|
43
|
+
daemon.log
|
|
44
|
+
daemon-*.log.gz
|
|
45
|
+
daemon.pid
|
|
46
|
+
# NOTE: Do NOT add negation patterns here.
|
|
47
|
+
# They would override fork protection in .git/info/exclude.
|
|
48
|
+
# Config files (metadata.json, config.yaml) are tracked by git by default
|
|
49
|
+
# since no pattern above ignores them.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Beads - AI-Native Issue Tracking
|
|
2
|
+
|
|
3
|
+
Welcome to Beads! This repository uses **Beads** for issue tracking - a modern, AI-native tool designed to live directly in your codebase alongside your code.
|
|
4
|
+
|
|
5
|
+
## What is Beads?
|
|
6
|
+
|
|
7
|
+
Beads is issue tracking that lives in your repo, making it perfect for AI coding agents and developers who want their issues close to their code. No web UI required - everything works through the CLI and integrates seamlessly with git.
|
|
8
|
+
|
|
9
|
+
**Learn more:** [github.com/steveyegge/beads](https://github.com/steveyegge/beads)
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### Essential Commands
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Create new issues
|
|
17
|
+
bd create "Add user authentication"
|
|
18
|
+
|
|
19
|
+
# View all issues
|
|
20
|
+
bd list
|
|
21
|
+
|
|
22
|
+
# View issue details
|
|
23
|
+
bd show <issue-id>
|
|
24
|
+
|
|
25
|
+
# Update issue status
|
|
26
|
+
bd update <issue-id> --claim
|
|
27
|
+
bd update <issue-id> --status done
|
|
28
|
+
|
|
29
|
+
# Sync with Dolt remote
|
|
30
|
+
bd dolt push
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Working with Issues
|
|
34
|
+
|
|
35
|
+
Issues in Beads are:
|
|
36
|
+
- **Git-native**: Stored in `.beads/issues.jsonl` and synced like code
|
|
37
|
+
- **AI-friendly**: CLI-first design works perfectly with AI coding agents
|
|
38
|
+
- **Branch-aware**: Issues can follow your branch workflow
|
|
39
|
+
- **Always in sync**: Auto-syncs with your commits
|
|
40
|
+
|
|
41
|
+
## Why Beads?
|
|
42
|
+
|
|
43
|
+
✨ **AI-Native Design**
|
|
44
|
+
- Built specifically for AI-assisted development workflows
|
|
45
|
+
- CLI-first interface works seamlessly with AI coding agents
|
|
46
|
+
- No context switching to web UIs
|
|
47
|
+
|
|
48
|
+
🚀 **Developer Focused**
|
|
49
|
+
- Issues live in your repo, right next to your code
|
|
50
|
+
- Works offline, syncs when you push
|
|
51
|
+
- Fast, lightweight, and stays out of your way
|
|
52
|
+
|
|
53
|
+
🔧 **Git Integration**
|
|
54
|
+
- Automatic sync with git commits
|
|
55
|
+
- Branch-aware issue tracking
|
|
56
|
+
- Intelligent JSONL merge resolution
|
|
57
|
+
|
|
58
|
+
## Get Started with Beads
|
|
59
|
+
|
|
60
|
+
Try Beads in your own projects:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Install Beads
|
|
64
|
+
curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash
|
|
65
|
+
|
|
66
|
+
# Initialize in your repo
|
|
67
|
+
bd init
|
|
68
|
+
|
|
69
|
+
# Create your first issue
|
|
70
|
+
bd create "Try out Beads"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Learn More
|
|
74
|
+
|
|
75
|
+
- **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs)
|
|
76
|
+
- **Quick Start Guide**: Run `bd quickstart`
|
|
77
|
+
- **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples)
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
*Beads: Issue tracking that moves at the speed of thought* ⚡
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{"key":"auto_compact_enabled","value":"false"}
|
|
2
|
+
{"key":"compact_batch_size","value":"50"}
|
|
3
|
+
{"key":"compact_parallel_workers","value":"5"}
|
|
4
|
+
{"key":"compact_tier1_days","value":"30"}
|
|
5
|
+
{"key":"compact_tier1_dep_levels","value":"2"}
|
|
6
|
+
{"key":"compact_tier2_commits","value":"100"}
|
|
7
|
+
{"key":"compact_tier2_days","value":"90"}
|
|
8
|
+
{"key":"compact_tier2_dep_levels","value":"5"}
|
|
9
|
+
{"key":"compaction_enabled","value":"false"}
|
|
10
|
+
{"key":"issue_prefix","value":"forkhub"}
|
|
11
|
+
{"key":"schema_version","value":"6"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{"created_at":"2026-03-01T18:53:06Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-2kl","issue_id":"forkhub-158","type":"blocks"}
|
|
2
|
+
{"created_at":"2026-03-01T18:53:18Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-3fr","issue_id":"forkhub-1ji","type":"blocks"}
|
|
3
|
+
{"created_at":"2026-03-01T18:53:18Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-dz2","issue_id":"forkhub-1ji","type":"blocks"}
|
|
4
|
+
{"created_at":"2026-03-01T18:53:18Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-158","issue_id":"forkhub-3fr","type":"blocks"}
|
|
5
|
+
{"created_at":"2026-03-01T18:53:17Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-5xt","issue_id":"forkhub-3fr","type":"blocks"}
|
|
6
|
+
{"created_at":"2026-03-01T18:53:17Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-dz2","issue_id":"forkhub-3fr","type":"blocks"}
|
|
7
|
+
{"created_at":"2026-03-01T18:53:17Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-v0i","issue_id":"forkhub-3fr","type":"blocks"}
|
|
8
|
+
{"created_at":"2026-03-01T18:53:17Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-wtq","issue_id":"forkhub-3fr","type":"blocks"}
|
|
9
|
+
{"created_at":"2026-03-01T18:53:17Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-zlr","issue_id":"forkhub-3fr","type":"blocks"}
|
|
10
|
+
{"created_at":"2026-03-01T18:53:12Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-6e6","issue_id":"forkhub-5xt","type":"blocks"}
|
|
11
|
+
{"created_at":"2026-03-01T18:53:12Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-v0i","issue_id":"forkhub-5xt","type":"blocks"}
|
|
12
|
+
{"created_at":"2026-03-01T18:53:06Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-2kl","issue_id":"forkhub-5y4","type":"blocks"}
|
|
13
|
+
{"created_at":"2026-03-01T18:53:09Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-158","issue_id":"forkhub-6e6","type":"blocks"}
|
|
14
|
+
{"created_at":"2026-03-01T18:53:09Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-l7z","issue_id":"forkhub-6e6","type":"blocks"}
|
|
15
|
+
{"created_at":"2026-03-01T18:53:09Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-ufp","issue_id":"forkhub-6e6","type":"blocks"}
|
|
16
|
+
{"created_at":"2026-03-01T18:53:09Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-l7z","issue_id":"forkhub-8ej","type":"blocks"}
|
|
17
|
+
{"created_at":"2026-03-01T18:53:09Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-ufp","issue_id":"forkhub-8ej","type":"blocks"}
|
|
18
|
+
{"created_at":"2026-03-01T18:53:09Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-l7z","issue_id":"forkhub-dpd","type":"blocks"}
|
|
19
|
+
{"created_at":"2026-03-01T18:53:14Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-5xt","issue_id":"forkhub-dz2","type":"blocks"}
|
|
20
|
+
{"created_at":"2026-03-01T18:53:14Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-wtq","issue_id":"forkhub-dz2","type":"blocks"}
|
|
21
|
+
{"created_at":"2026-03-01T18:53:14Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-wwz","issue_id":"forkhub-dz2","type":"blocks"}
|
|
22
|
+
{"created_at":"2026-03-01T18:53:06Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-2kl","issue_id":"forkhub-l7z","type":"blocks"}
|
|
23
|
+
{"created_at":"2026-03-01T18:53:06Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-2kl","issue_id":"forkhub-ufp","type":"blocks"}
|
|
24
|
+
{"created_at":"2026-03-01T18:53:12Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-5y4","issue_id":"forkhub-v0i","type":"blocks"}
|
|
25
|
+
{"created_at":"2026-03-01T18:53:12Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-6e6","issue_id":"forkhub-v0i","type":"blocks"}
|
|
26
|
+
{"created_at":"2026-03-01T18:53:12Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-5y4","issue_id":"forkhub-wtq","type":"blocks"}
|
|
27
|
+
{"created_at":"2026-03-01T18:53:12Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-dpd","issue_id":"forkhub-wtq","type":"blocks"}
|
|
28
|
+
{"created_at":"2026-03-01T18:53:14Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-5y4","issue_id":"forkhub-wwz","type":"blocks"}
|
|
29
|
+
{"created_at":"2026-03-01T18:53:14Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-6e6","issue_id":"forkhub-wwz","type":"blocks"}
|
|
30
|
+
{"created_at":"2026-03-01T18:53:14Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-v0i","issue_id":"forkhub-wwz","type":"blocks"}
|
|
31
|
+
{"created_at":"2026-03-01T18:53:12Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-5y4","issue_id":"forkhub-zlr","type":"blocks"}
|
|
32
|
+
{"created_at":"2026-03-01T18:53:12Z","created_by":"Joshua Oliphant","depends_on_id":"forkhub-8ej","issue_id":"forkhub-zlr","type":"blocks"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:48:04Z","event_type":"created","id":1,"issue_id":"forkhub-2kl","new_value":"","old_value":""}
|
|
2
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:48:27Z","event_type":"created","id":2,"issue_id":"forkhub-ufp","new_value":"","old_value":""}
|
|
3
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:48:37Z","event_type":"created","id":3,"issue_id":"forkhub-l7z","new_value":"","old_value":""}
|
|
4
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:48:52Z","event_type":"created","id":4,"issue_id":"forkhub-5y4","new_value":"","old_value":""}
|
|
5
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:49:06Z","event_type":"created","id":5,"issue_id":"forkhub-158","new_value":"","old_value":""}
|
|
6
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:49:27Z","event_type":"created","id":6,"issue_id":"forkhub-6e6","new_value":"","old_value":""}
|
|
7
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:49:38Z","event_type":"created","id":7,"issue_id":"forkhub-dpd","new_value":"","old_value":""}
|
|
8
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:49:52Z","event_type":"created","id":8,"issue_id":"forkhub-8ej","new_value":"","old_value":""}
|
|
9
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:50:07Z","event_type":"created","id":9,"issue_id":"forkhub-v0i","new_value":"","old_value":""}
|
|
10
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:50:21Z","event_type":"created","id":10,"issue_id":"forkhub-5xt","new_value":"","old_value":""}
|
|
11
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:50:34Z","event_type":"created","id":11,"issue_id":"forkhub-wtq","new_value":"","old_value":""}
|
|
12
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:50:46Z","event_type":"created","id":12,"issue_id":"forkhub-zlr","new_value":"","old_value":""}
|
|
13
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:51:08Z","event_type":"created","id":13,"issue_id":"forkhub-wwz","new_value":"","old_value":""}
|
|
14
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:51:30Z","event_type":"created","id":14,"issue_id":"forkhub-dz2","new_value":"","old_value":""}
|
|
15
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:51:58Z","event_type":"created","id":15,"issue_id":"forkhub-3fr","new_value":"","old_value":""}
|
|
16
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:52:19Z","event_type":"created","id":16,"issue_id":"forkhub-1ji","new_value":"","old_value":""}
|
|
17
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:52:26Z","event_type":"created","id":17,"issue_id":"forkhub-ah6","new_value":"","old_value":""}
|
|
18
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:52:27Z","event_type":"created","id":18,"issue_id":"forkhub-ime","new_value":"","old_value":""}
|
|
19
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:52:29Z","event_type":"created","id":19,"issue_id":"forkhub-c4i","new_value":"","old_value":""}
|
|
20
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:52:31Z","event_type":"created","id":20,"issue_id":"forkhub-jcr","new_value":"","old_value":""}
|
|
21
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:52:37Z","event_type":"created","id":21,"issue_id":"forkhub-l2u","new_value":"","old_value":""}
|
|
22
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:52:39Z","event_type":"created","id":22,"issue_id":"forkhub-z98","new_value":"","old_value":""}
|
|
23
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:52:44Z","event_type":"created","id":23,"issue_id":"forkhub-4id","new_value":"","old_value":""}
|
|
24
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:52:46Z","event_type":"created","id":24,"issue_id":"forkhub-00a","new_value":"","old_value":""}
|
|
25
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:52:50Z","event_type":"created","id":25,"issue_id":"forkhub-bgb","new_value":"","old_value":""}
|
|
26
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:52:52Z","event_type":"created","id":26,"issue_id":"forkhub-qqm","new_value":"","old_value":""}
|
|
27
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:52:55Z","event_type":"created","id":27,"issue_id":"forkhub-m7y","new_value":"","old_value":""}
|
|
28
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:52:56Z","event_type":"created","id":28,"issue_id":"forkhub-ddb","new_value":"","old_value":""}
|
|
29
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:55:45Z","event_type":"status_changed","id":29,"issue_id":"forkhub-2kl","new_value":"{\"status\":\"in_progress\"}","old_value":"{\"id\":\"forkhub-2kl\",\"title\":\"Project scaffolding: uv init, pyproject.toml, directory structure, git init\",\"description\":\"## Wave 0 — Must complete before all other work\\n\\n### What to build\\n1. Run `uv init` with src layout in the forkhub project directory\\n2. Configure `pyproject.toml`:\\n - Dependencies: `githubkit`, `claude-agent-sdk`, `typer[all]`, `rich`, `pydantic\\u003e=2.0`, `pydantic-settings`, `sentence-transformers`, `sqlite-vec`, `aiosqlite`\\n - Dev deps: `pytest`, `pytest-asyncio`, `pytest-cov`, `ruff`, `mypy`, `respx`\\n - Script entry point: `forkhub = \\\"forkhub.cli.app:app\\\"`\\n - Ruff config (line-length=99, target py312)\\n - Mypy config (strict mode)\\n3. Create all package directories with `__init__.py`: `src/forkhub/{cli,providers,embeddings,notifications,agent,services}/`\\n4. Create `src/forkhub/py.typed` (PEP 561 marker)\\n5. Create `tests/__init__.py`, `tests/conftest.py`, `tests/fixtures/` dir\\n6. Create minimal `src/forkhub/cli/app.py` with `app = typer.Typer(name=\\\"forkhub\\\", help=\\\"Monitor GitHub fork constellations\\\")`\\n7. Create `forkhub.toml.example` from spec §12\\n8. Init git repo, create comprehensive `.gitignore`\\n9. Run `uv sync` to install everything\\n10. All `.py` files must start with 2-line ABOUTME comments\\n\\n### Smoke tests\\n- Package imports successfully: `from forkhub.cli.app import app`\\n- `uv run forkhub --help` works\\n\\n### Acceptance criteria\\n- `uv run pytest` passes (smoke tests)\\n- `uv run ruff check src/ tests/` clean\\n- All directories exist with __init__.py files\",\"status\":\"open\",\"priority\":1,\"issue_type\":\"task\",\"owner\":\"joshua.oliphant@hey.com\",\"created_at\":\"2026-03-02T02:48:05Z\",\"created_by\":\"Joshua Oliphant\",\"updated_at\":\"2026-03-02T02:48:05Z\"}"}
|
|
30
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T18:59:50Z","event_type":"closed","id":30,"issue_id":"forkhub-2kl","new_value":"Scaffolding complete: uv project, all packages, CLI, smoke tests, lint clean","old_value":""}
|
|
31
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:00:31Z","event_type":"status_changed","id":31,"issue_id":"forkhub-ufp","new_value":"{\"status\":\"in_progress\"}","old_value":"{\"id\":\"forkhub-ufp\",\"title\":\"Pydantic data models\",\"description\":\"## Wave 1 — Foundation (parallelizable with 1.2, 1.3, 1.4)\\n\\n### File: `src/forkhub/models.py`\\n\\n### What to build\\nDefine all Pydantic data models used as the shared vocabulary across the system. These are pure data transfer objects, NOT ORM models.\\n\\n### Models to define\\n\\n**Enums (StrEnum):**\\n- `SignalCategory`: feature, fix, refactor, config, dependency, removal, adaptation, release\\n- `ForkVitality`: active, dormant, dead, unknown\\n- `TrackingMode`: owned, watched, upstream\\n\\n**Git/GitHub models:**\\n- `RepoInfo`: github_id, owner, name, full_name, default_branch, description, stars, fork_count, is_fork, parent_full_name\\n- `ForkInfo`: extends RepoInfo fields + commits_ahead, commits_behind, has_diverged, last_pushed_at, head_sha\\n- `ForkPage`: forks: list[ForkInfo], total_count, has_next_page, next_page\\n- `CompareResult`: ahead_by, behind_by, files: list[FileChange], commits: list[CommitInfo]\\n- `FileChange`: filename, status, additions, deletions, patch (optional)\\n- `CommitInfo`: sha, message, author, date\\n- `Release`: tag_name, name, body, published_at, prerelease\\n- `RateLimitInfo`: limit, remaining, reset_at (datetime)\\n\\n**Domain models:**\\n- `TrackedRepo`: id (uuid str), github_id, owner, name, full_name, tracking_mode, default_branch, description, fork_depth, excluded, last_synced_at, created_at\\n- `Fork`: id (uuid str), tracked_repo_id, github_id, owner, full_name, default_branch, description, vitality, stars, stars_previous, depth, last_pushed_at, commits_ahead, commits_behind, head_sha, created_at, updated_at\\n- `Signal`: id (uuid str), fork_id (optional—null for upstream signals), tracked_repo_id, category (SignalCategory), summary, detail, files_involved (list[str]), significance (1-10), is_upstream, release_tag, created_at\\n- `Cluster`: id (uuid str), tracked_repo_id, label, description, files_pattern (list[str]), fork_count, signal_ids (list[str]), created_at, updated_at\\n- `Digest`: id (uuid str), title, body, signal_ids (list[str]), config_id, delivered_at, created_at\\n- `DigestConfig`: id (uuid str), tracked_repo_id (optional), frequency, day_of_week, time_of_day, min_significance, categories (list[SignalCategory] | None), file_patterns (list[str] | None), backends (list[str]), created_at\\n- `DeliveryResult`: success (bool), backend_name (str), error (str | None)\\n\\n### TDD Tests (`tests/test_models.py`)\\n- Valid construction for each model\\n- Validation rejects bad data: significance outside 1-10, invalid category enum, etc.\\n- Serialization roundtrip (model_dump/model_validate)\\n- JSON array fields serialize correctly (files_involved, signal_ids)\\n- Enum membership for all three enums\\n- Optional fields handle None correctly\\n\\n### Dependencies\\n- Blocked by: Wave 0 scaffolding (forkhub-2kl)\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"joshua.oliphant@hey.com\",\"created_at\":\"2026-03-02T02:48:27Z\",\"created_by\":\"Joshua Oliphant\",\"updated_at\":\"2026-03-02T02:48:27Z\"}"}
|
|
32
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:00:31Z","event_type":"status_changed","id":32,"issue_id":"forkhub-l7z","new_value":"{\"status\":\"in_progress\"}","old_value":"{\"id\":\"forkhub-l7z\",\"title\":\"Protocol interfaces (plugin system)\",\"description\":\"## Wave 1 — Foundation (parallelizable with 1.1, 1.3, 1.4)\\n\\n### File: `src/forkhub/interfaces.py`\\n\\n### What to build\\nDefine the three `@runtime_checkable` Protocol classes that form the plugin extension points. Uses `typing.Protocol` for structural typing — no registration, no plugin registry.\\n\\n### Protocols\\n\\n**GitProvider** (8 async methods):\\n```python\\n@runtime_checkable\\nclass GitProvider(Protocol):\\n async def get_user_repos(self, username: str) -\\u003e list[RepoInfo]: ...\\n async def get_forks(self, owner: str, repo: str, *, page: int = 1) -\\u003e ForkPage: ...\\n async def compare(self, owner: str, repo: str, base: str, head: str) -\\u003e CompareResult: ...\\n async def get_releases(self, owner: str, repo: str, *, since: datetime | None = None) -\\u003e list[Release]: ...\\n async def get_repo(self, owner: str, repo: str) -\\u003e RepoInfo: ...\\n async def get_commit_messages(self, owner: str, repo: str, *, since: str | None = None) -\\u003e list[CommitInfo]: ...\\n async def get_file_diff(self, owner: str, repo: str, base: str, head: str, path: str) -\\u003e str: ...\\n async def get_rate_limit(self) -\\u003e RateLimitInfo: ...\\n```\\n\\n**NotificationBackend**:\\n```python\\n@runtime_checkable\\nclass NotificationBackend(Protocol):\\n async def deliver(self, digest: Digest) -\\u003e DeliveryResult: ...\\n def backend_name(self) -\\u003e str: ...\\n```\\n\\n**EmbeddingProvider**:\\n```python\\n@runtime_checkable\\nclass EmbeddingProvider(Protocol):\\n async def embed(self, texts: list[str]) -\\u003e list[list[float]]: ...\\n def dimensions(self) -\\u003e int: ...\\n```\\n\\nImport all referenced types from `models.py`.\\n\\n### TDD Tests (`tests/test_interfaces.py`)\\n- Create minimal conforming stub classes (just enough to satisfy the Protocol)\\n- Verify `isinstance(stub, GitProvider)` returns True\\n- Verify `isinstance(stub, NotificationBackend)` returns True\\n- Verify `isinstance(stub, EmbeddingProvider)` returns True\\n- Create non-conforming classes (missing methods) and verify isinstance returns False\\n- Verify all method signatures match expected parameter/return types\\n\\n### Dependencies\\n- Blocked by: Wave 0 scaffolding (forkhub-2kl)\\n- Needs models from 1.1 but can be built concurrently if types are imported from the same file\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"joshua.oliphant@hey.com\",\"created_at\":\"2026-03-02T02:48:38Z\",\"created_by\":\"Joshua Oliphant\",\"updated_at\":\"2026-03-02T02:48:38Z\"}"}
|
|
33
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:00:31Z","event_type":"status_changed","id":33,"issue_id":"forkhub-5y4","new_value":"{\"status\":\"in_progress\"}","old_value":"{\"id\":\"forkhub-5y4\",\"title\":\"Database layer (SQLite + sqlite-vec)\",\"description\":\"## Wave 1 — Foundation (parallelizable with 1.1, 1.2, 1.4)\\n\\n### File: `src/forkhub/database.py`\\n\\n### What to build\\n`Database` class using `aiosqlite` for async SQLite operations with sqlite-vec extension for vector similarity search.\\n\\n### Database class\\n- `Database(path: str | Path)` — async context manager\\n- `async init()` — creates DB file, loads sqlite-vec extension, runs schema DDL\\n- Uses `uuid4()` strings for all primary keys\\n\\n### Schema (from spec §8)\\nAll tables from the spec: `tracked_repos`, `forks`, `signals`, `clusters`, `cluster_members`, `digest_configs`, `digests`, `annotations`, `sync_state`. Plus all indexes.\\n\\nVector table:\\n```sql\\nCREATE VIRTUAL TABLE vec_signals USING vec0(\\n signal_id TEXT PRIMARY KEY,\\n embedding float[384]\\n);\\n```\\n\\n### CRUD methods\\nOrganized by entity:\\n- `save_tracked_repo()`, `get_tracked_repo(id)`, `get_tracked_repo_by_name(full_name)`, `list_tracked_repos(mode=None)`, `delete_tracked_repo(id)`, `update_tracked_repo()`\\n- `save_fork()`, `get_fork(id)`, `get_fork_by_name(full_name)`, `list_forks_for_repo(repo_id)`, `update_fork()`\\n- `save_signal()`, `get_signals_for_repo(repo_id)`, `get_signals_since(since: datetime)`, `get_signals_by_ids(ids: list[str])`\\n- `save_cluster()`, `get_clusters_for_repo(repo_id, min_size=2)`, `add_cluster_member(cluster_id, signal_id, fork_id)`, `update_cluster()`\\n- `save_digest_config()`, `get_digest_config(repo_id=None)`, `get_global_digest_config()`\\n- `save_digest()`, `get_latest_digest(config_id)`\\n- `set_sync_state(key, value)`, `get_sync_state(key) -\\u003e str | None`\\n\\n### Vector operations\\n- `save_signal_embedding(signal_id: str, embedding: list[float])` — uses `sqlite_vec.serialize_float32()`\\n- `search_similar_signals(embedding: list[float], limit: int = 5) -\\u003e list[tuple[str, float]]` — returns (signal_id, distance) pairs\\n\\n### TDD Tests (`tests/test_database.py`)\\nAll tests use in-memory SQLite (`:memory:`):\\n- Test init creates all tables (query sqlite_master)\\n- Test sqlite-vec extension loads and vec_signals table works\\n- Test CRUD for each entity: insert, read by id, read by name, list, update, delete\\n- Test foreign key cascades: delete tracked_repo cascades to forks and signals\\n- Test vector similarity search returns nearest neighbors in correct order\\n- Test save_signal_embedding + search roundtrip\\n- Test sync_state get/set\\n- Test concurrent operations don't corrupt\\n\\n### Dependencies\\n- Blocked by: Wave 0 scaffolding (forkhub-2kl)\\n- Uses Pydantic models from 1.1 for return types\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"joshua.oliphant@hey.com\",\"created_at\":\"2026-03-02T02:48:52Z\",\"created_by\":\"Joshua Oliphant\",\"updated_at\":\"2026-03-02T02:48:52Z\"}"}
|
|
34
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:00:31Z","event_type":"status_changed","id":34,"issue_id":"forkhub-158","new_value":"{\"status\":\"in_progress\"}","old_value":"{\"id\":\"forkhub-158\",\"title\":\"Configuration system (Pydantic Settings + TOML)\",\"description\":\"## Wave 1 — Foundation (parallelizable with 1.1, 1.2, 1.3)\\n\\n### File: `src/forkhub/config.py`\\n\\n### What to build\\n`ForkHubSettings` using Pydantic Settings with TOML config file + environment variable overrides.\\n\\n### Settings classes (nested BaseModel groups under one BaseSettings)\\n\\n```python\\nclass GitHubSettings(BaseModel):\\n token: str = \\\"\\\"\\n username: str = \\\"\\\"\\n\\nclass AnthropicSettings(BaseModel):\\n api_key: str = \\\"\\\"\\n analysis_budget_usd: float = 0.50\\n model: str = \\\"sonnet\\\"\\n digest_model: str = \\\"haiku\\\"\\n\\nclass DatabaseSettings(BaseModel):\\n path: str = \\\"~/.local/share/forkhub/forkhub.db\\\"\\n\\nclass SyncSettings(BaseModel):\\n polling_interval: str = \\\"6h\\\"\\n max_forks_per_repo: int = 5000\\n max_github_requests_per_hour: int = 4000\\n\\nclass AnalysisSettings(BaseModel):\\n max_deep_dives_per_fork: int = 10\\n\\nclass EmbeddingSettings(BaseModel):\\n provider: str = \\\"local\\\"\\n model: str = \\\"all-MiniLM-L6-v2\\\"\\n\\nclass DigestSettings(BaseModel):\\n frequency: str = \\\"weekly\\\"\\n day_of_week: str = \\\"monday\\\"\\n time: str = \\\"09:00\\\"\\n min_significance: int = 5\\n backends: list[str] = [\\\"console\\\"]\\n\\nclass TrackingSettings(BaseModel):\\n default_fork_depth: int = 1\\n auto_discover_owned: bool = True\\n track_sibling_forks: bool = True\\n\\nclass ForkHubSettings(BaseSettings):\\n github: GitHubSettings = GitHubSettings()\\n anthropic: AnthropicSettings = AnthropicSettings()\\n database: DatabaseSettings = DatabaseSettings()\\n sync: SyncSettings = SyncSettings()\\n analysis: AnalysisSettings = AnalysisSettings()\\n embedding: EmbeddingSettings = EmbeddingSettings()\\n digest: DigestSettings = DigestSettings()\\n tracking: TrackingSettings = TrackingSettings()\\n```\\n\\n### Config source priority\\nOverride `settings_customise_sources`: init args \\u003e env vars \\u003e TOML file \\u003e defaults\\n\\n### TOML file resolution\\nSearch order: `./forkhub.toml`, then `~/.config/forkhub/forkhub.toml`\\n\\n### Env var format\\nPrefix: `FORKHUB_`, nested delimiter: `__`\\nExample: `FORKHUB_GITHUB__TOKEN=ghp_xxx`, `FORKHUB_DIGEST__FREQUENCY=daily`\\n\\n### Sensitive field handling\\nToken and api_key fields should use `SecretStr` or be excluded from `__repr__` to prevent accidental logging.\\n\\n### TDD Tests (`tests/test_config.py`)\\n- Test defaults load with no config file and no env vars\\n- Test TOML file loading (create temp toml file)\\n- Test env var overrides work (monkeypatch env)\\n- Test nested config via env vars (e.g., `FORKHUB_DIGEST__FREQUENCY=daily`)\\n- Test config file path resolution (project root found first)\\n- Test sensitive fields (token, api_key) not in repr/str output\\n- Test path expansion (`~` expands to home dir in database.path)\\n\\n### Dependencies\\n- Blocked by: Wave 0 scaffolding (forkhub-2kl)\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"joshua.oliphant@hey.com\",\"created_at\":\"2026-03-02T02:49:07Z\",\"created_by\":\"Joshua Oliphant\",\"updated_at\":\"2026-03-02T02:49:07Z\"}"}
|
|
35
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:05:57Z","event_type":"closed","id":35,"issue_id":"forkhub-ufp","new_value":"Wave 1 complete: all 164 tests passing, lint clean","old_value":""}
|
|
36
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:05:57Z","event_type":"closed","id":36,"issue_id":"forkhub-l7z","new_value":"Wave 1 complete: all 164 tests passing, lint clean","old_value":""}
|
|
37
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:05:57Z","event_type":"closed","id":37,"issue_id":"forkhub-5y4","new_value":"Wave 1 complete: all 164 tests passing, lint clean","old_value":""}
|
|
38
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:05:57Z","event_type":"closed","id":38,"issue_id":"forkhub-158","new_value":"Wave 1 complete: all 164 tests passing, lint clean","old_value":""}
|
|
39
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:06:23Z","event_type":"status_changed","id":39,"issue_id":"forkhub-6e6","new_value":"{\"status\":\"in_progress\"}","old_value":"{\"id\":\"forkhub-6e6\",\"title\":\"GitHub provider (githubkit async)\",\"description\":\"## Wave 2 — Providers (parallelizable with 2.2, 2.3)\\n\\n### File: `src/forkhub/providers/github.py`\\n\\n### What to build\\n`GitHubProvider` class implementing the `GitProvider` Protocol using the `githubkit` library.\\n\\n### Class design\\n- Constructor: `GitHubProvider(token: str)` — creates `GitHub(token, auto_retry=RetryRateLimit(max_retry=2))` client\\n- `asyncio.Semaphore(10)` for concurrent GitHub API call control\\n- All methods are async, return our Pydantic models (not githubkit models)\\n\\n### Method implementations\\n1. `get_user_repos(username)` — `github.rest.repos.async_list_for_authenticated_user()` with pagination via `github.rest.paginate()`, map to `list[RepoInfo]`\\n2. `get_forks(owner, repo, page)` — `github.rest.repos.async_list_forks()` with pagination, map to `ForkPage`. Calculate `has_diverged` by checking if fork's pushed_at \\u003e created_at.\\n3. `compare(owner, repo, base, head)` — `github.rest.repos.async_compare_commits(owner, repo, basehead=f\\\"{base}...{head}\\\")`, map to `CompareResult` with files and commits\\n4. `get_releases(owner, repo, since)` — `github.rest.repos.async_list_releases()`, filter by `published_at \\u003e since`, map to `list[Release]`\\n5. `get_repo(owner, repo)` — `github.rest.repos.async_get()`, map to `RepoInfo`\\n6. `get_commit_messages(owner, repo, since)` — `github.rest.repos.async_list_commits(since=since)`, map to `list[CommitInfo]`\\n7. `get_file_diff(owner, repo, base, head, path)` — compare then extract the specific file's patch string from the files list\\n8. `get_rate_limit()` — `github.rest.rate_limit.async_get()`, map to `RateLimitInfo`\\n\\n### Mapping helpers\\nPrivate `_to_repo_info()`, `_to_fork_info()`, etc. methods to convert githubkit response models to our Pydantic models.\\n\\n### Error handling\\n- `PrimaryRateLimitExceeded` — re-raise as custom `RateLimitError`\\n- `RequestFailed` with 404 — re-raise as custom `RepoNotFoundError`\\n- `RequestFailed` with other codes — re-raise as custom `GitProviderError`\\n- Define these exceptions in `src/forkhub/exceptions.py`\\n\\n### ETag caching\\ngithubkit has built-in HTTP cache support — leverage it. Unchanged resources return 304 and use cached data automatically.\\n\\n### TDD Tests (`tests/test_github_provider.py`)\\nUse `respx` (httpx mock library) since githubkit uses httpx internally:\\n- Test each of the 8 methods maps API responses to correct Pydantic models\\n- Test pagination for repos and forks (multi-page responses)\\n- Test rate limit info extraction\\n- Test error handling: 404 raises RepoNotFoundError, 403 rate limit raises RateLimitError\\n- Test semaphore limits concurrency\\n- Fixture files: `tests/fixtures/github/repos.json`, `forks.json`, `compare.json`, `releases.json`, `rate_limit.json`\\n\\n### Dependencies\\n- Blocked by: Wave 1 (models, interfaces, config)\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"joshua.oliphant@hey.com\",\"created_at\":\"2026-03-02T02:49:28Z\",\"created_by\":\"Joshua Oliphant\",\"updated_at\":\"2026-03-02T02:49:28Z\"}"}
|
|
40
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:06:24Z","event_type":"status_changed","id":40,"issue_id":"forkhub-dpd","new_value":"{\"status\":\"in_progress\"}","old_value":"{\"id\":\"forkhub-dpd\",\"title\":\"Local embedding provider (sentence-transformers)\",\"description\":\"## Wave 2 — Providers (parallelizable with 2.1, 2.3)\\n\\n### File: `src/forkhub/embeddings/local.py`\\n\\n### What to build\\n`LocalEmbeddingProvider` implementing the `EmbeddingProvider` Protocol using sentence-transformers with the `all-MiniLM-L6-v2` model.\\n\\n### Class design\\n```python\\nclass LocalEmbeddingProvider:\\n def __init__(self, model_name: str = \\\"all-MiniLM-L6-v2\\\"):\\n self._model_name = model_name\\n self._model: SentenceTransformer | None = None # Lazy loaded\\n\\n async def embed(self, texts: list[str]) -\\u003e list[list[float]]:\\n # Lazy load model on first call\\n if self._model is None:\\n self._model = await asyncio.to_thread(SentenceTransformer, self._model_name)\\n # sentence-transformers is sync, wrap in to_thread\\n embeddings = await asyncio.to_thread(self._model.encode, texts)\\n return embeddings.tolist()\\n\\n def dimensions(self) -\\u003e int:\\n return 384 # all-MiniLM-L6-v2 output dimensions\\n```\\n\\n### Key design decisions\\n- **Lazy loading**: Model is NOT loaded at construction time. First `embed()` call triggers download/load (~22MB model, ~80MB with deps). This keeps import time fast.\\n- **Thread wrapping**: `model.encode()` is synchronous and CPU-bound. `asyncio.to_thread()` prevents blocking the event loop.\\n- **Return type**: Convert numpy array to `list[list[float]]` via `.tolist()` for clean Pydantic/JSON serialization.\\n\\n### TDD Tests (`tests/test_embeddings.py`)\\nThese are real integration tests — they load the actual model (per project rules: no mocks):\\n- Test `embed()` returns correct dimensions (384 per text)\\n- Test batch embedding of multiple texts returns correct count\\n- Test single text embedding\\n- Test empty list input returns empty list\\n- Test lazy loading: model is None after construction, loaded after first embed()\\n- Test `dimensions()` returns 384\\n- Test embeddings are normalized (cosine similarity friendly)\\n- Test similar texts produce closer embeddings than dissimilar texts\\n\\n### Dependencies\\n- Blocked by: Wave 1 (interfaces)\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"joshua.oliphant@hey.com\",\"created_at\":\"2026-03-02T02:49:38Z\",\"created_by\":\"Joshua Oliphant\",\"updated_at\":\"2026-03-02T02:49:38Z\"}"}
|
|
41
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:06:24Z","event_type":"status_changed","id":41,"issue_id":"forkhub-8ej","new_value":"{\"status\":\"in_progress\"}","old_value":"{\"id\":\"forkhub-8ej\",\"title\":\"Console notification backend (Rich)\",\"description\":\"## Wave 2 — Providers (parallelizable with 2.1, 2.2)\\n\\n### Files\\n- `src/forkhub/notifications/console.py`\\n- `src/forkhub/cli/formatting.py`\\n\\n### What to build\\n`ConsoleBackend` implementing `NotificationBackend` Protocol. Renders digests to terminal using Rich panels, tables, and formatting matching the digest example from spec §3.6.\\n\\n### ConsoleBackend class\\n```python\\nclass ConsoleBackend:\\n def __init__(self, console: Console | None = None):\\n self._console = console or Console()\\n\\n async def deliver(self, digest: Digest) -\\u003e DeliveryResult:\\n # Render digest body to console using Rich\\n # Return DeliveryResult(success=True, backend_name=\\\"console\\\")\\n\\n def backend_name(self) -\\u003e str:\\n return \\\"console\\\"\\n```\\n\\n### Formatting helpers (`formatting.py`)\\nShared Rich formatting utilities used by both the notification backend and CLI commands:\\n- `signal_icon(category: SignalCategory) -\\u003e str` — maps categories to icons (⭐ feature, 🔧 fix, 🏷️ release, 📦 cluster, etc.)\\n- `significance_style(score: int) -\\u003e str` — Rich style based on score (red for 8+, yellow for 5-7, dim for \\u003c5)\\n- `format_signal(signal: Signal) -\\u003e Panel` — Rich panel for a single signal\\n- `format_cluster(cluster: Cluster) -\\u003e Panel` — Rich panel for a cluster callout\\n- `format_digest_section(title: str, signals: list[Signal], clusters: list[Cluster]) -\\u003e Group` — section with header (YOUR REPOS, UPSTREAM CHANGES, WATCHED REPOS)\\n- `format_digest(digest: Digest) -\\u003e Group` — full digest rendering\\n\\n### Digest format (match spec §3.6)\\n```\\n══════════════════════════════════════════════════════════════\\n ForkHub Weekly Digest — March 1, 2026\\n══════════════════════════════════════════════════════════════\\n\\nYOUR REPOS\\n───────────────────────────────────────────────────────────\\n myproject (3 new signals across 2 forks)\\n\\n ⭐ alice/myproject — feature (significance: 8)\\n Added WebSocket support for real-time updates.\\n\\n 📦 CLUSTER FORMING: \\\"Connection pool improvements\\\" (2 forks)\\n ...\\n```\\n\\n### TDD Tests (`tests/test_notifications.py`)\\n- Test `deliver()` returns successful DeliveryResult\\n- Test `backend_name()` returns \\\"console\\\"\\n- Test formatting produces expected Rich output (use `Console(file=StringIO())` to capture)\\n- Test with empty digest (no signals)\\n- Test with digest containing all signal categories\\n- Test cluster formatting\\n- Test significance color coding\\n\\n### Dependencies\\n- Blocked by: Wave 1 (models, interfaces)\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"joshua.oliphant@hey.com\",\"created_at\":\"2026-03-02T02:49:52Z\",\"created_by\":\"Joshua Oliphant\",\"updated_at\":\"2026-03-02T02:49:52Z\"}"}
|
|
42
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:16:21Z","event_type":"closed","id":42,"issue_id":"forkhub-6e6","new_value":"Closed","old_value":""}
|
|
43
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:16:21Z","event_type":"closed","id":43,"issue_id":"forkhub-dpd","new_value":"Closed","old_value":""}
|
|
44
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:16:21Z","event_type":"closed","id":44,"issue_id":"forkhub-8ej","new_value":"Closed","old_value":""}
|
|
45
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:17:03Z","event_type":"status_changed","id":45,"issue_id":"forkhub-v0i","new_value":"{\"status\":\"in_progress\"}","old_value":"{\"id\":\"forkhub-v0i\",\"title\":\"Tracker service (repo discovery and management)\",\"description\":\"## Wave 3 — Services (parallelizable with 3.3, 3.4)\\n\\n### File: `src/forkhub/services/tracker.py`\\n\\n### What to build\\n`TrackerService` manages tracked repositories — add, remove, discover owned repos, exclude/include.\\n\\n### Class: `TrackerService(db: Database, provider: GitProvider)`\\n\\n### Methods\\n- `discover_owned_repos(username: str) -\\u003e list[TrackedRepo]` — fetches user repos via GitProvider, inserts new ones as tracking_mode=\\\"owned\\\", skips already-tracked repos, respects excluded flag. On each call, discovers newly created repos.\\n- `track_repo(owner: str, repo: str, mode: TrackingMode = TrackingMode.watched, depth: int = 1) -\\u003e TrackedRepo` — adds repo to tracking. Fetches repo info from GitHub to populate metadata. Raises if already tracked.\\n- `untrack_repo(owner: str, repo: str) -\\u003e None` — removes from tracking (cascades delete forks, signals via FK)\\n- `exclude_repo(repo_name: str) -\\u003e None` — sets excluded=True on an owned repo\\n- `include_repo(repo_name: str) -\\u003e None` — sets excluded=False\\n- `list_tracked_repos(mode: TrackingMode | None = None) -\\u003e list[TrackedRepo]` — filtered list, excludes excluded repos unless specifically requested\\n- `detect_upstream_repos(username: str) -\\u003e list[TrackedRepo]` — finds repos the user has forked (is_fork=True in their repo list), gets the parent repo info, tracks the parent with mode=\\\"upstream\\\"\\n\\n### TDD Tests (`tests/test_tracker.py`)\\nUse real in-memory SQLite + a stub GitProvider (a real class that implements the Protocol with canned data, not a mock framework):\\n- Test discover_owned_repos adds new repos, returns them\\n- Test discover_owned_repos skips already-tracked repos on second call\\n- Test discover_owned_repos respects excluded flag\\n- Test track_repo adds a watched repo with correct metadata\\n- Test track_repo raises on duplicate\\n- Test untrack_repo removes repo and cascaded data\\n- Test exclude/include toggling\\n- Test list_tracked_repos filters by mode\\n- Test detect_upstream_repos identifies forked repos and tracks their parents\\n\\n### Dependencies\\n- Blocked by: Wave 2 (GitHub provider, database)\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"joshua.oliphant@hey.com\",\"created_at\":\"2026-03-02T02:50:08Z\",\"created_by\":\"Joshua Oliphant\",\"updated_at\":\"2026-03-02T02:50:08Z\"}"}
|
|
46
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:17:03Z","event_type":"status_changed","id":46,"issue_id":"forkhub-5xt","new_value":"{\"status\":\"in_progress\"}","old_value":"{\"id\":\"forkhub-5xt\",\"title\":\"Sync service (fork discovery, comparison, change detection)\",\"description\":\"## Wave 3 — Services (depends on Tracker service)\\n\\n### File: `src/forkhub/services/sync.py`\\n\\n### What to build\\n`SyncService` orchestrates the sync pipeline: discover forks, compare HEAD SHAs, detect changes, check releases, update vitality/stars.\\n\\n### Class: `SyncService(db: Database, provider: GitProvider, tracker: TrackerService, settings: SyncSettings)`\\n\\n### Data types\\n```python\\n@dataclass\\nclass RepoSyncResult:\\n repo: TrackedRepo\\n new_forks: int\\n changed_forks: list[Fork]\\n new_releases: list[Release]\\n errors: list[str]\\n\\n@dataclass\\nclass SyncResult:\\n repos_synced: int\\n total_changed_forks: int\\n total_new_releases: int\\n results: list[RepoSyncResult]\\n errors: list[str]\\n```\\n\\n### Methods\\n- `sync_all() -\\u003e SyncResult` — syncs all non-excluded tracked repos. Uses `asyncio.gather()` with semaphore for concurrency.\\n- `sync_repo(repo: TrackedRepo) -\\u003e RepoSyncResult`:\\n 1. Discover forks via `provider.get_forks()` (paginated), save/update in DB\\n 2. For each fork: compare current HEAD SHA with stored `fork.head_sha` — skip if unchanged\\n 3. For changed forks: fetch `provider.compare()` data, update fork record (commits_ahead, commits_behind, head_sha)\\n 4. Check for new releases via `provider.get_releases(since=repo.last_synced_at)`\\n 5. Update fork vitality based on `last_pushed_at`\\n 6. Update star counts and calculate velocity (stars - stars_previous)\\n 7. Update `repo.last_synced_at`\\n 8. Return RepoSyncResult with changed forks and new releases\\n- `_classify_vitality(last_pushed_at: datetime | None) -\\u003e ForkVitality` — active if pushed within 90 days, dormant if 90-365, dead if \\u003e365, unknown if no push date\\n\\n### Error handling\\n- If a single fork comparison fails (404 — fork deleted, etc.), log error and continue with remaining forks\\n- If rate limit is hit, stop sync early and report partial results\\n\\n### TDD Tests (`tests/test_sync.py`)\\nUse real in-memory SQLite + stub GitProvider:\\n- Test full sync pipeline: discovers forks, compares, returns changed list\\n- Test HEAD SHA comparison: unchanged forks are skipped\\n- Test changed forks get updated head_sha, commits_ahead, commits_behind\\n- Test vitality classification (active/dormant/dead boundaries)\\n- Test star velocity calculation\\n- Test new release detection with since filtering\\n- Test error handling: individual fork failure doesn't stop sync\\n- Test sync_all aggregates results from multiple repos\\n- Test last_synced_at gets updated\\n\\n### Dependencies\\n- Blocked by: Tracker service, Wave 2 (providers)\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"joshua.oliphant@hey.com\",\"created_at\":\"2026-03-02T02:50:22Z\",\"created_by\":\"Joshua Oliphant\",\"updated_at\":\"2026-03-02T02:50:22Z\"}"}
|
|
47
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:17:03Z","event_type":"status_changed","id":47,"issue_id":"forkhub-wtq","new_value":"{\"status\":\"in_progress\"}","old_value":"{\"id\":\"forkhub-wtq\",\"title\":\"Cluster detection service (vector similarity)\",\"description\":\"## Wave 3 — Services (parallelizable with 3.1, 3.4)\\n\\n### File: `src/forkhub/services/cluster.py`\\n\\n### What to build\\n`ClusterService` detects when multiple forks make similar changes independently using vector similarity on signal embeddings.\\n\\n### Class: `ClusterService(db: Database, embedding_provider: EmbeddingProvider)`\\n\\n### Methods\\n- `update_clusters(repo_id: str) -\\u003e list[Cluster]` — main entry point:\\n 1. Get all signals for this repo that don't yet have embeddings in vec_signals\\n 2. Generate embeddings via `embedding_provider.embed([signal.summary for signal in unembedded])`\\n 3. Store embeddings in vec_signals table via `db.save_signal_embedding()`\\n 4. For each recent signal (created in last sync), search for similar signals from DIFFERENT forks via `db.search_similar_signals()`\\n 5. If distance \\u003c threshold (default 0.3) and signals are from different forks, create or update a cluster\\n 6. When updating existing cluster: add the new signal as a member, increment fork_count if new fork, update description\\n 7. Return new or updated clusters\\n- `get_clusters(repo_id: str, min_size: int = 2) -\\u003e list[Cluster]` — retrieve existing clusters filtered by minimum fork count\\n- `_should_cluster(signal_a: Signal, signal_b: Signal, distance: float, threshold: float = 0.3) -\\u003e bool`:\\n - Must be from different forks (signal_a.fork_id != signal_b.fork_id)\\n - Distance must be below threshold\\n - Should have overlapping files_involved (at least one common file path or directory)\\n- `_generate_cluster_label(signals: list[Signal]) -\\u003e str` — derive a human-readable label from the common files/categories\\n\\n### TDD Tests (`tests/test_clusters.py`)\\nUse real in-memory SQLite + real LocalEmbeddingProvider (per project rules: no mocks):\\n- Test embedding generation and storage in vec_signals\\n- Test similar signals from different forks form a cluster\\n- Test similar signals from the SAME fork do NOT cluster\\n- Test cluster growth: new signal joins existing cluster, fork_count updates\\n- Test distance threshold filtering: far signals don't cluster\\n- Test _should_cluster with overlapping vs non-overlapping files\\n- Test get_clusters respects min_size filter\\n- Test cluster label generation\\n\\n### Dependencies\\n- Blocked by: Wave 2 (database with vec_signals, embedding provider)\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"joshua.oliphant@hey.com\",\"created_at\":\"2026-03-02T02:50:34Z\",\"created_by\":\"Joshua Oliphant\",\"updated_at\":\"2026-03-02T02:50:34Z\"}"}
|
|
48
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:17:03Z","event_type":"status_changed","id":48,"issue_id":"forkhub-zlr","new_value":"{\"status\":\"in_progress\"}","old_value":"{\"id\":\"forkhub-zlr\",\"title\":\"Digest service (generation and delivery)\",\"description\":\"## Wave 3 — Services (parallelizable with 3.1, 3.3)\\n\\n### File: `src/forkhub/services/digest.py`\\n\\n### What to build\\n`DigestService` generates and delivers periodic digest notifications from accumulated signals.\\n\\n### Class: `DigestService(db: Database, backends: list[NotificationBackend])`\\n\\n### Methods\\n- `generate_digest(config: DigestConfig, since: datetime | None = None) -\\u003e Digest`:\\n 1. Determine time range: `since` override or last digest's created_at for this config\\n 2. Query all signals in the time range via `db.get_signals_since()`\\n 3. Filter by config criteria:\\n - `min_significance`: exclude signals below threshold\\n - `categories`: only include specified categories (None = all)\\n - `file_patterns`: only include signals whose files_involved match any glob pattern (None = all)\\n 4. Group signals by tracked_repo_id\\n 5. Within each repo group, separate into sections: owned repos, upstream signals (is_upstream=True), watched repos\\n 6. Get cluster info for each repo via `db.get_clusters_for_repo()`\\n 7. Build digest title (e.g., \\\"ForkHub Weekly Digest — March 1, 2026\\\")\\n 8. Build digest body using the grouped signals and clusters\\n 9. Save digest to DB, return it\\n- `deliver_digest(digest: Digest) -\\u003e list[DeliveryResult]`:\\n - Call `backend.deliver(digest)` for each backend\\n - Collect all DeliveryResults\\n - Update digest.delivered_at in DB\\n- `generate_and_deliver(config: DigestConfig | None = None, since: datetime | None = None) -\\u003e tuple[Digest, list[DeliveryResult]]` — convenience method. If config is None, use global digest config.\\n- `_matches_file_patterns(signal_files: list[str], patterns: list[str]) -\\u003e bool` — glob matching using `fnmatch`\\n\\n### TDD Tests (`tests/test_digest.py`)\\nUse real in-memory SQLite + a stub NotificationBackend:\\n- Test digest generation with various signal combinations\\n- Test significance filtering: below-threshold signals excluded\\n- Test category filtering: only matching categories included\\n- Test file pattern filtering with glob matching\\n- Test grouping by repo and section (owned vs upstream vs watched)\\n- Test cluster info included in digest\\n- Test delivery dispatches to all backends and collects results\\n- Test deliver updates delivered_at timestamp\\n- Test empty digest when no signals since last digest\\n- Test generate_and_deliver convenience method\\n- Test time range: respects since parameter vs last digest date\\n\\n### Dependencies\\n- Blocked by: Wave 2 (database, notification backends)\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"joshua.oliphant@hey.com\",\"created_at\":\"2026-03-02T02:50:46Z\",\"created_by\":\"Joshua Oliphant\",\"updated_at\":\"2026-03-02T02:50:46Z\"}"}
|
|
49
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:26:56Z","event_type":"closed","id":49,"issue_id":"forkhub-v0i","new_value":"Closed","old_value":""}
|
|
50
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:26:56Z","event_type":"closed","id":50,"issue_id":"forkhub-5xt","new_value":"Closed","old_value":""}
|
|
51
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:26:56Z","event_type":"closed","id":51,"issue_id":"forkhub-wtq","new_value":"Closed","old_value":""}
|
|
52
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:26:56Z","event_type":"closed","id":52,"issue_id":"forkhub-zlr","new_value":"Closed","old_value":""}
|
|
53
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:28:01Z","event_type":"status_changed","id":53,"issue_id":"forkhub-wwz","new_value":"{\"status\":\"in_progress\"}","old_value":"{\"id\":\"forkhub-wwz\",\"title\":\"Agent SDK tools and prompts\",\"description\":\"## Wave 4 — Agent Layer (parallelizable with 4.2)\\n\\n### Files\\n- `src/forkhub/agent/tools.py` — custom tool definitions\\n- `src/forkhub/agent/prompts.py` — system prompts as constants\\n\\n### What to build: Custom Tools\\n\\n7 tools using the `@tool` decorator from `claude-agent-sdk`. Each tool wraps service/provider calls and needs access to Database and GitProvider (use module-level or closure-based dependency injection).\\n\\n**Tools:**\\n\\n1. `list_forks(owner: str, repo: str, page: int = 1, only_active: bool = True) -\\u003e dict`\\n - Wraps GitProvider.get_forks(), filters to active forks if only_active\\n - Returns dict with forks list and pagination info\\n\\n2. `get_fork_summary(fork_full_name: str) -\\u003e dict`\\n - Composite call: get commits ahead/behind, changed file list, recent commit messages\\n - Uses GitProvider.compare() and GitProvider.get_commit_messages()\\n - This is the CHEAP operation the agent should call first\\n\\n3. `get_file_diff(fork_full_name: str, file_path: str) -\\u003e str`\\n - Full diff for one specific file in a fork vs upstream\\n - Uses GitProvider.get_file_diff()\\n - This is the EXPENSIVE operation the agent calls selectively\\n\\n4. `get_releases(owner: str, repo: str, since_days: int = 30) -\\u003e list[dict]`\\n - Wraps GitProvider.get_releases() with date filtering\\n\\n5. `get_fork_stars(fork_full_name: str) -\\u003e dict`\\n - Returns star count + velocity (stars gained since last check)\\n - Reads from DB fork record\\n\\n6. `store_signal(fork_full_name: str, category: str, summary: str, significance: int, files_involved: list[str], detail: str | None = None) -\\u003e dict`\\n - Persists a classified signal to the database\\n - Validates category is a valid SignalCategory\\n - Returns the created signal ID\\n\\n7. `search_similar_signals(summary_text: str, limit: int = 5) -\\u003e list[dict]`\\n - Generates embedding for summary_text, searches vec_signals\\n - Returns similar signals with distances for cluster detection\\n\\nAll tools must:\\n- Return dicts (not Pydantic models) for Agent SDK serialization\\n- Return `{\\\"error\\\": \\\"message\\\"}` with `is_error: True` on failure, never raise exceptions\\n- Have clear docstrings that guide the agent on when/how to use them\\n\\n### What to build: Prompts\\n\\n`prompts.py` contains system prompts as string constants:\\n\\n- `COORDINATOR_PROMPT` — \\\"You are a fork analysis agent. Your job is to understand what's happening across the fork constellation...\\\" Includes strategy guidance (skim first, deep-dive selectively).\\n- `DIFF_ANALYST_PROMPT` — \\\"You are a fork analyst. Given a fork's summary, decide which changes are meaningful...\\\" Strategy: start with get_fork_summary, look at commits for intent, use get_file_diff selectively, call store_signal for findings.\\n- `DIGEST_WRITER_PROMPT` — \\\"You are a technical writer composing a fork activity digest...\\\" Guidelines: lead with significant items, group by repo, highlight clusters, be concise.\\n\\n### TDD Tests (`tests/test_agent_tools.py`)\\nUse real in-memory SQLite + stub GitProvider:\\n- Test each tool function independently\\n- Test list_forks with only_active filtering\\n- Test get_fork_summary returns correct composite data\\n- Test get_file_diff returns diff string\\n- Test store_signal persists correctly, returns signal ID\\n- Test store_signal validates category (rejects invalid)\\n- Test search_similar_signals returns results with distances\\n- Test error handling: tools return error dict on failure\\n\\n### Dependencies\\n- Blocked by: Wave 3 (services, database with data)\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"joshua.oliphant@hey.com\",\"created_at\":\"2026-03-02T02:51:09Z\",\"created_by\":\"Joshua Oliphant\",\"updated_at\":\"2026-03-02T02:51:09Z\"}"}
|
|
54
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:28:01Z","event_type":"status_changed","id":54,"issue_id":"forkhub-dz2","new_value":"{\"status\":\"in_progress\"}","old_value":"{\"id\":\"forkhub-dz2\",\"title\":\"Agent definitions, hooks, and analysis runner\",\"description\":\"## Wave 4 — Agent Layer (parallelizable with 4.1, but runner depends on tools)\\n\\n### Files\\n- `src/forkhub/agent/agents.py` — subagent definitions\\n- `src/forkhub/agent/hooks.py` — Agent SDK hooks\\n- `src/forkhub/agent/runner.py` — analysis orchestration\\n- `src/forkhub/services/analyzer.py` — thin library API wrapper\\n\\n### What to build: Subagent Definitions (`agents.py`)\\n\\n```python\\ndiff_analyst = AgentDefinition(\\n description=\\\"Analyzes a single fork in depth to classify its changes\\\",\\n prompt=DIFF_ANALYST_PROMPT, # from prompts.py\\n tools=[\\\"mcp__forkhub__get_fork_summary\\\", \\\"mcp__forkhub__get_file_diff\\\",\\n \\\"mcp__forkhub__get_releases\\\", \\\"mcp__forkhub__get_fork_stars\\\",\\n \\\"mcp__forkhub__store_signal\\\", \\\"mcp__forkhub__search_similar_signals\\\"],\\n model=\\\"sonnet\\\",\\n)\\n\\ndigest_writer = AgentDefinition(\\n description=\\\"Composes notification digests from accumulated signals\\\",\\n prompt=DIGEST_WRITER_PROMPT,\\n model=\\\"haiku\\\",\\n)\\n```\\n\\n### What to build: Hooks (`hooks.py`)\\n\\n1. `cost_tracker` — PostToolUse hook. Increments API call counter in sync_state for any tool starting with `mcp__forkhub__`. Tracks total calls per tool.\\n2. `rate_limit_guard` — PreToolUse hook. Before GitHub API tool calls (list_forks, get_fork_summary, get_file_diff, get_releases, get_fork_stars), checks rate limit. If remaining \\u003c 100, returns deny with message telling agent to focus on already-fetched data.\\n3. `pre_compact` — PreCompact hook. Logs the compaction event. Analysis progress is safe in DB (stored via store_signal tool calls).\\n\\n### What to build: Analysis Runner (`runner.py`)\\n\\n`AnalysisRunner(db: Database, provider: GitProvider, embedding_provider: EmbeddingProvider, settings: ForkHubSettings)`\\n\\nMain method: `analyze_repo(repo: TrackedRepo, changed_forks: list[Fork], new_releases: list[Release]) -\\u003e list[Signal]`:\\n1. Create MCP server from tools via `create_sdk_mcp_server(name=\\\"forkhub\\\", tools=[...])`\\n2. Configure `ClaudeAgentOptions`:\\n - `mcp_servers={\\\"forkhub\\\": mcp_server}`\\n - `agents={\\\"diff-analyst\\\": diff_analyst, \\\"digest-writer\\\": digest_writer}`\\n - `hooks` with cost_tracker, rate_limit_guard, pre_compact\\n - `max_budget_usd` from settings.anthropic.analysis_budget_usd\\n - `model` from settings.anthropic.model\\n3. Build coordinator prompt with: repo context, list of changed forks (names, commits ahead, files changed count), new releases summary\\n4. Start `ClaudeSDKClient` session, run `client.query()` with coordinator prompt\\n5. After session completes, query DB for all signals created during this analysis (by created_at \\u003e session_start)\\n6. Return the signals list\\n\\n**Batching for large constellations:**\\n- If len(changed_forks) \\u003e 30, split into batches of 30\\n- Run separate agent sessions per batch\\n- Aggregate signals across batches\\n\\n### Analyzer wrapper (`services/analyzer.py`)\\nThin wrapper that the library API (`ForkHub` class) calls:\\n```python\\nclass AnalyzerService:\\n def __init__(self, runner: AnalysisRunner):\\n self._runner = runner\\n\\n async def analyze(self, repo, changed_forks, new_releases) -\\u003e list[Signal]:\\n return await self._runner.analyze_repo(repo, changed_forks, new_releases)\\n```\\n\\n### TDD Tests\\n- `tests/test_agent_hooks.py`:\\n - Test cost_tracker increments counter in sync_state\\n - Test rate_limit_guard blocks when remaining \\u003c 100\\n - Test rate_limit_guard allows when remaining \\u003e= 100\\n- `tests/test_agent_runner.py`:\\n - Test coordinator prompt construction includes repo context and fork summary\\n - Test MCP server creation registers all 7 tools\\n - Test options configuration has correct budget, model, hooks\\n - Test batching: 60 forks split into 2 batches of 30\\n - Integration test (mark @pytest.mark.integration, needs ANTHROPIC_API_KEY): full analysis run\\n\\n### Dependencies\\n- Blocked by: Agent tools (4.1), Wave 3 services\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"joshua.oliphant@hey.com\",\"created_at\":\"2026-03-02T02:51:31Z\",\"created_by\":\"Joshua Oliphant\",\"updated_at\":\"2026-03-02T02:51:31Z\"}"}
|
|
55
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:38:16Z","event_type":"closed","id":55,"issue_id":"forkhub-wwz","new_value":"Closed","old_value":""}
|
|
56
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:38:16Z","event_type":"closed","id":56,"issue_id":"forkhub-dz2","new_value":"Closed","old_value":""}
|
|
57
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:38:42Z","event_type":"status_changed","id":57,"issue_id":"forkhub-3fr","new_value":"{\"status\":\"in_progress\"}","old_value":"{\"id\":\"forkhub-3fr\",\"title\":\"CLI commands: init, track, repos, forks, clusters, sync, digest, config\",\"description\":\"## Wave 5 — CLI Layer\\n\\n### Files\\nAll in `src/forkhub/cli/`:\\n- `app.py` — Root Typer app, composes all subcommands\\n- `init_cmd.py` — `forkhub init`\\n- `track_cmd.py` — `track`, `untrack`, `exclude`, `include`\\n- `repos_cmd.py` — `repos`\\n- `forks_cmd.py` — `forks`, `inspect`\\n- `clusters_cmd.py` — `clusters`\\n- `sync_cmd.py` — `sync`\\n- `digest_cmd.py` — `digest`, `digest-config`\\n- `config_cmd.py` — `config show/set/path`\\n\\n### Architecture principle\\nCLI layer is THIN. Each command:\\n1. Parses arguments (Typer handles this)\\n2. Loads config, initializes services\\n3. Calls library service method\\n4. Formats output with Rich (using formatting.py helpers)\\n\\n### Command specifications\\n\\n**init** (`init_cmd.py`):\\n- `forkhub init --user \\u003cgithub_username\\u003e --token \\u003cgithub_token\\u003e`\\n- Interactive first-time setup. Stores token in config file, discovers owned repos, detects upstream repos\\n- Shows Rich table of discovered repos with [owned]/[upstream] labels\\n- Creates config directory if needed\\n\\n**track/untrack/exclude/include** (`track_cmd.py`):\\n- `forkhub track \\u003cowner/repo\\u003e --depth \\u003cn\\u003e` — calls TrackerService.track_repo, shows confirmation\\n- `forkhub untrack \\u003cowner/repo\\u003e` — calls TrackerService.untrack_repo, shows confirmation\\n- `forkhub exclude \\u003crepo_name\\u003e` — calls TrackerService.exclude_repo\\n- `forkhub include \\u003crepo_name\\u003e` — calls TrackerService.include_repo\\n\\n**repos** (`repos_cmd.py`):\\n- `forkhub repos [--owned|--watched|--upstream]` — Rich table: name, mode, forks count, last synced, excluded status\\n\\n**forks** (`forks_cmd.py`):\\n- `forkhub forks \\u003cowner/repo\\u003e --active --sort [significance|stars|recent|ahead] --category \\u003ccat\\u003e --limit \\u003cn\\u003e`\\n- Rich table: fork name, vitality, stars, commits ahead, top signal summary, significance\\n- `forkhub inspect \\u003cfork_full_name\\u003e` — detailed view: all signals for this fork, star history, files changed\\n\\n**clusters** (`clusters_cmd.py`):\\n- `forkhub clusters \\u003cowner/repo\\u003e --min-size \\u003cn\\u003e` — Rich panels showing each cluster: label, description, fork count, member signals\\n\\n**sync** (`sync_cmd.py`):\\n- `forkhub sync [--repo \\u003cowner/repo\\u003e] [--full]`\\n- Shows Rich progress bar during sync\\n- After sync: summary of forks discovered, forks changed, signals created, releases found\\n- `--full` forces re-crawl ignoring HEAD SHA cache\\n\\n**digest** (`digest_cmd.py`):\\n- `forkhub digest [--since \\u003cdate\\u003e] [--dry-run] [--repo \\u003cowner/repo\\u003e]`\\n- `--dry-run` generates digest and prints to console without marking as delivered\\n- `forkhub digest-config --frequency [daily|weekly|on_demand] --day [mon-sun] --time HH:MM --min-significance \\u003cn\\u003e --categories \\u003clist\\u003e --files \\u003cpatterns\\u003e --backends \\u003clist\\u003e`\\n\\n**config** (`config_cmd.py`):\\n- `forkhub config show` — pretty-print current config (mask sensitive values)\\n- `forkhub config set \\u003ckey\\u003e \\u003cvalue\\u003e` — set a config value in TOML file\\n- `forkhub config path` — show config file location\\n\\n### Common pattern for all commands\\n```python\\nimport asyncio\\nfrom forkhub.config import ForkHubSettings\\nfrom forkhub.database import Database\\n\\ndef _get_services():\\n \\\"\\\"\\\"Initialize config, DB, and services. Used by all commands.\\\"\\\"\\\"\\n settings = ForkHubSettings()\\n db = Database(settings.database.path)\\n # ... return services tuple\\n```\\n\\n### TDD Tests (`tests/test_cli.py` or split per command)\\nUse Typer's `CliRunner`:\\n- Test each command with valid inputs produces expected output\\n- Test error cases (repo not found, not initialized, invalid args)\\n- Test `--help` for every command\\n- Test init creates config file\\n- Test repos/forks/clusters display correct table output\\n- For sync/digest: test that they invoke the correct service methods\\n\\n### Dependencies\\n- Non-agent commands (repos, forks, track, config, clusters) can start once Wave 3 services exist\\n- Agent-dependent commands (sync with analysis, digest) wait for Wave 4\\n- All commands need Wave 1 (models, config, database)\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"joshua.oliphant@hey.com\",\"created_at\":\"2026-03-02T02:51:58Z\",\"created_by\":\"Joshua Oliphant\",\"updated_at\":\"2026-03-02T02:51:58Z\"}"}
|
|
58
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:51:58Z","event_type":"closed","id":58,"issue_id":"forkhub-3fr","new_value":"Closed","old_value":""}
|
|
59
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T19:52:12Z","event_type":"status_changed","id":59,"issue_id":"forkhub-1ji","new_value":"{\"status\":\"in_progress\"}","old_value":"{\"id\":\"forkhub-1ji\",\"title\":\"ForkHub public API class and integration tests\",\"description\":\"## Wave 6 — Public API + Integration\\n\\n### Files\\n- `src/forkhub/__init__.py` — ForkHub class (public API)\\n- `tests/test_integration.py` — end-to-end tests\\n\\n### What to build: ForkHub Class\\n\\nThe main entry point for library consumers. Composes all services and exposes a clean async API.\\n\\n```python\\nclass ForkHub:\\n def __init__(\\n self,\\n settings: ForkHubSettings | None = None,\\n git_provider: GitProvider | None = None,\\n notification_backends: list[NotificationBackend] | None = None,\\n embedding_provider: EmbeddingProvider | None = None,\\n ):\\n # Load settings (or use provided)\\n # Create default providers if not injected:\\n # - GitHubProvider(settings.github.token) if git_provider is None\\n # - [ConsoleBackend()] if notification_backends is None\\n # - LocalEmbeddingProvider() if embedding_provider is None\\n # Initialize Database from settings.database.path\\n # Wire up all services: TrackerService, SyncService, ClusterService, DigestService, AnalyzerService\\n\\n async def __aenter__(self): await self._db.init(); return self\\n async def __aexit__(self, *args): await self._db.close()\\n\\n async def init(self, username: str) -\\u003e list[TrackedRepo]:\\n \\\"\\\"\\\"First-time setup: discover owned repos, detect upstreams.\\\"\\\"\\\"\\n async def track(self, owner: str, repo: str, mode: TrackingMode = TrackingMode.watched, depth: int = 1) -\\u003e TrackedRepo: ...\\n async def untrack(self, owner: str, repo: str) -\\u003e None: ...\\n async def sync(self, repo: str | None = None, full: bool = False) -\\u003e SyncResult:\\n \\\"\\\"\\\"Full sync pipeline: discover forks, compare, analyze, cluster.\\\"\\\"\\\"\\n async def get_repos(self, mode: TrackingMode | None = None) -\\u003e list[TrackedRepo]: ...\\n async def get_forks(self, owner: str, repo: str, active_only: bool = False) -\\u003e list[Fork]: ...\\n async def get_clusters(self, owner: str, repo: str, min_size: int = 2) -\\u003e list[Cluster]: ...\\n async def generate_digest(self, since: datetime | None = None, repo: str | None = None) -\\u003e Digest: ...\\n async def deliver_digest(self, digest: Digest) -\\u003e list[DeliveryResult]: ...\\n```\\n\\n### Key design decisions\\n- Async context manager for DB lifecycle\\n- All providers are injectable (Protocol-based) for testing and customization\\n- Default providers created from settings if not provided\\n- Library consumers can swap any provider (e.g., custom GitProvider for GitLab)\\n\\n### What to build: Integration Tests\\n\\nFull pipeline tests exercising real DB + real embeddings + respx-mocked GitHub HTTP:\\n\\n1. **Full lifecycle test**: init(username) → discovers repos → sync() → signals stored in DB → clusters detected → generate_digest() → digest has content → deliver_digest() → console output\\n2. **Watched repo test**: track(\\\"vercel/next.js\\\") → sync() → forks discovered → signals → digest includes watched section\\n3. **Upstream tracking test**: init finds user's forks → upstream parents tracked → sync() → upstream releases detected → signal with is_upstream=True\\n4. **Empty sync test**: sync() when no forks have changed → no signals, no analysis triggered\\n5. **Custom provider injection test**: pass custom GitProvider and verify it's used instead of default\\n\\n### TDD Tests (`tests/test_forkhub_api.py`)\\n- Test ForkHub class construction with defaults\\n- Test custom provider injection\\n- Test async context manager (aenter/aexit)\\n- Test all public methods delegate to correct services\\n\\n### Dependencies\\n- Blocked by: Waves 4 and 5 complete (all services, agent layer, CLI)\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"joshua.oliphant@hey.com\",\"created_at\":\"2026-03-02T02:52:20Z\",\"created_by\":\"Joshua Oliphant\",\"updated_at\":\"2026-03-02T02:52:20Z\"}"}
|
|
60
|
+
{"actor":"Joshua Oliphant","comment":null,"created_at":"2026-03-01T20:01:42Z","event_type":"closed","id":60,"issue_id":"forkhub-1ji","new_value":"Closed","old_value":""}
|