Skip to content
Merged
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
63 changes: 63 additions & 0 deletions .claude/skills/project-setup/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
name: project-setup
description: Set up a new GitHub project from docs/GITHUB_PROJECT.md — author the plan (milestones, labels, issues), validate it, and push it to GitHub with make populate. Use when starting a project from this template, when the user asks to "create the roadmap", "set up the project board", "populate GitHub", or when docs/GITHUB_PROJECT.md still contains the shipped example.
---

# Set up a new project from GITHUB_PROJECT.md

`docs/GITHUB_PROJECT.md` is the single source of truth for project
STRUCTURE (milestones, labels, issue definitions, prose, dependency
graphs). GitHub owns STATE (issue numbers, open/closed, assignees).
This skill covers the one-time bootstrap: author the file, then push it
to GitHub.

## Procedure

1. **Author the plan** by replacing the worked example in
`docs/GITHUB_PROJECT.md`, keeping every structural convention:
- `**Repository**: \`owner/name\`` header line (this is where the
scripts learn the target repo; no `--repo` flag needed).
- `## Labels` section, one entry per line:
`` - `name` (RRGGBB) — Description ``. Include one
`milestone-N-<slug>` label per milestone — these are load-bearing
(update uses them to place issues into regions).
- `## Milestones` section with `### Milestone N — Title` blocks, each
carrying `**Description:**` and `**Exit criterion:**`.
- Per-milestone issue sections wrapped in markers:
`<!-- BEGIN GENERATED: milestone-N -->` ... `<!-- END GENERATED: milestone-N -->`,
containing `### Issue MN-k: Title` headings with `**Labels:**`
and `**Milestone:**` metadata lines, separated by `---`.
- Optional but valuable: a hand-authored mermaid dependency graph.
2. **Validate** (no network): `make lint` — fix every error it reports.
Warnings are advisories.
3. **Preview**: `make populate-dry` — read the plan it prints. `+`
lines will be created; `-` lines already exist; `!` lines are label
collisions you must resolve first (rename in the plan or on GitHub —
the tool never creates a near-duplicate label scheme).
4. **Push**: `make populate` (append `--` nothing; it prompts before
creating; use `python3 scripts/gh_project_populate.py docs/GITHUB_PROJECT.md --yes`
for non-interactive runs). Issue numbers are written back into the
headings as `(#N)` suffixes as each issue is created.
5. **Canonicalize**: `make update` — rewrites the generated regions from
GitHub so the file and GitHub agree exactly from day one.

## Safety properties you can rely on

- Re-running `make populate` never duplicates: existing labels (by
name), milestones (by title), and issues (by `[MN-k]` title prefix or
recorded `(#N)`) are skipped.
- An interrupted populate is rerun-safe: every created issue's number is
persisted to the file before the next API call.
- Existing labels are never overwritten, even when the plan disagrees
on color or description — differences are reported for a human.

## Pitfalls

- `gh` must be authenticated (`gh auth status`); the target repo must
already exist on GitHub.
- In environments that authenticate via `GH_TOKEN`/`GITHUB_TOKEN`
(GitHub Actions), set `NO_ENV_PREFIX=1` on the make invocation — by
default the scripts strip those variables to stop them shadowing a
keychain token.
- Never remove the `[MN-k]` prefix from issue titles on GitHub; it is
the join key for idempotency and for update.
66 changes: 66 additions & 0 deletions .claude/skills/roadmap-upkeep/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
name: roadmap-upkeep
description: Keep docs/GITHUB_PROJECT.md current with GitHub — pull live issue state into the file (make update), check for drift, add new issues or milestones to an already-populated project, and interpret the freshness workflow. Use for "update the roadmap", "is the plan current", "sync the project file", "add an issue to the plan", or after issues were closed/edited on GitHub.
---

# Keep the roadmap current

After bootstrap (see the project-setup skill), GitHub owns issue STATE;
`docs/GITHUB_PROJECT.md` mirrors it inside marked regions and owns the
surrounding prose. Two commands, two directions:

| Command | Direction | Rewrites |
| --------------- | ------------- | --------------------------------- |
| `make update` | GitHub → file | only the BEGIN/END GENERATED regions |
| `make populate` | file → GitHub | nothing in the file except `(#N)` write-backs |

## Routine refresh

- `make update-check` — is the file current? Succeeds when current,
fails otherwise; the output says whether the file drifted or the
check itself failed (auth, network — NOT drift; fix the cause before
trusting any answer). When an exit code must distinguish drift from
failure, call the script directly —
`python3 scripts/gh_project_update.py docs/GITHUB_PROJECT.md --check`
follows diff(1) (0 current / 1 drifted / 2 failed); `make` collapses
any recipe failure to exit 2.
- `make update` — rewrite the generated regions from live GitHub state.
Hand-written prose outside the markers is preserved byte-for-byte.
Commit the result.

## Adding work to a live project

1. New issue in an existing milestone: add a
`### Issue MN-k: Title` block (next free k) inside that milestone's
region, with `**Labels:**` including `milestone-N-<slug>`. Fan-out
sub-tickets of issue MN-k are `MN-ka`, `MN-kb`, ...
2. New milestone: add the `### Milestone N — Title` block, a
`milestone-N-<slug>` label entry, and a new
`<!-- BEGIN GENERATED: milestone-N -->` region for its issues.
3. `make lint`, then `make populate-dry`, then `make populate` —
existing entities are skipped, only the additions are created.
4. `make update` to canonicalize.

Issue bodies of already-created issues are edited ON GITHUB (the region
mirror would overwrite in-file edits on the next update). Prose outside
regions is edited in the file.

## The scheduled freshness workflow

`.github/workflows/project-plan-update.yml` runs Mondays (and on
manual dispatch). By default it only REPORTS drift in the run's step
summary. To let it act, set the repository variable
`PROJECT_PLAN_AUTO_UPDATE` to `pr` (opens/refreshes a pull request —
requires "Allow GitHub Actions to create and approve pull requests" in
the repo's Actions settings) or `commit` (pushes directly). Delete the
workflow file if no scheduled runs are wanted.

## Reading update's output

- `### Issue M1-2: Title (#42)` — open issue #42.
- `### Issue M1-2: Title (#42, closed)` — closed on GitHub.
- `**Assignees**: @login` — present only when assigned.
- `_(no open or closed issues with `milestone-N-*` label)_` — the
region exists but no issue carries its label yet.
- A `no rendering rule` comment — the region id is not `milestone-N`;
fix the marker id.
52 changes: 52 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: ci

# The template's own CI: structural lint of the example plan, the test
# suite (offline; a recorded fake gh), shell lint, and a minimal Python
# lint. Deliberately not Nix-based — consumers must be able to use this
# template with nothing but python3, gh, and make (the flake is a
# convenience, never a requirement).

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
checks:
runs-on: ubuntu-latest
strategy:
matrix:
# 3.11 is the documented floor; 3.12 the current default.
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Structural lint of the example plan
run: make lint

- name: Test suite
run: make test

static:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Shellcheck
run: shellcheck scripts/ci/*.sh

- name: Python lint (errors and pyflakes only)
run: |
pip install ruff
ruff check --select E9,F scripts/
45 changes: 45 additions & 0 deletions .github/workflows/project-plan-update.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: project-plan-update

# Keeps docs/GITHUB_PROJECT.md fresh. By default this only REPORTS
# drift to the run's step summary — no bot commits. Opt in to automatic
# updates by setting the repository variable PROJECT_PLAN_AUTO_UPDATE to
# `pr` (open/refresh a pull request) or `commit` (push directly); see
# scripts/ci/project-plan-update.sh, which holds all of the logic and is
# runnable and testable locally. Delete this file (or disable the
# workflow in the Actions tab) if you want no scheduled runs at all.

on:
schedule:
- cron: "0 6 * * 1" # Mondays, 06:00 UTC
workflow_dispatch:

# contents:write and pull-requests:write are exercised only when the
# PROJECT_PLAN_AUTO_UPDATE variable arms the commit/pr modes; the
# default report-only path reads issues and writes nothing.
permissions:
contents: write
issues: read
pull-requests: write

jobs:
freshness:
name: Is the plan current?
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Check, and update if armed
env:
GH_TOKEN: ${{ github.token }}
PROJECT_PLAN_AUTO_UPDATE: ${{ vars.PROJECT_PLAN_AUTO_UPDATE }}
# Required here. The update script strips GH_TOKEN and
# GITHUB_TOKEN by default, which is exactly how Actions
# authenticates; without this every gh call exits with a
# message pointing at `gh auth login`, i.e. away from the
# real cause.
NO_ENV_PREFIX: "1"
run: scripts/ci/project-plan-update.sh
49 changes: 49 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# CLAUDE.md

This repository is a **template** for running a GitHub project roadmap
from one markdown file. If you are reading this inside a project
created from the template, the file to care about is
`docs/GITHUB_PROJECT.md`.

## The contract (one source of truth per concern)

| Concern | Source of truth | Who edits it |
| --- | --- | --- |
| STRUCTURE: milestones, labels, issue definitions, prose, dependency graphs | `docs/GITHUB_PROJECT.md` | humans and Claude, in the file |
| STATE: issue numbers, open/closed, assignees | GitHub | the normal GitHub workflow |

Two commands move information between them:

- `make populate` — file → GitHub. Creates labels, milestones, issues;
writes each new issue number back into the file immediately.
Idempotent: re-running skips everything that already exists.
- `make update` — GitHub → file. Rewrites ONLY the regions between
`<!-- BEGIN GENERATED: ... -->` / `<!-- END GENERATED: ... -->`
markers; hand-written prose is preserved byte-for-byte.

Corollaries for editing:

- Prose outside the markers: edit in the file, freely.
- Issue bodies after bootstrap: edit ON GITHUB (in-file edits inside a
generated region are overwritten by the next `make update`).
- Never strip the `[MN-k]` prefix from issue titles on GitHub — it is
the join key for idempotency and for update.

## Working here

- `make help` lists every target. `make lint` (structure, no network)
and `make test` (offline suite against a recorded fake `gh`) must
pass before any PR.
- Two committed skills cover the workflows: `project-setup` (bootstrap a
new project from the plan file) and `roadmap-upkeep` (keep the file
and GitHub in sync, add issues/milestones to a live project). Prefer
them over improvising.
- Python here follows a functional house style: total functions,
`Result`-based error handling (never exceptions for control flow),
frozen dataclasses, pure parsing/planning core with `gh` calls at the
edges, a `File:` header docstring in every file, and a test for every
pure function. Match it.
- The scripts directory is self-contained on purpose (vendored `_utils`,
`scripts/VERSION`): downstream projects re-vendor it wholesale. Do
not add imports that reach outside `scripts/`, and bump
`scripts/VERSION` when its behavior changes.
53 changes: 53 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# File: Makefile
#
# The primary UX for this template. `make help` (the default) lists the
# targets; `make populate` / `make update` are the two directions of the
# contract stated in the README.
#
# Variables:
# PLAN plan file (default: docs/GITHUB_PROJECT.md)
# REPO owner/name override (default: the plan file's
# **Repository**: header)
# PYTHON interpreter (default: python3)
# NO_ENV_PREFIX set (to anything) wherever gh authenticates *through*
# GH_TOKEN/GITHUB_TOKEN — GitHub Actions included; by
# default the scripts strip those variables so they
# cannot shadow a keychain-stored token.

PYTHON ?= python3
PLAN ?= docs/GITHUB_PROJECT.md
REPO ?=

FLAGS := $(if $(REPO),--repo $(REPO)) $(if $(NO_ENV_PREFIX),--no-env-prefix)

.PHONY: help populate-dry populate update update-check lint test guard-gh
.DEFAULT_GOAL := help

help: ## Show this help
@grep -hE '^[a-zA-Z][a-zA-Z0-9_-]*:.*## ' $(MAKEFILE_LIST) \
| awk 'BEGIN {FS = ":.*## "} {printf " \033[1m%-14s\033[0m %s\n", $$1, $$2}'

populate-dry: guard-gh ## Preview what populate would create (no mutations)
$(PYTHON) scripts/gh_project_populate.py $(PLAN) $(FLAGS) --dry-run

populate: guard-gh ## Create labels, milestones, issues on GitHub from $(PLAN)
$(PYTHON) scripts/gh_project_populate.py $(PLAN) $(FLAGS)

update: guard-gh ## Rewrite $(PLAN)'s generated regions from live GitHub state
$(PYTHON) scripts/gh_project_update.py $(PLAN) $(FLAGS)

update-check: guard-gh ## Report whether $(PLAN) is stale (exit 1) without writing
$(PYTHON) scripts/gh_project_update.py $(PLAN) $(FLAGS) --check

lint: ## Validate $(PLAN)'s structure (no network)
$(PYTHON) scripts/gh_project_lint.py $(PLAN)

test: ## Run the test suite (no network; uses a recorded fake gh)
$(PYTHON) -m unittest discover -s scripts/tests

guard-gh:
@command -v gh >/dev/null || { \
echo "error: the GitHub CLI (gh) is required for this target."; \
echo " https://cli.github.com/ (or: nix develop)"; \
exit 1; \
}
Loading
Loading