Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions docs/superpowers/plans/2026-04-28-github-sprint-planner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# github-sprint-planner — Implementation Notes

**Date:** 2026-04-28
**Status:** Implemented (v1.0.0)
**PR:** https://github.com/infraspecdev/tesseract/pull/36

---

## What was built

A Claude Code plugin for sprint planning with GitHub Issues + Projects v2. Teams write sprint plan documents (HTML or markdown), then use the plugin to bulk-create GitHub Issues, link them as sub-issues to epics, and add them to a Projects v2 board — all in one command.

---

## How it works

```
1. User writes a plan doc (HTML file with story divs)
2. /sprint-sync → parses plan doc + fetches current GitHub Issues
3. Shows diff: which stories exist, which need to be created
4. User confirms → sprint_bulk_create fires
5. Issues created, linked to epic as sub-issues, added to Projects v2
6. /sprint-status → shows epic overview table
```

---

## Files delivered

| File | Purpose |
|------|---------|
| `server/main.py` | FastMCP entry point — loads config, wires up all tools |
| `server/github_client.py` | GitHub REST v3 + GraphQL v4 wrapper |
| `server/config.py` | Pydantic models for `sprint-planner.json` |
| `server/action_log.py` | Append-only JSON log of all mutations |
| `server/parsers/html_parser.py` | Parses HTML plan docs via BeautifulSoup |
| `server/tools/sync.py` | `sprint_sync` — read-only diff |
| `server/tools/bulk_create.py` | `sprint_bulk_create` — create + link + add to project |
| `server/tools/bulk_update.py` | `sprint_bulk_update` — batch update assignees/labels/state |
| `server/tools/bulk_rename.py` | `sprint_bulk_rename` — preview/apply epic prefix renames |
| `server/tools/sprint_status.py` | `sprint_status` — epic overview with stats |
| `server/tools/action_log_tool.py` | `sprint_action_log` — query past operations |
| `commands/sprint-sync.md` | `/sprint-sync` slash command |
| `commands/sprint-plan.md` | `/sprint-plan` slash command |
| `commands/sprint-status.md` | `/sprint-status` slash command |
| `skills/sprint-planning/SKILL.md` | Auto-invoked skill — guides Claude to use bulk tools |
| `.gitignore` | Excludes `sprint-planner.json` and `epics/` |
| `examples/sprint-planner.example.json` | Example config |
| `README.md` | Setup and usage docs |

---

## Key implementation decisions

- **Projects v2 uses GraphQL** — REST API has no Projects v2 write support. All project board operations (add item, set iteration) go through GraphQL v4.
- **Sub-issue linking is native** — GitHub has a native sub-issues API (`POST /issues/{parent}/sub_issues`). No custom fields needed.
- **`gh auth token` fallback** — If `GITHUB_TOKEN` env var is not set, the server calls `gh auth token` automatically so users who ran `gh auth login` don't need to set anything.
- **Preview before mutating** — `sprint_sync` and `sprint_bulk_rename` both default to read-only/preview mode. Writes only happen when `apply=True` or the user confirms.
- **Action log** — Every write operation is appended to a JSON log with enough info to roll back manually.
166 changes: 166 additions & 0 deletions docs/superpowers/specs/2026-04-28-github-sprint-planner-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Design: github-sprint-planner plugin

**Date:** 2026-04-28
**Issue:** https://github.com/infraspecdev/tesseract/issues/27
**Status:** Implemented (v1.0.0)

---

## Problem

Teams using GitHub Issues + Projects v2 have no way to bulk-create sprint stories from plan documents, link them to epics as sub-issues, and add them to a project board — all in one shot. Doing this manually means 40+ permission approvals and copy-pasting from a plan doc into GitHub one issue at a time.

---

## Decision

Build a Claude Code plugin `github-sprint-planner/` — a Python MCP server that reads sprint plan documents (HTML or markdown), diffs them against existing GitHub Issues, and bulk-creates stories with sub-issue linking and Projects v2 support.

---

## Architecture

### Plugin structure

```
github-sprint-planner/
├── .claude-plugin/plugin.json # Plugin manifest
├── .mcp.json # Tells Claude Code how to start the MCP server
├── .gitignore # Excludes sprint-planner.json and epics/
├── commands/
│ ├── sprint-sync.md # /sprint-sync command
│ ├── sprint-plan.md # /sprint-plan command
│ └── sprint-status.md # /sprint-status command
├── skills/sprint-planning/
│ ├── SKILL.md # Auto-invoked skill
│ └── card-format.md # Issue body format reference
├── examples/sprint-planner.example.json
├── server/
│ ├── main.py # FastMCP entry point, registers all tools
│ ├── github_client.py # GitHub REST v3 + GraphQL v4 wrapper
│ ├── config.py # Pydantic models for sprint-planner.json
│ ├── action_log.py # Append-only JSON log of all mutations
│ ├── parsers/
│ │ ├── base.py # Story dataclass + parser interface
│ │ ├── html_parser.py # Parses HTML plan docs via BeautifulSoup
│ │ └── markdown_parser.py # Markdown stub
│ └── tools/
│ ├── sync.py # sprint_sync tool
│ ├── bulk_create.py # sprint_bulk_create tool
│ ├── bulk_update.py # sprint_bulk_update tool
│ ├── bulk_rename.py # sprint_bulk_rename tool
│ ├── sprint_status.py # sprint_status tool
│ ├── action_log_tool.py # sprint_action_log tool
│ └── _helpers.py # Shared status normalisation
├── pyproject.toml
└── README.md
```

### Layer responsibilities

| Layer | File(s) | Responsibility |
|-------|---------|----------------|
| Entry | `main.py` | Load config, create client, register tools, run MCP server |
| Config | `config.py` | Parse `sprint-planner.json` into Pydantic models |
| API client | `github_client.py` | GitHub REST + GraphQL calls, no business logic |
| Parsers | `parsers/` | Read HTML/markdown plan docs → `list[Story]` |
| Tools | `tools/` | Sprint logic: diff, create, update, rename, status |
| Logging | `action_log.py` | Append-only JSON log of all mutations with rollback info |

---

## Config shape

```json
{
"version": "1",
"github": {
"token_env": "GITHUB_TOKEN",
"owner": "your-org",
"repo": "your-repo",
"project_number": 49
},
"team": [
{ "name": "Alice", "github_login": "alice" }
],
"plan_docs": {
"format": "html",
"base_path": "./epics",
"epics": [
{
"id": "E26",
"name": "Multi account CUR data",
"plan_doc": "multi-account-cur/plan.html",
"epic_issue_number": 26
}
]
},
"story_extraction": {
"html": {
"story_selector": "div.story[id^='story-']",
"name_pattern": "Story \\d+: (.+)",
"issue_selector": "a.badge-github",
"status_selector": ".badge:not(.badge-github):not(.badge-to-create)"
}
},
"action_log": { "path": "./epics/github_actions.json" }
}
```

---

## GitHub API mapping

| Operation | Endpoint |
|-----------|----------|
| Create issue | `POST /repos/{owner}/{repo}/issues` |
| Update issue | `PATCH /repos/{owner}/{repo}/issues/{number}` |
| Fetch sub-issues | `GET /repos/{owner}/{repo}/issues/{epic}/sub_issues` |
| Link sub-issue | `POST /repos/{owner}/{repo}/issues/{epic}/sub_issues` |
| Add to project | GraphQL: `addProjectV2ItemById` |
| Set iteration | GraphQL: `updateProjectV2ItemFieldValue` |

Projects v2 requires GraphQL — `github_client.py` handles both REST and GraphQL.

---

## Tools

### `sprint_sync`
1. Load config → get epic config (epic_issue_number)
2. Parse plan doc → `list[Story]`
3. Fetch sub-issues of the epic from GitHub
4. Match stories ↔ issues (exact by issue number, fuzzy by name)
5. Return diff: `match / to_create / to_update / to_link`

### `sprint_bulk_create`
For each story:
1. `POST /repos/{owner}/{repo}/issues` → create issue
2. `POST /repos/{owner}/{repo}/issues/{epic}/sub_issues` → link to epic
3. GraphQL: `addProjectV2ItemById` → add to project board
4. GraphQL: `updateProjectV2ItemFieldValue` → set iteration (if provided)
5. Log action with rollback info

### `sprint_bulk_update`
For each update: `PATCH /repos/{owner}/{repo}/issues/{number}` + log action

### `sprint_bulk_rename`
1. Fetch sub-issues for each epic
2. Compare current title against `naming.story_format`
3. Preview mode (default): return list of proposed renames
4. Apply mode: `PATCH` each non-compliant title + log action with rollback

### `sprint_status`
1. For each epic: fetch sub-issues
2. Normalise state + labels → status (open/in-progress/done/blocked)
3. Return table grouped by epic/status/assignee

---

## Authentication

Tries `GITHUB_TOKEN` env var first, falls back to `gh auth token` output.

```python
token = os.environ.get("GITHUB_TOKEN") or _get_gh_cli_token()
```
8 changes: 8 additions & 0 deletions shield/adapters/github/.mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"shield-pm-adapter": {
"command": "uv",
"args": ["run", "--directory", "${CLAUDE_PLUGIN_ROOT}/adapters/github", "python", "server/main.py"]
}
}
}
71 changes: 71 additions & 0 deletions shield/adapters/github/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# GitHub PM Adapter — Contributor Guide

## What this is

MCP server that exposes `pm_*` tools for GitHub Issues + Projects v2. Implements the same interface as `shield/adapters/clickup/` — both adapters are interchangeable from Shield's perspective.

## Structure

```
server/
├── main.py # FastMCP entry point, _LazyProxy/_DepsLoader pattern
├── config.py # Config loading: .shield.json → pm.json → sprint-planner.json
├── github_client.py # Async GitHub REST v3 + GraphQL v4 wrapper (httpx)
├── action_log.py # Append-only JSONL log of all write operations
├── parsers/ # HTML plan doc parsers (shared with ClickUp adapter)
└── tools/
├── capabilities.py # pm_get_capabilities — static, no config needed
├── status.py # pm_get_status — sub-issues + grouping
├── sync.py # pm_sync — diff plan doc vs GitHub Issues
├── bulk_create.py # pm_bulk_create — create + link issues
├── bulk_update.py # pm_bulk_update — batch state/label/assignee changes
├── rename.py # pm_bulk_rename — preview or apply title renames
├── relationships.py # pm_link_story_to_epic — add sub-issues
├── action_log_tool.py # pm_action_log — query the action log
└── _helpers.py # normalize_status(), shared utils
tests/
├── conftest.py # Shared fixtures (mock capabilities, config)
└── test_contract.py # Contract tests — adapter declares correct capabilities
```

## Lazy loading pattern

The server starts without config so it doesn't fail when no `.shield.json` exists. Config is loaded on first tool call that needs it (`_DepsLoader.ensure_loaded()`). `pm_get_capabilities` is the only tool that never triggers loading.

Do not move config loading into `__init__` or module-level code — this would break the MCP server startup.

## Adding a new tool

1. Create `server/tools/my_tool.py` with a `register(mcp, client, ...)` function
2. Add `@mcp.tool()` inside `register()`
3. Import and call `my_tool.register(mcp, ...)` in `server/main.py`
4. Add the tool name to the `capabilities` list in `server/tools/capabilities.py`
5. Add a contract test in `tests/test_contract.py` if the tool has a new pm_* name
6. Mirror the same tool in `shield/adapters/clickup/server/tools/` if it belongs in the shared interface

## Config loading order

`load_shield_config()` in `config.py`:
1. Walk up from `cwd` to find `.shield.json` → read `project` name
2. Read `~/.shield/projects/<project>/pm.json`
3. Token: `~/.shield/credentials.json` → `GITHUB_TOKEN` env → `gh auth token`

Falls back to `load_config()` (legacy `sprint-planner.json`) if no `.shield.json` found.

## GitHub API notes

- Sub-issues use the REST endpoint `POST /repos/{owner}/{repo}/issues/{parent}/sub_issues` — this is a newer API, requires `repo` scope
- Projects v2 uses GraphQL v4 — requires `project` scope
- `get_sub_issues()` returns `[]` (not 404) if the issue has no sub-issues

## Tests

```bash
uv run pytest tests/ -v
```

Contract tests verify the adapter declares the right capabilities and the config schema is valid. They do not hit the network. Add integration tests under `tests/integration/` (gitignored) for live API tests.

## Versioning

Version lives in `pyproject.toml` only. Do not add a `version` field to `.claude-plugin/plugin.json` — per the project's versioning convention, the marketplace version in `.claude-plugin/marketplace.json` takes precedence for relative-path plugins.
Loading
Loading