Track work as GitHub issues in this monorepo — create epics + child/sub-issues, label them consistently by package/app, and tie them into the commit workflow. Use when planning a feature, breaking work into tasks, or asked to "track this in GitHub", "make issues", "set up tracking".
Installs into .claude/skills of the current project.
Are you the author of Github Tasks?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/friendlyinternet-github-tasks)
---
name: github-tasks
layer: method
description: Track work as GitHub issues in this monorepo — create epics + child/sub-issues, label them consistently by package/app, and tie them into the commit workflow. Use when planning a feature, breaking work into tasks, or asked to "track this in GitHub", "make issues", "set up tracking".
allowed-tools: mcp__github__issue_write, mcp__github__sub_issue_write, mcp__github__issue_read, mcp__github__list_issues, mcp__github__search_issues, mcp__github__get_label, Bash, Read, Grep
---
# GitHub Task Tracking
The canonical task tracker for this repo is **GitHub Issues** (`FriendlyInternet/nuxt-crouton`). This skill defines how to create and label them consistently so every task maps to a real part of the monorepo.
## Write it as a hypothesis (assumption-first — the default for every epic & issue)
**Frame work as an assumption, not a task list.** A task says "do X". A hypothesis says "we think *if* we do X, *then* Y will happen — and Y is what we want", so later we can look back and honestly say whether we were right. It's more human, it forces the *why* and the *how-we'll-know* up front, and it's what the daily digest surfaces. Use it for **every epic and issue as much as possible**; only trivial chores (a typo, a dep bump) may fall back to a plain task description.
The hypothesis has **4 parts**:
1. **We think that** — *if* we [do/change X], *then* [outcome Y] will happen — and Y is what we want. *(the hypothesis)*
2. **We'll do that by** — [this, this, and that]. *(the work)*
3. **We'll be right if** — [these things turn out to be true]. *(success conditions)*
4. **We'll know by** — [measuring / checking these signals]. *(measurement)*
**It's a lens over the sections below, not a new competing heading.** Don't bolt a 5th section on — map the hypothesis onto what's already required:
| Hypothesis part | Lives in |
|----------|----------|
| 1–2 · the hypothesis + the work (plain) | the **👤 For humans** lead (open the body with "We think that…") |
| 2 · the work (precise) | the **🤖 For agents** block |
| 3–4 · we'll be right if / we'll know by | the **🧪 How to test** section (retitle it `## 🧪 We'll be right if / We'll know by` when it reads more naturally as the hypothesis's check) |
A good epic body therefore *opens* with a `## Hypothesis` block, then the 👤/🤖 sections expand it, then the check closes it. See epic #359 and #357 / issues #358, #360, #361 for worked examples.
## Writing for two audiences (REQUIRED — applies to issues, PRs, and commits)
Everything that lands in GitHub is written for **two readers**, in this order:
### 👤 For humans (first — and it must be genuinely easy to read)
Lead with this. Plain language a busy person skims in seconds: *what changed, why it matters, what to expect* — impact over mechanics. Short sentences, no unexplained jargon, no raw file paths unless they're the point. Use a **diagram only when it makes the change easier to understand** (a flow, a before/after, an architecture or state change). Mermaid renders in issue/PR bodies — use it there. **Never add a diagram for decoration**; if it doesn't earn its space, leave it out.
### 🤖 For agents
A precise, structured block an AI can act on without guessing: scope, exact files/paths and symbols, behaviour changes, acceptance criteria, follow-ups, links to issues/docs.
Use explicit headings (`## 👤 For humans` / `## 🤖 For agents`) so both are obvious. Scale to the change — a one-line human summary is fine for something small — but **always include both**.
## Record what you *didn't* do (Considered & rejected)
A **"why not" is as load-bearing as a "why"** — it's the evidence the chosen path beat *something*, with reasons that may later expire. Whenever a decision had genuine alternatives, **write them down**, so future-us (and agents doing archaeology) stop re-litigating settled questions and understand the *shape* of a decision, not just its outcome.
- Add a short **`Considered & rejected`** note — one line per option: `option → ❌ why not`. It lives in the **🤖 For agents** block by default (or its own small block, or a comment on the issue/epic).
- **Required only when alternatives were actually weighed** — trivial chores opt out (same as the hypothesis framing).
- The **epic** is the natural home for a cross-cutting "why not" — record it in the epic's **Decisions log** (below), *when the decision is made*, while the reasoning is fresh.
Worked example (from epic #392):
> **Considered & rejected — E2E perf**
> - Bump Playwright workers → ❌ the harness shares one dev server + SQLite *per job*; parallel workers race on mutated state → flaky.
> - Shared "build packages once" prebuild job → ❌ serialises a parallel matrix; saves CI *minutes*, not wall-clock.
> - Cache the built packages → ❌ a stale cache makes the regression smoke pass *falsely* — defeating its purpose.
## The epic's Decisions log (capture the call *when it's made*)
`Considered & rejected` catches the alternatives you weighed **when you write the issue**. But the decisions that leak worst are the ones made **halfway through the work** — "team-scope at the query layer", "layout composition is deterministic, not LLM", "fake the payment in the POC". At authoring time they don't exist yet, so no up-front section can catch them; by close they're lost to chat.
So every epic carries a running **`## 🧭 Decisions`** log — in the epic body, or a maintained pinned comment on the epic — that you **append to the moment a real design call is made**, mid-work, while the reasoning is fresh. It's the *same reflex* as updating a POC's `HANDOFF.md` / `spec.json` at a sign-off (the `spec` skill), pointed at the epic: one concept, two homes.
**The format** — newest at the bottom, one line per decision:
```
[YYYY-MM-DD] Decision → why → (Rejected: X — why not)
```
**Append-only — reversals are kept, never edited away.** This is the one deliberate difference from the POC `spec.json`, which *supersedes* (newest wins, prune the stale entry — "this is how it behaves *now*"). A decision log does the opposite: when a decision is later reversed, append a **new** line recording the reversal and the rule learned — don't touch the original. The reversal *is* the lesson, exactly as `AGENTS.md` demands ("negative results are first-class data — record the reversal and extract the rule, don't delete it"). A ledger that erases its own reversals is the re-litigation trap wearing a fresh coat.
**Keep it a habit, not a gate.** Like the hypothesis framing, this is a strongly-modeled convention with a worked example — *not* a required section and *not* hook-enforced. Decision capture dies the instant it's a box you tick to unblock a merge (you get "N/A" and "see chat"). Trivial epics opt out.
Worked example (a running log on an epic):
> ## 🧭 Decisions
> - [2026-06-14] Team-scope at the **query layer** (drizzle `where team_id`), not per-route middleware → one choke-point you can't forget to add on a new endpoint → (Rejected: per-route guard — trivially omitted on the next route added; Rejected: DB row-level security — D1/SQLite has none).
> - [2026-06-20] POC default layout is **deterministic** (`layout-compose.ts` rules), not LLM-generated → reproducible, reviewable, no token cost on every boot → (Rejected: LLM "place the blocks" — non-deterministic and slow, can't diff two runs).
> - [2026-06-28] **Reversed** the 06-14 call *for the reporting endpoints only*: they team-scope in **middleware** after all → their cross-collection joins made per-query scoping duplicative and error-prone → rule learned: query-layer scoping for single-collection reads, middleware for joins.
## How to test (REQUIRED on every closeable issue/PR — written for a human)
Every issue that changes observable behaviour, and every PR, MUST carry a **`## 🧪 How to test`** section written for someone who knows the *app concept* but not the code. It is not "run the unit tests" — it is *where a person clicks and what they should see*. Treat it as the acceptance check: if a non-developer can't follow it to confirm the change, it's not done.
Write it as:
- **What changed** — one or two plain sentences: what's different now vs. before.
- **Where you'll see it** — the concrete surface: the URL/page, the button, the screen. Name it the way a user would ("the top-right log-out button", "the access-code screen"), not by file or component.
- **Steps** — a short numbered walk-through, each step an action + the expected result. Include the **before/after contrast** where it matters ("previously the kassa still showed; now you land on the access-code screen").
- **Test data** — any PINs, logins, or seed URLs needed to reproduce (e.g. `/test1/nl/vlaamsekermis`, helper PIN `1234`).
Keep it tight and skimmable. A `mermaid` flow is welcome when the steps branch or the state change is the point — never for decoration.
### Epic acceptance rollup (the epic is the verification unit)
Sub-issues are the **work** unit; the **epic** is the **verification** unit. When all of an epic's sub-issues have merged (the feature has "landed in the app"), post a single **`## 🧪 Verify the whole thing`** comment on the epic *before closing it* — so the owner does **one** QA pass instead of hunting across sub-issues:
- **What landed** — one plain line per merged sub-issue/PR (what's now different).
- **Where to test** — the **one** link: the deployed preview or production URL, plus any test data (PINs/logins/seed URLs) needed.
- **Walkthrough** — the per-issue "How to test" steps **stitched into one ordered pass** a human runs end-to-end, with the before/after where it matters.
Close the epic **only after that pass passes** (or the owner confirms). This turns "a bunch of merged PRs" into a single "now go click these and confirm it all works" checklist for a non-technical owner. If a sub-issue couldn't be auto-verified (e.g. needs a device), say so explicitly in the rollup rather than implying it's confirmed.
### Human-action tasks: assign them, and close them when answered (REQUIRED)
**Not every task ends in a PR.** When something needs **the owner** to act — a visual sign-off, a decision, an approval, a manual run/deploy — capture it as a **discrete issue assigned to them**, *never* as a bare comment buried on another issue. The owner's **"assigned to me" filter is their entire to-do list**; an action that lives only in a comment is invisible to it. Trackers/epics stay **unassigned** — only actionable tasks carry an assignee, so "assigned" always means "you, now".
These tasks are resolved by a **comment/confirmation, not a merge**, so they need their own closing path — and it's the agent's job, not the owner's:
- **Close it the instant the action is confirmed done** — the owner says "looks good", the deploy goes green, the question is answered — in that *same turn*, and drop its `status:*` label. Then run the parent/epic walk-up below.
- An action **answered in a comment but left open is a tracking leak**: the assignee filter fills with done-but-open noise and stops being trustworthy. Closing travels *with* the answer, exactly as closing a leaf travels with its merge.
So an issue closes by **either** path — `Closes #NN` on a merged PR, **or** a confirmed human-action task the agent closes on the spot — and **both** then walk up the tree:
### Closing a child? Always check the parent (REQUIRED — do it the moment the PR merges *or the task is answered*)
A merged PR auto-closes the issues in its `Closes #NN` lines — but **a parent epic has no `Closes` line of its own, so it never auto-closes**, and a fully-delivered epic left open is the most common stale-tracking bug.
> **⚠️ An epic-integration PR uses `Refs #NN`, never `Closes #NN` (#1690).** That "an epic has
> no `Closes` line" is an *assumption this whole flow rests on*, not something GitHub enforces —
> and epic #1652 broke it: its integration PR wrote `Closes #1652`, so merging closed the epic
> instantly, skipping the `/close-epic` gate (step 4) and landing the postmortem *after* the
> close it was meant to precede. Write `Refs #NN` on the epic→`main` PR and close via
> `/close-epic`. **Sub-issue PRs keep `Closes #NN`** — the resume merge-on-approval step
> identifies the gate PR by exactly that string (#1663), so don't "fix" those too. CI warns on
> a closing keyword aimed at an `epic`-labelled issue (`warn-closes-blocked.yml`). So **the instant you close an issue — or a PR merges that auto-closes one — walk up the tree, unprompted.** Don't wait to be asked and don't defer it to a later task: closing the leaf is not "done" until you've checked the branch above it. Closing the epic travels *with* the merge that closes its last child — same logical step, not a follow-up someone has to remember.
1. Resolve the parent: `mcp__github__issue_read` (`method: get`) → `parent_issue_url`, or read the epic's `get_sub_issues`.
2. If the issue has a parent, check whether **all** of the parent's children are now `closed` (`get_sub_issues` → every child's `state`).
3. **If they are, the epic is ready to close — but the epic is the *verification* unit, so never silently close it.** Post the `## 🧪 Verify the whole thing` rollup (above) as a comment on the epic, then **explicitly ask the owner to close it** (e.g. "All N sub-issues are merged — close the epic?") and close it as `completed` **only on their confirmation** (or after the end-to-end pass passes). If a sub-issue couldn't be auto-verified (needs a device, a manual check), say so in the rollup rather than implying it's confirmed.
4. **Postmortem before closing (verify = *does it work?*; postmortem = *how did it go?*).** After the verify rollup and **before** the epic is closed, run the **`postmortem`** skill on the epic — it posts a retro (what went well / what was hard, evidence-backed / 1–3 improvement proposals) and offers to mint accepted, not-already-tracked proposals as `workflow` issues, and ends with a **`🔭 Next` handoff** — the next epic to start + a paste-ready next-session prompt — so closing one epic opens the next (#615). This is how the loop tightens over time (epic #403). Skip only for a trivial epic. Once the postmortem has run the epic carries `status:ready-to-close`, so the owner can close it in one gesture by commenting **`/close-epic`** on the epic (`close-epic-on-comment.yml`, gated on that label, #856) — the label is the precondition, so an epic whose postmortem hasn't run can't be closed this way.
5. Recurse: a parent can itself be a child of a grander epic — keep walking up until you hit one with open siblings or no parent.
Don't stop at the issue you were asked about; closing the leaf without checking the branch above it leaves the epic falsely "in progress".
**Recurring/standing chores are standalone, never epic sub-issues.** A ticket that's re-armed on a cadence and intentionally never permanently closed (e.g. the quarterly dependency sweep) must **not** be a sub-issue of a deliverable epic. A future-dated child that legitimately never closes silently defeats the "are all children closed?" check above, so a fully-delivered epic is pinned open forever and never reaches its verify + postmortem close-out — the most common stale-tracking bug, just slower to spot (the #233 → #244 case: the epic sat done-but-open for days behind the next quarterly sweep). File the recurring chore **standalone** (it may *link* the originating epic for context, not parent to it), and when it re-arms, open the next occurrence standalone too. (#422)
**Titles are human-first too.** Issue/PR titles read like plain English that anyone grasps at a glance ("Run the whole app on a Raspberry Pi and print directly"), not jargon ("node-server preset + in-process TCP drainer"). Keep the technical specifics in the 🤖 body, never the title.
## Core rules
1. **Every issue maps to a package or an app — never "root".** If it feels like root-level work (CI, deploy, ops), label it with the **app it serves** (e.g. CI that builds fanfare → `app:fanfare`). Harness / method work that serves the *whole* monorepo rather than one app — `.claude/**`, `AGENTS.md`, skill/agent/hook changes — maps to **`meta:agents`** (see rule 3). There is deliberately no `root` label.
2. **Exactly one `type:*` label** per issue — except an **epic**, which carries `epic` instead of a type.
3. **Component label = where the source actually changes** (mirrors the commit-scope convention):
- `pkg:<name>` for package source (`packages/*`, e.g. `pkg:crouton-sales`)
- `app:<name>` for app/deployment/ops work (`apps/*`, e.g. `app:fanfare`)
- `worker:<name>` for `workers/*`
- **`meta:agents`** for **harness / method** work with no single app or package owner — `.claude/**` (skills, agents, hooks), `AGENTS.md`, and cross-cutting method changes. This *is* the component for that work: it serves the whole monorepo, so it satisfies rule 1's "never root" rather than being an exception to it.
- Package work that also lands a schema/config change in an app gets **both** (e.g. `pkg:crouton-sales` + `app:fanfare`).
4. Use meta labels where they apply: `epic`, `spike`, `needs-triage`.
5. **Link issues & PRs when talking to the user.** Any time you mention an issue/PR in a chat reply, render it as a clickable link to the full URL (`[#303](https://github.com/FriendlyInternet/nuxt-crouton/issues/303)`, `[#376](https://github.com/FriendlyInternet/nuxt-crouton/pull/376)`) so the owner can open it in one click. This is a **chat-reply** convention only — commit messages keep bare `(#NN)`, and PR bodies use `Closes #NN` (not a URL) so GitHub auto-closes the issue on merge.
## Structure
- **Epic** — one tracking issue per initiative. Body: goals, a checklist of workstreams, links to design docs. Labels: `epic` + the primary `pkg:`/`app:` it spans.
- **Child issues** — one per workstream, each linked as a **sub-issue** of the epic so GitHub shows a progress bar.
- Keep issue bodies tight: scope, acceptance criteria, links to `docs/`, and the required **`## 🧪 How to test`** section (above).
## How to create them (tools)
Issues, sub-issues, and labels are managed through the GitHub MCP tools:
- Create / update: `mcp__github__issue_write` (`method: create|update`, pass `labels: [...]`).
- Link a child under a parent: `mcp__github__sub_issue_write` (`method: add`, `issue_number` = parent, `sub_issue_id` = the child's **id** from its create response — not its number).
- Read / list: `mcp__github__issue_read`, `mcp__github__list_issues`, `mcp__github__search_issues`.
**Scan without overflowing the context.** A broad `list_issues` / `search_issues` (e.g. `labels: ["epic"]`, or a keyword search with no `in:title`) can return a 90k–140k-char blob that overflows the tool result and has to be sliced out-of-band — pure wasted turns. For any repo-wide scan (the dedup and epic walk-up steps are where this bites): pass **`minimal_output: true`**, keep **`perPage` small (5–10)**, and prefer **`in:title`** filters over broad body matches. Only widen when a narrow query genuinely misses.
**Labels must already exist** before you can apply them — applying an unknown label errors. New labels are added via labels-as-code (below), not by the API.
**Projects v2 boards can't be created or managed via these tools** (UI-only). Tell the user to create the board in the web/iOS app; if they enable the project's "Auto-add" workflow, issues you create land on the board automatically.
## Labels-as-code
The label taxonomy lives in **`.github/labels.yml`** and mirrors the workspace: `pkg:*` for every `packages/*`, `app:*` for every `apps/*`, `worker:*` for `workers/*`, plus `type:*` and meta labels. It's synced **non-destructively** by `.github/workflows/labels.yml` (`skip_delete`) on changes to `main` or via `workflow_dispatch`.
To add or change a label: edit `.github/labels.yml`, commit, and let the workflow sync it on merge to `main`. When a new package or app is added, add its `pkg:`/`app:` label here too.
## Fit into the task workflow
GitHub issues slot into the repo's task-execution flow (see `CLAUDE.md`):
1. **Check for existing work FIRST, then pick / open an issue** — the issue is the unit of work. Because sessions are ephemeral and a teammate (or a past you) may already have opened the epic/tasks, **always search before creating**. This is no longer a soft step: run the **`issue-dedup`** skill (it searches open **and** recently-closed work by `epic` label + keywords, surfaces matches, and forces a **reuse / replace / new** decision), and the **`require-issue-dedup` PreToolUse hook blocks any `issue_write` create whose body lacks a `Dedup-checked:` attestation line**. If a matching epic or task already exists, continue *that* one (assign yourself, set `status:in-progress`) instead of opening a duplicate. Only when nothing matches do you open a new epic + sub-issues for a multi-step initiative. (Then, when you *pick up* an issue, the sibling **`issue-sanity-check`** skill is your pessimistic go/no-go — see CLAUDE.md step 1.)
2. **Mark in progress — do this the moment you START, not after.** Apply the `status:in-progress` label (swap to `status:blocked` when waiting; remove the status label on close). **The label is the signal that moves the board:** `.github/workflows/project-status.yml` listens for the `status:in-progress`/`status:blocked` label and writes the Project's *Status field* (→ **In progress** / **Blocked**) via a PAT, since these MCP tools can't write Projects v2 fields directly (`list_issue_fields` is empty). PR opened → **In review** and merged/closed → **Done** are handled by the same workflow + the Project's built-in workflows. **Prerequisite:** the `PROJECTS_TOKEN` repo secret must be set — without it the workflow is a green no-op and nothing moves, so the label is the only signal and the board won't reflect it.
3. **Branch + do the work** — work on a feature branch; follow `CLAUDE.md` patterns; run `pnpm typecheck`.
4. **Commit** — use the `/commit` skill, referencing the issue in the body (e.g. `(#NN)`).
5. **Open a PR** — early is fine. Put `Closes #NN` in the body so the issue auto-closes on merge. Let CI run and fix failures (the PR can be watched/autofixed).
6. **Merge preserving commits** (merge/rebase — don't squash by default; squash only a noisy `wip`/`oops` history, per `AGENTS.md` § Commits → Merge policy) → the issue closes automatically and the branch is deleted. Don't push feature work straight to `main`.
7. **Walk up the epic tree (REQUIRED — part of the merge, not an afterthought).** The moment the merge auto-closes the leaf issue, run the parent check in *"Closing a child? Always check the parent"* above. If that merge closed the epic's **last** open child, post the `## 🧪 Verify the whole thing` rollup on the epic, run the **`postmortem`** skill (retro + improvement proposals — see step 4 there), and **ask the owner to close it** (close on confirmation). A merge is not "done" until the parent epic is either closed or explicitly handed off for the verify + postmortem pass. When watching/auto-merging a PR, do this walk-up as soon as the merge lands.
Work lands via **PRs**, not direct pushes to `main`. Issues are the source of truth for *what* to do; `docs/PROGRESS_TRACKER.md` (if used) becomes an optional phase-level rollup, not the per-task tracker.
## Quick reference
| Want | Label(s) |
|------|----------|
| New feature in a package | `type:feat` `pkg:<name>` |
| Feature touching a package + app schema/config | `type:feat` `pkg:<name>` `app:<name>` |
| App/deployment/ops/CI work | `type:chore`/`type:docs` `app:<name>` |
| Cross-cutting initiative | `epic` `pkg:<name>` (+ `app:<name>`) |
| Harness / skill / agent / method change (`.claude/**`, `AGENTS.md`) | `type:docs`/`type:chore` `meta:agents` (+ `epic` if an epic) |
| Time-boxed proof-of-concept | `spike` `type:feat` `<component>` |