diff --git a/CHANGES.md b/CHANGES.md
index 7524770..04cce0d 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,34 @@
 # Changelog
 
+## 0.3.0 (2026-10-13)
+
+### pygittools
+
+- Rebuild the `pgt` terminal UI on pyratatui.
+- Add a worktrees tab for listing and switching worktrees.
+- Add an actions page that runs project scripts from `pyproject.toml` or `actions.toml`, including named action groups.
+- Add Fibonacci story points to the task schema.
+- Fix a crash when opening `pgt` in a repository with no commits.
+
+### pygitweb
+
+- Add sprint boards with Fibonacci story points, a board switcher, and moving tasks between boards. Column headers show a running points total.
+- Improve the task UI with add, archive, and delete actions, plus board selection.
+- Render a project's markdown blog as its own page, including the archive, search, and assets.
+- Restyle the project list, summary, boards, tasks, and activity feed to follow the Tabler layout, including repository stats and open merge requests on the summary.
+- Fix board column headers and horizontal scrolling.
+- Let users issue a limited number of Smart HTTP keys, read-only or read-write and with an expiry, and log clone and push attempts to `smart_http.log`.
+- Enable and disable installed plugins from the settings page, and check plugins against annotated protocols when they register.
+- Use one base template per page, and emit table cell contents as text.
+- Fix code-block alignment that shifted with display scaling.
+
+### Tests and docs
+
+- Run JavaScript unit tests and browser integration tests with Playwright.
+- Lay the repository out as a uv workspace, with packages under `src` and the `uv_build` backend.
+- Require Python 3.12 for the workspace, which plugin protocols need.
+- Polish the README.
+
 ## 0.2.3 (2026-08-12)
 
 ### pygittools
diff --git a/README.md b/README.md
index 6e7f7a1..bc89c2f 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# PyGitWeb: Innovative yet simple project hosting
+# PyGitWeb: Project hosting with FastAPI and PyGit2
 
 PyGitWeb and its bundled tools allow users to:
 - Self-host git repositories (+ associated settings, permissions, and metadata)
@@ -8,14 +8,14 @@ PyGitWeb and its bundled tools allow users to:
 
 PyGitTools, the core package of PyGitWeb, allows users to (without involving the web server):
 - Script Python workflows with Tasks, Merge Requests, and Hooks within a repositories
-- Setup hooks and jobs to run on your machine, including server-side hooks for branch protection
+- Setup hooks and actions; Actions are defined in configuration and can be used to 
 - Use the bundled git terminal app (pgt) to quickly perform git actions and even manage tasks from the cli.
 
 ## Installation and Usage
 
 To run with UV in bash _or_ powershell:
 ```bash
-uv run --with pygitweb uvicorn pygitweb.main:app --host 0.0.0.0 --port 8000
+uv run --with pygitweb uvicorn pygitweb.main:app
 ```
 
 By default, this will set up local authenication and generate an `admin` account with a randomly generated password.
@@ -23,17 +23,18 @@ By default, this will set up local authenication and generate an `admin` account
 If you are running locally with a firewall and NO reverse proxy or other risk of external traffic, you can run with auth disabled by setting `PYGITWEB_AUTH` falsy:
 
 ```bash
-PYGITWEB_AUTH=0 uv run --with pygitweb uvicorn pygitweb.main:app --host 0.0.0.0 --port 8000
+PYGITWEB_AUTH=0 uv run --with pygitweb uvicorn pygitweb.main:app
 ```
 
 ```powershell
-$env:PYGITWEB_AUTH=0; uv run --with pygitweb uvicorn pygitweb.main:app --host 0.0.0.0 --port 8000
+$env:PYGITWEB_AUTH=0; uv run --with pygitweb uvicorn pygitweb.main:app
 ```
 
 Furthermore, if you just want to evalute `pgt` (the terminal app for using git) without involving the web server, use the following command in a project folder:
 
 ```bash
-uv run --with pygittools pgt
+uv tool install pygittools
+pgt # run this from within a git repo dir
 ```
 
 ## Installation with Docker
@@ -46,10 +47,10 @@ Clone the repo over smart-http:
 ```bash
 git clone https://app.pygitweb.com/http/pygitweb;
 cd pygitweb; uv sync --all-packages --all-groups;
-uv run uvicorn pygitweb.main:app --reload --host 0.0.0.0 --port 8000
+uv run uvicorn pygitweb.main:app --reload
 ```
 
-Setting up your pre-commit hooks is recommended, but to manually run pre-commit checks, do:
+The pre-commit actions are defined in pyproject.toml, but effectively boil down to:
 
 ```bash
 uv run ruff check --fix;
@@ -58,9 +59,9 @@ uv run mypy .;
 uv run pytest .;
 ```
 
-There are some open mypy issues, but any pytest regressions are considered blocking.
+There are optional playwright tests triggerable with `uv run pytest . --playwright --js-tests`
 
-To run tests and generate a coverage report, do
+To generate a coverage report, do
 
 ```bash
 uv run coverage run -m pytest .;
diff --git a/pygittools/README.md b/pygittools/README.md
index a464805..2f30e25 100644
--- a/pygittools/README.md
+++ b/pygittools/README.md
@@ -4,14 +4,12 @@ Hooks, tooling, and pygit2 workflows used by PyGitWeb. Can be used headless for 
 
 ## PGT
 
-PGT is a command line curses application you can use to make commits, switch branches and repos, and do other basic git tasks. Keybinds:
- - Arrow keys or `j`/`k` to navigate
- - `Enter` to confirm actions, stage or unstage files or categories
- - `Space` to collapse menus
- - `ctrl-c` or `esc` to exit.
+`pgt` is a terminal UI for commits, branches, tasks, and worktrees. The home page has **Status**, **Log**, **Tasks**, and **Worktrees** tabs. `H` / `L` switch tabs, `J` / `K` move the selection, `Q` or `Ctrl-C` quits, and `?` opens help. On **Status**, `!` or the Actions row runs project scripts from `pyproject.toml` or `actions.toml`. Full key bindings are in the in-app help (`src/pygittools/help.md`).
 
 To run from the repo root, use `uv run pgt`.
 
+Task objects may include `points`, a Fibonacci story-point value: `0`, `1`, `2`, `3`, `5`, `8`, `13`, or `21`. Leave it unset when the task is unestimated.
+
 ## Hook framework
 
 `pygittools.hooks` provides a thin `Hook` base class plus one subclass per git hook type
diff --git a/pygittools/pyproject.toml b/pygittools/pyproject.toml
index 4fba6be..24332b1 100644
--- a/pygittools/pyproject.toml
+++ b/pygittools/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "pygittools"
-version = "0.2.2"
+version = "0.3.0"
 description = "Hooks and task storage in Git"
 readme = "README.md"
 license = "MIT"
diff --git a/pygittools/src/pygittools/help.md b/pygittools/src/pygittools/help.md
index 8eef8c8..97a9753 100644
--- a/pygittools/src/pygittools/help.md
+++ b/pygittools/src/pygittools/help.md
@@ -4,13 +4,13 @@ PyGitTools terminal UI for browsing a local git repository.
 
 ## Home page
 
-The top line shows the project name followed by **Status** and **Log** tabs. The active tab is highlighted in yellow.
+The top line shows the project name followed by **Status**, **Log**, **Tasks**, and **Worktrees** tabs. The active tab is highlighted in yellow.
 
-- **H** / **Left** or **L** / **Right** — switch between **Status** and **Log** (anywhere except while typing a commit message)
-- **J** / **Up** — move selection up (from the changes list, to the actions row, then the branch row, then the project row)
-- **K** / **Down** — move selection down
+- **H** / **Left** or **L** / **Right** — previous or next tab (anywhere except while typing a commit message)
+- **K** / **Up** — move selection up (from the changes list, to the actions row, then the branch row, then the project row)
+- **J** / **Down** — move selection down
 - **Project row** — **Enter** opens the projects table (PyGitWeb `PROJECTROOT`)
-- **Branch row** — **Enter** opens a filterable list of local branches to check out
+- **Branch row** — **Enter** opens a filterable list of local branches to check out. On **Tasks**, **Enter** syncs the current board with the default remote. On **Worktrees**, **Enter** opens the add-worktree form.
 - **Actions row** — always on **Status**. **Enter** opens the actions page when the project has action sets; otherwise the row reads **No Action Groups**
 - **Commit row** — type a message, then **Enter** to commit staged changes (runs git hooks); **Enter** with an empty message starts typing
 - **Space** — collapse or expand a section header
@@ -19,7 +19,7 @@ The top line shows the project name followed by **Status** and **Log** tabs. The
 - **Log tab** — **J** / **K** scroll the commit history (newest first)
 - **?** — open this help page
 - **!** — open the actions page (scripts from ``pyproject.toml`` or ``actions.toml``)
-- **Q** — quit
+- **Q** or **Ctrl-C** — quit
 
 Commit or hook failures appear in the status bar.
 
@@ -37,8 +37,8 @@ project's ``.venv``. An ``actions.toml`` set looks like this:
       { name = "lint", script = "scripts/lint.py" },
     ]
 
-- **J** / **Up** — move selection up
-- **K** / **Down** — move selection down
+- **K** / **Up** — move selection up
+- **J** / **Down** — move selection down
 - **H** / **Left** or **L** / **Right** — previous or next action set
 - **Tab** / **Shift-Tab** — next or previous action set
 - **Enter** — run the selected action (or **Run all**)
@@ -51,8 +51,8 @@ staged (success) or unstaged (failure) theme colors.
 ## Branch picker
 
 - **Type** — filter branches by name
-- **J** / **Up** — move selection up
-- **K** / **Down** — move selection down
+- **K** / **Up** — move selection up
+- **J** / **Down** — move selection down
 - **Enter** — check out the selected branch (`*` marks current), or create a new branch from the current branch when the filter matches nothing
 - **Esc** / **Q** — return to the home page
 
@@ -65,11 +65,37 @@ Lists git repositories under the PyGitWeb project directory. Settings are read f
 `PYGITWEB_PROJECTROOT`, `PYGITWEB_PROJECTS_LIST`, `PYGITWEB_PROJECT_MAXDEPTH`, and
 `PYGITWEB_LIST_ALL`.
 
-- **J** / **Up** — move selection up
-- **K** / **Down** — move selection down
+- **K** / **Up** — move selection up
+- **J** / **Down** — move selection down
 - **Enter** — open the selected project and return to the home page
 - **Esc** / **Q** — return without switching
 
+## Tasks tab
+
+Lists boards stored in the repository. The board row shows the current board and its task count. Each status is a collapsible section with a **(+ New Task)** row.
+
+- **J** / **K** — move the selection
+- **Enter** on a status header, or **Space** — collapse or expand that status
+- **Enter** on **(+ New Task)** — open the task editor for a new task
+- **Enter** on a task — open the task editor
+- **Branch row** + **Enter** — sync the current board with the default remote (`origin` when it exists)
+
+## Worktrees tab
+
+Lists linked worktrees. The active worktree is marked with `*`.
+
+- **J** / **K** — move the selection
+- **Enter** on **(+ Add worktree)**, or **Enter** on the branch row — open the add-worktree form
+- **Enter** on another worktree — switch the TUI to that worktree
+- **D** — remove the selected worktree. The active worktree cannot be removed.
+
+## Add worktree
+
+- **Type** — path, then an optional branch. Leave the branch empty and git creates a new branch named after the directory, based on `HEAD`. An existing local branch is checked out as-is; an unknown name is created with `-b`.
+- **J** / **K** — switch between the path and branch fields
+- **Enter** — create the worktree. A relative path is resolved from the current worktree.
+- **Esc** / **Q** — return to the home page
+
 ## Help page
 
 - **Up** / **K** — scroll up
diff --git a/pygitweb/README.md b/pygitweb/README.md
index 6f48531..df34c92 100644
--- a/pygitweb/README.md
+++ b/pygitweb/README.md
@@ -6,7 +6,7 @@ Gitweb reimplementation using **Python**, **FastAPI**, and **Pygit2**, ported fr
 
 Settings load from `~/.pygitweb/settings.json` (or `PYGITWEB_SETTINGS_CONFIG`), `PYGITWEB_*`
 environment variables, and optionally a `.env` file in the working directory via
-`pydantic-settings`. See `pygitweb/config.py` and `pygitweb/settings.schema.json` for the full schema. Common ones:
+`pydantic-settings`. See `src/pygitweb/config.py` and `settings.schema.json` for the full schema. Common ones:
 
 - `PYGITWEB_PROJECTROOT` — absolute path to directory containing git repositories (default: `$HOME`)
 - `PYGITWEB_PROJECTS_LIST` — directory to scan, or path to a project-list file (default: `PROJECTROOT`)
@@ -16,7 +16,11 @@ environment variables, and optionally a `.env` file in the working directory via
 - `PYGITWEB_AUTH` — `0` / `false` / `off` disables auth; `1` / `true` / `on` enables it. If **unset or empty**, auth defaults to **on**.
 - `PYGITWEB_AUTH_CONFIG` — path to the auth JSON file (default: `~/.pygitweb/auth.json`). Generated to a reasonable default if not present.
 - `PYGITWEB_SETTINGS_CONFIG` — path to the settings JSON file (default: `~/.pygitweb/settings.json`). Created from the environment on first run if missing.
+- `PYGITWEB_ENABLED_PLUGINS` — installed plugins to load. The settings page can change this list and reload plugins immediately (`POST /settings/pygitweb/plugins`).
+- `PYGITWEB_API_KEY_MAX_COUNT` — maximum Smart HTTP API keys each local user may hold (default: `2`).
+- `PYGITWEB_API_KEY_EXPIRY_DAYS` — lifetime of a newly issued API key, in days. `0` means the key does not expire (the default).
 - Browser login: `GET /login` (optional `?next=/path`), `POST /login` with form fields `username`, `password`, `next`; sets an HttpOnly cookie with an opaque in-process session id (also accepted by protected routes via `Authorization: Bearer`). Sessions are wiped when the process exits. `GET /logout?next=/` revokes the session and clears the cookie.
+- Account and Smart HTTP keys (auth required): `GET /user` is the account page. `POST /user/keys` with JSON `{"id": "...", "readonly": false}` issues a key and returns the plaintext secret once; only its scrypt hash is stored. `readonly: true` allows fetch but not push. `DELETE /user/keys?id=<id>` revokes one key. Clone and push attempts are appended to `~/.pygitweb/smart_http.log`.
 
 ## Routes
 
@@ -66,6 +70,14 @@ Load the `/docs` page for a detailed view of routes (below the readme).
 | search (page) | — | `GET /project/{project}/search` | Per-project search UI page. |
 | search_help | — | | TODO |
 
+**Blog, boards, and tasks**
+
+- `GET /project/{project}/blog/`: markdown blog page when `articles/manifest.json` exists at `HEAD`. Otherwise the summary link stays disabled.
+- `GET /project/{project}/blog/raw/{path}`: raw blob from `HEAD` for the blog viewer (markdown, manifest, and assets). Paths that escape the tree return `404`.
+- `POST /tasks/move?project=<name>&board=<source>&task=<ref>&target=<board>`: move a task onto another board, such as a sprint board. Column headers on the board page show a running points total. `points` on create and update accepts `0`, `1`, `2`, `3`, `5`, `8`, `13`, or `21`; any other value is stored as unestimated.
+- `POST /tasks/archive?project=<name>&board=<source>&task=<ref>`: move a task to the archive board.
+- `POST /tasks/delete?project=<name>&board=<source>&task=<ref>`: delete a task.
+
 ## Query parameter short names (CGI mapping)
 
 - `p` → project
diff --git a/pygitweb/pyproject.toml b/pygitweb/pyproject.toml
index c6c9fca..6e6ac09 100644
--- a/pygitweb/pyproject.toml
+++ b/pygitweb/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "pygitweb"
-version = "0.2.3"
+version = "0.3.0"
 description = "Gitweb reimplementation with FastAPI and Pygit2"
 readme = "README.md"
 license = "MIT"
diff --git a/pyproject.toml b/pyproject.toml
index b13ae32..e8f8ab4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "pygitweb-workspace"
-version = "0.0.0"
+version = "0.3.0"
 description = "Workspace root for postgit, pygittools, and pygitweb"
 requires-python = ">=3.12"
 dependencies = []
diff --git a/uv.lock b/uv.lock
index 50dae25..473446e 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1120,7 +1120,7 @@ wheels = [
 
 [[package]]
 name = "pygittools"
-version = "0.2.2"
+version = "0.3.0"
 source = { editable = "pygittools" }
 dependencies = [
     { name = "abcreg" },
@@ -1152,7 +1152,7 @@ requires-dist = [
 
 [[package]]
 name = "pygitweb"
-version = "0.2.3"
+version = "0.3.0"
 source = { editable = "pygitweb" }
 dependencies = [
     { name = "abcreg" },
@@ -1185,7 +1185,7 @@ requires-dist = [
 
 [[package]]
 name = "pygitweb-workspace"
-version = "0.0.0"
+version = "0.3.0"
 source = { virtual = "." }
 
 [package.dev-dependencies]
