Files
ai-workflow-course/blog/10-issues-task-layer.md
T
claude f47ccee96d docs(blog): add 17-post jpaul.me blog series for the course
A standalone blog/ folder (not course content) with drafts for jpaul.me:
an announcement, a getting-started piece, then a hybrid weekly series —
one post per module for Units 1-2 (posts 03-13) and one per unit for the
back half (14-16) plus a capstone finale (17). Each post carries WordPress
metadata, a [COURSE LINK] placeholder, and [insert screenshot] blocks for
Justin to fill before publishing. README.md holds the manifest + checklist.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EghAChc9UbcF78t55mfdf
2026-06-22 17:49:00 -04:00

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]([COURSE LINK]), 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.