Files
ai-workflow-course/blog/10-issues-task-layer.md
2026-06-23 07:28:55 -04:00

14 KiB

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, 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, meaning "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 (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; 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.