# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What is project-hub

Multi-repo workspace manager that unifies local git state and GitLab forge data (MRs, pipelines, vulnerabilities) into a single MCP server + web dashboard. Designed for engineers managing many repos on GitLab from a single parent directory.

Single Python process, dual interfaces: MCP (stdio for Claude Code) + HTTP (FastAPI/htmx dashboard). Background asyncio task refreshes git+forge data on a TTL cycle, diffs with SQLite cache to generate events.

## Build & Run

```bash
uv sync                          # install deps
uv run project-hub --help        # CLI help
uv run project-hub               # standalone dashboard (opens browser)
uv run project-hub --mcp         # MCP mode (stdio, embeds webui)
uv run project-hub init          # bootstrap .project-hub/ workspace
uv run project-hub auth login <instance>  # store PAT
uv run project-hub auth status
uv run project-hub exclude <repo>
uv run project-hub include <repo>
uv run project-hub sync-now      # force immediate refresh
```

## Testing

```bash
uv run pytest tests/ -v                    # full suite
uv run pytest tests/test_events.py -v      # single file
uv run pytest tests/test_cache.py -k "test_store_mrs" -v  # single test
```

`asyncio_mode = "auto"` is set in pyproject.toml — async tests run without explicit markers.

## Linting

```bash
pylint project_hub/ --max-line-length=120    # target: no E/W; C/R are advisory
pyflakes project_hub/                        # must be clean (zero output)
radon cc project_hub/ -s -n C                # C is tolerated if justified; D+ must be fixed
```

Linters are installed system-wide (not in venv). Run before pushing.

## Architecture

```
project_hub/
  cli.py                   # Click entry points (standalone / --mcp / auth / init / exclude / include / sync-now)
  core/
    workspace.py           # Workspace orchestrator: start → discover → refresh loop → stop
                           # Also: WorkspaceState enum + detect_state() for dormancy
    cache.py               # SQLite (WAL mode, aiosqlite) — MRs, issues, pipelines, vulns, events, pull_status, ui_state
    config.py              # YAML config loading with defaults
    discovery.py           # Scan CWD children for .git/, parse remotes, classify forges
    models.py              # All dataclasses: Repo, GitStatus, MR, Issue, Pipeline, Job, Vulnerability, Event, PullStatus, RepoResult
    events.py              # Diff old vs new state → Event list + filter_events (mine/all/none)
  forge/
    provider.py            # ForgeProvider ABC + AuthenticationError + supported_features
    auth.py                # PAT resolution: auth.json → env vars (GITLAB_TOKEN/GITHUB_TOKEN/BITBUCKET_TOKEN)
    gitlab.py              # python-gitlab + gql wrapper (one instance per forge host)
    github.py              # PyGithub wrapper
    bitbucket.py           # atlassian-python-api Cloud wrapper (PRs, issues, pipelines — no vulns)
  git/
    ops.py                 # fetch, status, pull_safe (conflict detection via git merge-tree)
  mcp/
    server.py              # FastMCP server: lifespan, tools, resources, notifications
  web/
    app.py                 # FastAPI app factory, routes, htmx fragments
    templates/             # Jinja2: dashboard, MRs, issues, vulns, pipelines, events
    static/                # CSS + vendored htmx.min.js
```

### Key design decisions

- **Core API is framework-agnostic.** `workspace.py`, `cache.py`, `events.py` have zero MCP/FastAPI imports. Both interfaces are thin adapters.
- **stdout reserved for MCP protocol.** All logging goes to stderr (`logging.basicConfig(stream=sys.stderr)`). No transitive dependency may write to stdout.
- **python-gitlab is synchronous.** All forge calls wrapped with `asyncio.to_thread()` and bounded by a semaphore (default 10 concurrent).
- **Conflict detection is non-destructive.** Uses `git merge-tree --write-tree` (Git 2.38+ required) — in-memory merge, no worktree mutations.
- **Event persistence.** Events stored in SQLite with `seen` boolean, survive across sessions. `AUTH_EXPIRED` events always bypass watch filters.

### Dormancy (4 states)

| Condition | State | MCP behavior |
|---|---|---|
| `.no-project-hub` in CWD | Disabled (opt-out) | Zero tools |
| `.project-hub/` in CWD | Active | Full tools + refresh |
| `.project-hub/` in parent only | Disabled (sub-repo) | Zero tools |
| Neither | Dormant | Only `init` tool |

`.no-project-hub` always wins. After `init`, the server sends `send_tool_list_changed()` so the client re-fetches tools.

### Refresh cycle (Workspace._do_refresh_inner)

1. `git fetch --all` on all repos (parallel subprocess)
2. Pull status computation per repo (parallel `check_pull_status_sync`)
3. Stale branch detection per repo (parallel `git_status`), generates `BRANCH_STALE` events for branches whose upstream was deleted
4. Fetch MRs, pipelines, vulns per forge repo (parallel, semaphore-bounded)
5. Vuln reconciliation: for repos on a non-default branch, calls `get_pipeline_findings` on the branch pipeline and marks vulns as `fixed_in_branch` if absent from findings
6. Diff new state against cache → generate events
7. Filter events by `watch` config (mine/all/none)
8. Write to cache per-repo inside the results loop (not atomic across all repos) + emit MCP resource notifications

### Workspace layout

```
.project-hub/
  config.yaml      # auto-generated on init
  cache.db         # SQLite (created on first refresh)
  workspace.md     # manual business notes
```

Auth tokens: `~/.config/project-hub/auth.json` (global, keyed by forge host).

## MCP tools

`init`, `configure_forge`, `list_repos`, `repo_status`, `fetch_all`, `pull_safe`, `list_mrs`, `list_issues`, `list_pipelines`, `get_pipeline_jobs`, `get_job_log`, `get_vulnerabilities`, `acknowledge_alerts`, `rescan`, `auth_status`, `auth_login`

All tools accepting `repo?` use the **directory name** as identifier (e.g. `pa-services-gateway`).

## MCP resources

`workspace://topology`, `workspace://status`, `workspace://alerts`, `workspace://notes`, `workspace://dashboard`

## Web UI routes

Full pages: `/`, `/mrs`, `/issues`, `/vulns`, `/pipelines`, `/settings`, `/repo/{name}`
Redirects: `/events` → `/` (301)

Fragments (htmx):
- `/fragment/{dashboard,mrs,issues,vulns,pipelines}` — main list fragments (mrs/issues/vulns/pipelines accept `?filter=`)
- `/fragment/sync-status`, `/fragment/events-panel` — dashboard widgets
- `/fragment/repo/{name}/{mrs,issues,pipelines,vulns}` — per-repo detail tabs
- `/fragment/pipeline-jobs/{repo_name}/{pipeline_id}` — job list for a pipeline

Actions:
- `POST /action/sync` — trigger refresh cycle
- `POST /action/fetch/{repo_name}` — git fetch one repo
- `POST /action/pull/{repo_name}`, `POST /action/pull-all` — pull one or all repos
- `POST /action/checkout-default`, `POST /action/checkout-default/{repo_name}` — switch stale branches to default
- `POST /action/dismiss-event/{id}`, `POST /action/dismiss-all-events` — mark events seen
- `POST /action/pin/{repo_name}`, `POST /action/unpin/{repo_name}`, `POST /action/reorder` — dashboard card ordering
- `POST /action/exclude/{repo_name}`, `POST /action/include/{repo_name}`, `POST /action/rescan` — repo management
- `POST /api/refresh` — trigger refresh + return JSON summary

## V1 scope boundaries

GitLab, GitHub, and Bitbucket Cloud supported. Bitbucket: PRs, issues, pipelines — no vulnerability scanning (provider declares unsupported via `supported_features`). Single workspace. PAT/token auth only. No MR actions (approve/merge/comment). No desktop notifications. V1.1 planned: DB maintenance, OAuth device flow, multi-workspace.
