3671541d6b
Co-authored-by: claude <claude@jpaul.io> Co-committed-by: claude <claude@jpaul.io>
135 lines
14 KiB
Markdown
135 lines
14 KiB
Markdown
<!--
|
|
Suggested title: Who Picks This Up? Writing Issues for a Team of Humans and Agents
|
|
Alt title: The Issue Is the Interface: Routing Work to People and Agents
|
|
Slug: the-workflow-issues-task-layer
|
|
Meta description: An issue is how you hand a piece of work to someone else — and "someone
|
|
else" is now a mix of humans and agents. Here's how to write issues
|
|
good enough that either one can pick them up cold.
|
|
Tags: AI, developer workflow, issues, GitHub, agents, project management
|
|
-->
|
|
|
|
# Who Picks This Up? Writing Issues for a Team of Humans and Agents
|
|
|
|
A few posts back I made a big deal about the repo being durable memory the AI can read — that a fresh chat session can reconstruct "where were we?" from `git log`, `git status`, and `git diff` instead of you re-explaining your project for the hundredth time. That's true, and it's load-bearing for everything else. But there's a gap in it that I glossed over, and it's worth stopping on.
|
|
|
|
Git only ever tells you what *happened*. Settled history, and whatever's in flight right now. It is completely silent on the work that *hasn't started yet* — the bug somebody reported, the feature you promised a coworker, the cleanup you keep deferring to "next week." None of that is in the code, because by definition it isn't code yet. So where does it live?
|
|
|
|
For most people, the honest answer is: in their head, a Slack thread, and a chat tab they'll lose. Which is exactly the evaporating-memory problem we just spent all that effort fixing, sneaking back in through a side door.
|
|
|
|
This post is about the durable home for that forward-looking work. It's the next module in [The Workflow](https://git.jpaul.io/justin/ai-workflow-course), and the tool is one you already half-know under a different name: the issue tracker.
|
|
|
|
## An issue is just a written unit of work that lives next to the code
|
|
|
|
Strip the project-management vocabulary away and an issue is one thing: **a written, addressable unit of work that lives next to the code instead of in someone's head.** It has a title, a body, some metadata — labels, an assignee, a status — and a stable number you can link to, search, and close.
|
|
|
|
You already know this shape. It's a ticket. Jira, Linear, ServiceNow, your help-desk queue — same idea. What matters for our purposes is that **every git forge has issues built in**, sitting in the same place as your repo. GitHub Issues, GitLab, Gitea, Forgejo, Bitbucket, Azure Boards — the feature set varies, the concept doesn't. And because they're attached to the repo, an issue can reference a commit, a file, or a line, and the code that resolves it can point back at the issue. The *description* of the work and the *code* that does it end up living one click apart.
|
|
|
|
So now your project has two memories, and they split the timeline cleanly:
|
|
|
|
| Layer | Answers | Lives in |
|
|
|-------|---------|----------|
|
|
| The repo | "What happened / what's in flight right now?" | commits, working tree |
|
|
| The issue tracker | "What still needs to happen, and who has it?" | issues, labels, assignees |
|
|
|
|
A teammate who joins tomorrow reads the repo to learn the *code* and reads the open issues to learn the *work*. Both are ground truth. Neither depends on anyone remembering anything. Hold onto that framing — it's about to matter more than it used to, because "a teammate who joins tomorrow" might not be a person.
|
|
|
|
## Write it for a stranger
|
|
|
|
Here's the thing almost everyone gets wrong: most issues are written badly because they're written *for the author* — who already has all the context and doesn't need any of it spelled out. A good issue is written for **a stranger**, because increasingly the thing that picks it up *is* one. A teammate you've never met. Future-you who's forgotten. Or an agent with no memory at all.
|
|
|
|
Four parts carry the weight:
|
|
|
|
1. **Title** — specific and scannable. Someone skimming forty titles should know what each one is. `done command crashes on a bad index` beats `bug in cli`.
|
|
2. **Context / problem** — what's wrong or missing, and *why it matters*. For a bug, the exact command and what happened. This is the part a lazy issue skips, and then nobody can act on it.
|
|
3. **Acceptance criteria** — the checklist that defines *done*. Concrete, verifiable: "`done 99` prints an error and exits non-zero instead of a traceback." This is the single most valuable part, for reasons I'll sharpen in a second.
|
|
4. **Scope / out of scope** — what this issue does *not* cover, so a one-line fix doesn't quietly become a refactor.
|
|
|
|
Let me show you the difference, because it's stark. Here's the bad version:
|
|
|
|
> **Title:** fix the done thing
|
|
> the done command is broken, please fix
|
|
|
|
Nobody — human or agent — can do anything with that without coming back to ask you three questions. Here's the same bug, written for a stranger:
|
|
|
|
> **Title:** `done` command crashes on an out-of-range or non-integer index
|
|
>
|
|
> **Context:** `python cli.py done 99` on a list with 3 tasks raises an uncaught `IndexError` and dumps a traceback. `python cli.py done abc` raises `ValueError`. Either way the user sees a stack trace instead of a helpful message.
|
|
>
|
|
> **Acceptance criteria:**
|
|
> - `done <index>` with an out-of-range index prints a clear error (e.g. `no task at index 99`) and exits non-zero.
|
|
> - `done <non-integer>` prints a clear error and exits non-zero.
|
|
> - A valid `done <index>` still works exactly as before.
|
|
>
|
|
> **Out of scope:** changing how tasks are stored or numbered.
|
|
|
|
That second one is pickup-ready. It's also, not coincidentally, exactly the format an agent needs. Same artifact, two readers.
|
|
|
|
[insert a screenshot referencing a well-formed GitHub issue for tasks-app, showing title, context, and a checkbox acceptance-criteria list here]
|
|
|
|
## Labels describe; assignment routes
|
|
|
|
A title says what one issue *is*. **Labels** are how you slice the whole backlog at once. Keep the taxonomy small and orthogonal — a few axes, not forty decorative tags:
|
|
|
|
- **Type** — `bug`, `feature`, `chore`. What kind of work.
|
|
- **Priority** — `p1`/`p2`/`p3`. How much it matters.
|
|
- **Area** — `cli`, `storage`, `docs`. Which part of the system.
|
|
- **Readiness** — a single `ready` label meaning "well-formed enough to start." This one earns its keep in the AI era: it's the signal that an issue has solid acceptance criteria and can be handed off — to a person *or* an agent — without more discussion.
|
|
|
|
Resist label sprawl. If a label never changes how you filter or who picks up the work, delete it. Five labels you trust beat thirty you don't.
|
|
|
|
Then there's **assignment**, which is different from labeling and does the thing labels can't: it routes. Assigning an issue puts *one* name on it — the owner, the person (or agent) the rest of the team can assume is handling it. The discipline that matters is *one* owner; an issue assigned to three people is assigned to no one. (Unassigned-but-`ready` is a fine state too — it just means "available, grab it.")
|
|
|
|
## The roster is mixed now
|
|
|
|
And here's the actual point of this post, the thing that makes a 2026 issue tracker different from a 2015 one.
|
|
|
|
The list of things you can assign an issue *to* used to be "the people on the team." It increasingly includes **agents.** An issue can be routed to a person, or handed to an issue-to-PR agent that reads the issue, makes the change on a branch, and opens it up for review. (Building that agent is a whole module later in the course — Unit 5 — and we're not doing it here. The point right now is just that it's a possible *assignee*, and that changes how you write the issue.)
|
|
|
|
The exact mechanism is still settling and differs everywhere — some forges let you assign an agent like a user, some trigger it with a label, some kick it off from a comment. Don't anchor on the plumbing. Anchor on this: **the well-formed issue is the one interface that works for every assignee on the roster.** A human and an agent need the same things from an issue — clear title, real context, acceptance criteria that define done. Write it well and you've written it for both.
|
|
|
|
So how do you decide who gets what? The heuristic that's served me is this, and notice it's a property of the *issue*, not the model:
|
|
|
|
**Hand it to an agent when the work is well-scoped, has concrete acceptance criteria, and follows a pattern already in the codebase.** A `delete <index>` command for our `tasks-app` is a perfect candidate — it mirrors the existing `done` command almost exactly, "delete" is unambiguous, and you can verify the result in seconds. The bug above is another: contained, reproducible, testable.
|
|
|
|
**Keep it with a human when the issue carries real ambiguity, design judgment, or cross-cutting risk.** "Add task priorities" sounds small but isn't — how many levels? Does the list re-sort? How are priorities displayed and stored? Those are product decisions an agent will *answer confidently and probably wrongly*, because nothing in the issue tells it the right call. A human resolves the ambiguity first, often by splitting it into clear sub-issues — at which point the pieces may *become* agent-ready.
|
|
|
|
Notice what the heuristic doesn't ask: how smart the model is. It asks how well-specified the *work* is. A vague issue degrades gracefully with a human — they ask you a question — and catastrophically with an agent, which guesses and produces a confident, plausible, wrong PR.
|
|
|
|
## The AI angle: your issue is now a task spec
|
|
|
|
A generic project-management lesson would teach the exact same issue tracker. What's specific to AI-assisted work is that **the issue has quietly become an agent's task specification**, and that raises the stakes on writing it well in a few concrete ways:
|
|
|
|
- **Acceptance criteria are the agent's definition of done.** A human reads fuzzy criteria and fills the gaps with judgment. An agent reads them literally and stops the moment they're satisfied — so vague criteria produce work that's technically complete and actually wrong.
|
|
- **A bad issue fails an agent harder than a human.** The failure modes aren't symmetric. Hand a person an underspecified ticket and you get a question. Hand an agent the same ticket and you get a confident, plausible, wrong PR that costs *more* to review than the work would have taken. The cheap insurance is the clarity you put in *before* assigning.
|
|
- **Your committed config plus the issue is the whole brief.** That AI instructions file you committed a few modules back carries the standing context — conventions, build and test commands, what not to touch. The issue carries the specific task. Together they're enough for an agent to attempt the work with no live conversation at all.
|
|
|
|
The reframe: writing a clear issue used to be a courtesy to your teammates. Now it's the difference between an agent that ships the right change and one that burns a review cycle. The skill got *more* valuable, not less.
|
|
|
|
## Try it on the tasks-app
|
|
|
|
The lab is deliberately low-stakes — you're writing issues, not code, so your AI assistant can stay in a browser tab. Against the `tasks-app` repo you pushed to a forge:
|
|
|
|
1. **Find three real pieces of work.** A bug (`python cli.py done 99` and `done abc` both crash — run them and watch), a small patterned feature (`delete <index>`, mirroring `done`), and a judgment-heavy one (task priorities).
|
|
2. **Draft all three as well-formed issues** — title, context with repro steps, acceptance criteria, out-of-scope. This is a great place to *use* the AI: paste a file, ask it to draft acceptance criteria, then **edit them down.** The model over-produces; tightening its draft is exactly the skill.
|
|
3. **Create, label, and route them.** Assign the priorities feature to a human (you — it has open design questions). Earmark the bug and the `delete` feature for an agent — actual agent assignee, an `agent-ready` label, or just a note saying "suitable for an issue-to-PR agent." The mechanism doesn't matter yet; the *decision* does.
|
|
4. **Write one sentence per issue explaining why it went where it went** — in terms of the issue's clarity, not the model's smarts. That sentence *is* the routing skill.
|
|
|
|
Then filter your forge's issue list by the `ready` label. What you're looking at is exactly the work that's pickable right now, by anyone or anything, with nobody explaining anything. That filtered view is the shared task memory, made real.
|
|
|
|
## Where it breaks
|
|
|
|
Issues are not the repo, and they don't behave like it — a few honest caveats:
|
|
|
|
- **Issues lie when they go stale; git doesn't.** The repo is ground truth by construction — it *is* the code. An issue is a *claim* about work, and claims rot. A backlog full of issues that were fixed months ago is worse than no backlog, because people and agents *trust* it. Closing issues is as much a discipline as opening them.
|
|
- **Acceptance criteria can't capture genuine ambiguity.** The whole agent-ready-vs-human split assumes you *can* write clear criteria. For real design problems you can't yet — and that's not a writing failure, it's the nature of the work. Forcing crisp criteria onto an open question just hides the question.
|
|
- **Routing to an agent is delegation, not abdication.** "Assign to agent" means "an agent does the first pass," not "an agent merges to `main`." Everything it produces still lands as a reviewable pull request behind the review and CI gates that come later in the course. If your mental model is the latter, fix it now.
|
|
- **Over-tooling a tiny project is its own failure.** A solo throwaway script does not need a labeled, prioritized backlog. Issues earn their keep when work is shared — across people, across agents, or across enough time that you'd otherwise forget. Below that, a `TODO` comment is fine.
|
|
|
|
## You're done when
|
|
|
|
You've got three well-formed issues on your forge for `tasks-app` — each with a title, context, and concrete acceptance criteria, not a one-line "fix the thing." At least one is routed to a human, at least one is earmarked for an agent, and you can state *why* in terms of the issue's clarity rather than the model's intelligence. When a stranger could pick up any of your `ready` issues and start without asking you a single question, you've written them well.
|
|
|
|
Which is the whole setup for what's next: somebody — or something — picks up one of those issues, does the work on a branch, and opens it back up as a pull request for you to review. Reviewing a change you didn't write, possibly *couldn't* have written as fast, is one of the most important and least-taught skills in this entire space. That's the next post.
|
|
|
|
Following along, or routing work to agents already in your day job? I want to hear how it's actually going — the mechanics are still settling and the field reports are gold. Drop a comment; I read them.
|