The issue tracker is where brain-kit's planned work lives. Not a markdown file, not a plan document, not a session transcript. This page is the working agreement: what each label means, what a milestone commits to, what the project board is for, and what an agent is expected to do before it writes a line of code.
It is short on purpose. If a rule here needs a paragraph to justify it, the
justification belongs in the thing it governs — scripts/labels.ts for the
taxonomy, .github/ISSUE_TEMPLATE/ for the shape of an issue.
| It is… | It lives in… |
|---|---|
| Planned work, of any size | A GitHub issue |
| A theme that needs several issues | An epic issue, with sub-issues |
| A decision that binds future work | docs/decisions/, linked from the issue |
| A question with no work attached yet | A GitHub Discussion |
| A security vulnerability | A private advisory — never an issue |
| Deployment, container or hosting specifics of the maintainer's instance | The private schlessera/brain-ui repo, off the board |
| Progress on work in flight | Comments on the issue, and the PR |
Nothing durable lives in a plan document any more. docs/plans/ and the old
.plan/ tree were both status files and design records at once, which meant
every reader had to work out which lines were still true. Decisions moved to
docs/decisions/; open work moved to issues; progress logs were not worth
keeping once the work shipped.
Three repositories make up the open-source project and two are one person's private instance of it. AGENTS.md ("The five repositories") is the canonical statement. This section is the tracker's view of it.
| Repository | Owns | Visibility |
|---|---|---|
| brain-kit | Every line of behaviour. The published packages. | public |
| brain-template | The starting point for your brain: config, skills, empty content. Generated from template/ here. |
public |
| brain-hosting-template | The starting point for self-hosting: a PWA that manages your brain remotely, with container, compose, proxy and branding over the published packages. | public |
| brain | The maintainer's own brain. Personal data. | private, permanently |
| brain-ui | The maintainer's own hosted PWA. An instance, not a product. | private, permanently |
The first three are the product and the only repositories on the project
board. brain and brain-ui are instances of the two templates: they consume
the public project, and the public project never depends on, links to or
describes them. brain-ui is where the hosting template will be extracted
from, and it is where a production incident gets written down. Nothing in
a public repository should link to either one or depend on it existing.
Ask what the issue is actually about:
- Behaviour — what the CLI, the server, the UI or a package does. → brain-kit, always. Even when the symptom was seen in a deployment.
- What a generated brain contains — a config key, a skill, the onboarding interview, what the first clone looks like. → brain-template.
- What a generated host contains — the Dockerfile, the compose file, the proxy, the environment contract. → brain-hosting-template.
- One deployment's reality — its host, an incident on it, a migration of its volumes. → brain-ui. Its content and data → brain. Neither goes on the project board.
When a change needs brain-kit and one of the templates, each gets its own issue
and both carry the upstream: label pointing at the other. The two templates
carry no labels for each other: work that spans both goes through a brain-kit
issue that links each. When one side is the
maintainer's private instance, only the private issue links: a public issue
never names, labels or links a private one. A public issue never restates a
private deployment detail to save a click. The private repositories keep
their own label taxonomy in their own trees.
Before creating or editing an issue in a public repository, run its body
through the same gate the tree is held to — the github skill
(.agents/skills/github/) has the command.
Four namespaces and a flat meta set. The full list with descriptions is
scripts/labels.ts; bun scripts/sync-labels.ts --repo <owner/name> pushes it
(dry run by default).
type:— what kind of work.featfixrefactortestdocschorespike. Exactly one per issue.area:— where it lands, mirroring the package layout. One or more.priority:—p0drop everything,p1this milestone or the next,p2wanted but unscheduled,p3someday. No priority label means unsorted, which is an honest state.needs:— what blocks it when the blocker is not another issue:decision,design,repro,human,use-case.- Meta —
epic,agent-ready,blocked,contract,breaking,security,upstream: …, plus GitHub's triage set.
Workflow state has no labels. Backlog, ready, in progress and done live in the project board's Status field. A label and a board column tracking the same fact drift inside a week, and then neither can be trusted.
Three labels carry weight beyond their description:
contractmeans the change touches the machine surface in integration-contract.md. That implies aCONTRACT:commit prefix and the doc updated in the same commit. Addbreakingwhen a field is removed, renamed or retyped: that also needs a maintainer ruling on the issue before code is written. Applying either label is a claim, so check it.agent-readymeans a coding agent can take the issue unattended: context with real file paths, an explicit out-of-scope section, and acceptance criteria somebody else could check. See below.needs: humanmeans the next required action can only be performed by a human: approving an account login, supplying a credential or granting access, interactive publishing, or a verification explicitly reserved for the maintainer. State the exact action and what evidence completes it in the issue body. Removeagent-readywhen applying it; the two are mutually exclusive. A ruling usesneeds: decision, and a missing reproduction usesneeds: repro. Neither a browser check nor a keyed measurement alone impliesneeds: human: an authorized agent with the required access can run those.
Finish independent agent work before handing an issue over for a human action.
If that action is a prerequisite, apply needs: human immediately. When it is
completed, record the result, remove the label, and either restore agent-ready
if implementation remains and nothing else blocks it, or close the issue if
all acceptance criteria and verification are met. A planned human check at the
end does not block coding that can proceed now.
A milestone is a release, because a release is the only thing this repo
actually ships. All @schlessera/brain-* packages version in lockstep, so one
milestone covers all of them.
- An issue in the current milestone is a commitment for that release.
- An open backlog issue with no milestone is not scheduled.
1.0.0is the stability bar, not a date: the extension interfaces lose@experimentaland the integration contract stops moving under a minor.
Milestones are not themes. A theme is an epic; an epic can span three milestones, and its sub-issues carry the milestones individually.
Every merge needs a release milestone. Before merging, assign the appropriate
milestone to the PR and each issue it completes or advances, including docs, tests, CI and
partial deliveries using Refs. Confirm the upcoming release from the live
milestones and publication history; an old open milestone or 1.0.0 is not
automatically next. Read back the PR and issue assignments before merge and
verify them again when reconciling the squash and issue closure.
For already shipped work, use the first release containing its implementation, proved by merged-commit ancestry against published tags and changelogs. A late verification or closure date does not move unchanged shipped behavior into a later release. Unreleased work belongs to the confirmed upcoming milestone. Record attribution evidence when correcting history. A duplicate, rejected proposal or written-no decision records its resolution's release context; its milestone does not claim that the rejected feature shipped. Assign that context before a manual closure too. A new verification-only PR can belong to the upcoming release while the verified issue retains its original shipped milestone; record the distinction.
An epic records its initial delivered outcome. Later extensions, fixes and verification keep their own child milestones. Do not schedule an unfinished epic merely because one child merges, or replace its original delivery with the release of its newest follow-up. Unresolved backlog work stays unscheduled until there is an actual release commitment.
One project, brain-kit roadmap, spanning the three public repositories: brain-kit, brain-template and brain-hosting-template. The private instance repositories are never on it: they have their own private board, whose issues may name a public blocker. A public issue never names a private one. This board adds the two things labels cannot express: where an item is in flight, and when it is meant to happen.
| Field | What it is for |
|---|---|
| Status | Backlog → Ready → In progress → In review → Done |
| Track | The roadmap theme, matching the epics |
| Priority | Mirrors the priority: label, so the board can sort |
| Size | XS/S/M/L — a rough estimate of one sitting versus several |
| Start / Target | Two dates, for the roadmap view only. Intent, never a commitment — an item with neither does not appear there, which is the right default |
Track assignments live in scripts/sync-project.ts: each epic names its
roadmap theme, and its sub-issues inherit that theme using their own repository
identity, including across the public repositories. Standalone issues can have
a track without an epic. Keep the mapping current when adding an epic; editing
only the board is not enough to change a track already managed by sync.
An issue belongs to an epic when its deliverable advances that epic's outcome or directly verifies it. A shared package, a prerequisite, or discovery during the epic's work alone is not enough. Check scope and exclusions before linking; an issue has only one parent, so use references for other relationships. Leave work standalone when no existing epic owns its full scope.
Ready is the one that matters: it means the issue has been read, it is not
blocked, and it can be picked up now. An agent picking work should filter
Status: Ready and take the top item by priority.
Status is derived, not typed. bun scripts/sync-project.ts --apply
computes Backlog, Ready and In review from the labels and from whether an
open PR says it closes the issue. In progress and Done are statements about
a person or an agent rather than about labels, so the script reads them and
leaves them alone. .github/workflows/project-sync.yml runs it with --apply:
- When a brain-kit issue is opened, edited, labelled, unlabelled, closed or
reopened, it syncs that issue (
--issue). When the issue is closed, it also syncs the open issues that name it in aBlocked byline, so they unblock straight away. - When a same-repository PR is opened, edited, closed or reopened, it syncs the
issues the PR says it closes (
--pr). - Once a day, and on
gh workflow run project-sync, it runs the full sweep. That covers the two template repositories, whose events do not reach this workflow, and anything an event run missed.
The full sweep reads item IDs and Status/Priority/Track values together through all GraphQL cursor pages. Its final on-board verification traverses the pages again, so an item after the first 500 is recognized as present. A failed API read or a cursor that cannot advance stops the run rather than treating a partial listing as the board.
Every needs: label, including needs: human, prevents a derived Ready
status, even if agent-ready was left on by mistake. A human handoff does not
add a board column; move an item out of a manually set In progress state when
work pauses, because sync preserves that state.
An event run costs a few GraphQL points. The full sweep costs about 520 of the
account's 5,000 per hour, so it is not run per event. A label change reaches
the board through event runs without anyone running anything. Issue events,
PR events and sweeps share one concurrency group, so
only one apply runs at a time across the board. queue: max retains up to 100
pending runs; excess runs are cancelled and the daily or on-demand sweep
recovers their changes. Each run reads current issue state when it executes.
Do not run --apply from a terminal while the workflow exists: that bypasses
its serialization and can post a duplicate notice. The dry run is safe from
anywhere.
The blocked label is checked against the blockers it names. An issue that
waits on another says so on its own line in its body, starting with the
references: Blocked by #42, Blocked by #42 and #43, or
Blocked by schlessera/brain-template#42 across the board's repositories. Write it as a plain
line at the start of the line, optionally as a top-level bullet. Do not indent
it, quote it, nest it, make it a task item or put it inside an HTML comment.
Commentary may follow the list as long as it names no other reference. When
every issue it names is closed (a pull request counts once it has merged), the
script reports it, and with --apply posts a one-line comment naming the
closed blocker and removes the label. In the same run it then re-derives
Status, unless the Status is In progress or Done, and the issue lands in
Ready if nothing else holds it back. An issue labelled blocked keeps its label and is
reported as unverifiable in five cases. It has no such line. Some other line
in the body says "blocked by" in any form: prose, a quote, a heading, an
example in a code block, an HTML comment. A declaration names another
reference after its list. A blocker's state cannot be read, or it is a pull
request that closed without merging, which never counts as closed. Or it lives
outside the board's repositories, which the script never looks up: a public
run must not report on a private issue. The script clears a label
only on declarations it read in full. Closing a blocker unblocks nothing by itself — the line has to be there,
and the script has to run.
That is what the blocked label is for, and why it is worth applying: four of
the container issues and two of the template ones are agent-ready and not
pickable, because each waits on the one before it. Without blocked they would
sit in the Ready view as traps.
- File. Use a template. An issue with no acceptance criteria is not ready, whatever else it has.
- Triage.
type:,area:, a priority if it is sorted, and a milestone if it is committed. Add it to the board. - Ready. Context is complete and nothing blocks it. Add
agent-readyif a stranger could implement it from the body alone. The narrow blocked upper-layer selection exception below retains real dependency labels and board Status. Task templates do not add it automatically; triage establishes readiness. - In progress. Assign yourself, move the card. One issue at a time per
branch:
<type>/<issue-number>-<slug>. A dependent slice may use a separate branch under the stack rules. - In review. Open the PR with
Closes #<n>in the body when merging meets every acceptance criterion and verification step. If a required human step will remain, useRefs #<n>instead and name the step under "Anything left open". The PR template's checklist is the merge bar. Focused local preflight and behavioral receipts accompany complete selected hosted proof under #1326; drafts remain cheap, and failed/cancelled/missing selected jobs cannot merge. Assign and read back the PR and delivered issues' release milestones before merging; follow Milestones for release attribution. - Human handoff, when needed. Once independent agent work is complete,
update the body with the remaining action and its completion evidence,
replace
agent-readywithneeds: human, and leave the issue open. Keep deployment details and credentials out of public evidence. - Done. The PR merging normally closes the issue. After a human handoff, close it only when the required result is recorded. An epic closes when its last sub-issue does and its definition of done is met — those are not the same thing.
An issue that turns out to be wrong gets closed with a comment saying why.
wontfix is a legitimate outcome and does not need an apology.
Between those steps the issue has to stay true: its body is the current
specification and its comments are the history. Corrections are folded into the
body, a ruling is recorded where it unblocks the work, blocked comes off when
its last blocker closes (the project-sync workflow does it for a readable
Blocked by line), and a duplicate is closed into one survivor. The github
skill's "Keeping an issue true" has the procedure.
Use a stack when independently reviewable slices depend on each other. A single concern stays in one PR; unrelated work uses separate branches or stacks. Load the gh-stack skill for commands and non-interactive operation. These project rules apply alongside it:
- Plan layers before implementation. Each branch owns one reviewable slice, including its tests and required docs/changeset; every merged prefix must be usable and satisfy the existing merge bar. Keep the issue branch naming convention, adding a slice suffix when several layers advance one issue.
- A scoped upper-layer issue may be selected while
blockedonly when every remaining open blocker is an open issue represented by an open, non-draft lower-layer PR in the epic's declared stack, with complete passing selected proof for its current parent/head candidate. Verify actual membership, head/base and the declared immediate parent before claiming and again before checkout creation; start from that verified parent head. Any genuineneeds:label, assignment, non-stack blocker, missing/ambiguous/draft parent or unavailable/failed/cancelled/unexpectedly skipped selected proof rejects the exception. All-closed dependencies use ordinary reconciliation and a merged parent permits starting atmain. KeepBlocked byandblockedwhile the lower layer remains open; board Status andproject-syncare unchanged. The work-issue selection review gives passing/rejecting examples and the evidence to record. - The bottom PR targets
main; each upper PR targets its immediate parent. Runbun run check:pr --base origin/mainat the bottom andbun run check:pr --base <parent-ref>above it, after fetching or verifying that parent. CI and contract gates already use the PR's declared base. Recheck the diff and affected proof after rebasing or retargeting. - Each PR uses the repository template and carries its own title, labels,
release milestone and proof. Replace generated titles/bodies after
gh stack submit --auto; inspect each layer's contract diff and changeset requirement against its parent. Do not copy a lower layer's proof as an upper layer's receipt. - A partial slice uses
Refs #Nand states what remains. Only the layer that completes all criteria and required verification usesCloses #N. GitHub interprets closing keywords only for PRs targeting the default branch. The actor merging the stack verifies and reconciles each actually included layer's issue closure against delivered commits/tree, acceptance and required verification evidence. If automatic closure did not happen, close manually only after those criteria are satisfied and record delivery evidence. Partial deliveries and remaining human/verification work stay open with exact next actions. Every merge includes milestone readback for all included PRs and the issues each completes or advances. - Parent or ancestor review changes invalidate affected candidate proof and readiness. Communicate changed identities; each owner refreshes their own layer in dependency order, verifies the current immediate parent and its qualification, rebases, inspects the diff/conflicts, and runs parent-relative preflight plus required hosted confirmation. Retain older receipts with their actual identities. An absent owner requires a documented dependency or an explicit ownership handoff, never silent cross-owner edits/pushes. Respect clean dedicated worktrees and never push to a merged branch. Scope-ready labels alone do not establish current candidate qualification. A #2111 reservation never waives per-layer checks or covers several candidates.
- Merge stacked PRs with
gh stack merge <target> --yes --squash. A target includes its unmerged ancestors; a bare number resolves as a stack number before a PR number. Confirm the resolved membership and inspect all included PRs' current heads, bases, reviews, milestones and selected checks immediately before merging. The installed command has no--match-head-commitflag; do not describe that readback as an atomic head lock or bypass branch rules. If native stacks are unavailable, report the CLI error and use ordinary PRs only after deliberately reconciling their ancestry and bases. - Upper-layer CI proves a merge with its parent branch, not with live
main. Record that actual parent/head/checkout tree. After merging, verify every included PR's merge result onmain, the integrated commits/trees, issue states and milestones, and automatic main-push proof. A queued stack is not yet merged. Use the stack-specific guidance in CI and merge.
This repo is operated by coding agents, so this is a contract and not advice.
- Read the issue, its epic, and every link in both. The epic holds the shared context the task deliberately does not repeat.
- Check the binding decisions. ROADMAP.md "What binds
future work" and
docs/decisions/. A task that violates one of those is a task that was filed wrong — say so on the issue rather than implementing it. - Confirm the context is still true. Issue bodies cite
file:line. Code moves. Verify before building on a citation, and correct the issue if it has drifted. - Work the acceptance criteria, not the title. Each one is a thing to prove. A fix's test is shown failing on the tree before the change.
- Stay inside the scope. Everything the issue lists as out of scope stays out. Something genuinely necessary that the issue did not anticipate gets a comment, and its own issue if it is more than a line.
- Comment what changed about the plan, not what you did — the diff says what you did. Record the decisions you made that the issue did not make for you.
A contributor without write access opens their pull request from a fork, and it stays the PR of record: nobody re-opens it from a branch here to get CI. GitHub Actions runs the same CI and contract workflows for forks, same-repository pull requests and main pushes (CONTRIBUTING.md, "Contract checks"). Forks retain read-only permissions, no secrets and nonpersisted checkout credentials. GitHub may hold a first-time contributor's run until a maintainer approves it, and a held run is not a passing check. The rest of the review and merge process is unchanged.
Work found mid-session that is real but out of scope gets filed, not fixed and
not forgotten. Keep it honest: what was observed, where, and why it matters.
needs: repro is better than a confident guess, and an issue with no acceptance
criteria is a note to self — say so rather than labelling it agent-ready.
The repo-local github skill (.agents/skills/github/) carries the commands
and the exact body shapes.
CONTRIBUTING.md— prerequisites, how to run things, what gets merged.AGENTS.md— the hard rules that bind every change.docs/decisions/— decisions that bind future work.docs/integration-contract.md— the machine surface thecontractlabel refers to.